diff --git a/.gitignore b/.gitignore index d32d1b6dc..c4745294d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ _dist/ bin/ vendor/ +*.swp diff --git a/cmd/service/service.go b/cmd/service/service.go index a7fd57254..1e6dcb11b 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -4,10 +4,9 @@ import ( "fmt" "net/http" - "helm.sh/helm/v3/pkg/http/api/install" + "helm.sh/helm/v3/pkg/http/api" "helm.sh/helm/v3/pkg/http/api/list" "helm.sh/helm/v3/pkg/http/api/ping" - "helm.sh/helm/v3/pkg/http/api/upgrade" "helm.sh/helm/v3/pkg/servercontext" ) @@ -18,10 +17,13 @@ func main() { func startServer(appconfig *servercontext.Application) { router := http.NewServeMux() + //TODO: use gorilla mux and add middleware to write content type and other headers + cfg := servercontext.App().Config + service := api.NewService(cfg) router.Handle("/ping", ping.Handler()) router.Handle("/list", list.Handler()) - router.Handle("/install", install.Handler()) - router.Handle("/upgrade", upgrade.Handler()) + router.Handle("/install", api.Install(service)) + router.Handle("/upgrade", api.Upgrade(service)) err := http.ListenAndServe(fmt.Sprintf(":%d", 8080), router) if err != nil { diff --git a/go.mod b/go.mod index c7b25ac13..d53bb76b1 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.1.0 github.com/xenolf/lego v0.3.2-0.20160613233155-a9d8cec0e656 // indirect + go.uber.org/zap v1.10.0 golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152 golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 // indirect golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934 // indirect diff --git a/go.sum b/go.sum index 2cacc86b0..814bac222 100644 --- a/go.sum +++ b/go.sum @@ -506,8 +506,11 @@ go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/pkg/http/api/install.go b/pkg/http/api/install.go new file mode 100644 index 000000000..6acb31ec4 --- /dev/null +++ b/pkg/http/api/install.go @@ -0,0 +1,47 @@ +package api + +import ( + "encoding/json" + "net/http" + + "helm.sh/helm/v3/pkg/http/api/logger" +) + +type InstallRequest struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Chart string `json:"chart"` + Values map[string]interface{} `json:"values"` +} + +type InstallResponse struct { + Error string `json:"error,omitempty"` + Status string +} + +// RODO: we could use interface as well if everything's in same package +func Install(svc Service) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + var req InstallRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Errorf("[Install] error decoding request: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + defer r.Body.Close() + var response InstallResponse + res, err := svc.Install(r.Context(), req.Chart, req.Values) + if err != nil { + response.Error = err.Error() + logger.Errorf("[Install] error while installing chart: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + response.Status = res.status + if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Errorf("[Install] error writing response %v", err) + return + } + }) +} diff --git a/pkg/http/api/install/installcontract.go b/pkg/http/api/install/installcontract.go deleted file mode 100644 index bf6c6bb4f..000000000 --- a/pkg/http/api/install/installcontract.go +++ /dev/null @@ -1,14 +0,0 @@ -package install - -type InstallRequest struct { - RequestID string - ReleaseName string - ReleaseNamespace string - ChartPath string - Values string -} - -type InstallResponse struct { - Status bool - ReleaseStatus string -} diff --git a/pkg/http/api/install/installhandler.go b/pkg/http/api/install/installhandler.go deleted file mode 100644 index 3dbcbdef3..000000000 --- a/pkg/http/api/install/installhandler.go +++ /dev/null @@ -1,85 +0,0 @@ -package install - -import ( - "encoding/json" - "fmt" - "net/http" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/cli/values" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/servercontext" -) - -func Handler() http.Handler { - return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Header().Set("Content-Type", "application/json") - defer req.Body.Close() - - var request InstallRequest - decoder := json.NewDecoder(req.Body) - decoder.UseNumber() - - if err := decoder.Decode(&request); err != nil { - fmt.Printf("error in request: %v", err) - return - } - - request.RequestID = req.Header.Get("Request-Id") - request.ReleaseName = req.Header.Get("Release-Name") - request.ReleaseNamespace = req.Header.Get("Release-Namespace") - request.ChartPath = req.Header.Get("Chart-Path") - request.Values = req.Header.Get("Values") - - valueOpts := &values.Options{} - valueOpts.Values = append(valueOpts.Values, request.Values) - - status, releaseStatus, err := InstallChart(request.ReleaseName, request.ReleaseNamespace, request.ChartPath, valueOpts) - if err != nil { - fmt.Printf("error in request: %v", err) - return - } - - response := InstallResponse{Status: status, ReleaseStatus: releaseStatus} - payload, err := json.Marshal(response) - if err != nil { - fmt.Printf("error parsing response %v", err) - return - } - - res.Write(payload) - }) -} - -func InstallChart(releaseName, releaseNamespace, chartPath string, valueOpts *values.Options) (bool, string, error) { - install := action.NewInstall(servercontext.App().ActionConfig) - install.ReleaseName = releaseName - install.Namespace = releaseNamespace - - vals, err := valueOpts.MergeValues(getter.All(servercontext.App().Config)) - if err != nil { - return false, "", err - } - - cp, err := install.ChartPathOptions.LocateChart(chartPath, servercontext.App().Config) - if err != nil { - fmt.Printf("error in locating chart: %v", err) - return false, "", err - } - - var requestedChart *chart.Chart - if requestedChart, err = loader.Load(cp); err != nil { - fmt.Printf("error in loading chart: %v", err) - return false, "", err - } - - release, err := install.Run(requestedChart, vals) - if err != nil { - fmt.Printf("error in installing chart: %v", err) - return false, "", err - } - - return true, release.Info.Status.String(), nil -} diff --git a/pkg/http/api/logger/logger.go b/pkg/http/api/logger/logger.go new file mode 100644 index 000000000..534c3f902 --- /dev/null +++ b/pkg/http/api/logger/logger.go @@ -0,0 +1,40 @@ +package logger + +import ( + "go.uber.org/zap" +) + +var log *zap.SugaredLogger + +func Debugf(fmt string, args ...interface{}) { + log.Debugf(fmt, args...) +} + +func Fatalf(fmt string, args ...interface{}) { + log.Fatalf(fmt, args...) +} + +func Errorf(fmt string, args ...interface{}) { + log.Errorf(fmt, args...) +} + +func Infof(fmt string, args ...interface{}) { + log.Infof(fmt, args...) +} + +func Setup(level string) { + //TODO: map from env config + var zlog *zap.Logger + switch level { + case "debug": + zlog, _ = zap.NewDevelopment() + case "none": + zlog = zap.NewNop() + default: + zlog, _ = zap.NewProduction() + } + // This condition is to avoid race conditions in test cases + if log == nil { + log = zlog.Sugar() + } +} diff --git a/pkg/http/api/service.go b/pkg/http/api/service.go new file mode 100644 index 000000000..b84cea945 --- /dev/null +++ b/pkg/http/api/service.go @@ -0,0 +1,71 @@ +package api + +import ( + "context" + "fmt" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/servercontext" +) + +type Service struct { + config *cli.EnvSettings + install *action.Install +} + +type chartValues map[string]interface{} + +type installResult struct { + status string +} + +func (s Service) getValues(vals chartValues) (chartValues, error) { + valueOpts := &values.Options{} + //valueOpts.Values = append(valueOpts.Values, vals) + return valueOpts.MergeValues(getter.All(servercontext.App().Config)) +} + +func (s Service) Install(ctx context.Context, chartName string, values chartValues) (installResult, error) { + var result installResult + chart, err := s.loadChart(chartName) + if err != nil { + return result, err + } + vals, err := s.getValues(values) + if err != nil { + return result, fmt.Errorf("error merging values: %v", err) + } + release, err := s.install.Run(chart, vals) + if err != nil { + return result, fmt.Errorf("error in installing chart: %v", err) + } + if release.Info != nil { + result.status = release.Info.Status.String() + } + return result, nil +} + +func (s Service) loadChart(chartName string) (*chart.Chart, error) { + cp, err := s.install.ChartPathOptions.LocateChart(chartName, s.config) + if err != nil { + return nil, fmt.Errorf("error in locating chart: %v", err) + } + var requestedChart *chart.Chart + if requestedChart, err = loader.Load(cp); err != nil { + return nil, fmt.Errorf("error loading chart: %v", err) + } + return requestedChart, nil +} + +func NewService(cfg *cli.EnvSettings) Service { + return Service{ + config: cfg, + //TODO: not sure why this's needed, but we can refactor later,could be passed as param + install: action.NewInstall(servercontext.App().ActionConfig), + } +} diff --git a/pkg/http/api/upgrade.go b/pkg/http/api/upgrade.go new file mode 100644 index 000000000..7533b3308 --- /dev/null +++ b/pkg/http/api/upgrade.go @@ -0,0 +1,124 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/servercontext" + "helm.sh/helm/v3/pkg/storage/driver" +) + +type UpgradeRequest struct { + RequestID string + ReleaseName string + ReleaseNamespace string + ChartPath string +} + +type UpgradeResponse struct { + Status bool + ReleaseStatus string +} + +type UpgradeHandler struct { + service Service +} + +func Upgrade(service Service) http.Handler { + return UpgradeHandler{service: service} +} + +func (h UpgradeHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { + res.Header().Set("Content-Type", "application/json") + defer req.Body.Close() + + var request UpgradeRequest + decoder := json.NewDecoder(req.Body) + decoder.UseNumber() + + if err := decoder.Decode(&request); err != nil { + fmt.Printf("error in request: %v", err) + return + } + + request.RequestID = req.Header.Get("Request-Id") + request.ReleaseName = req.Header.Get("Release-Name") + request.ReleaseNamespace = req.Header.Get("Release-Namespace") + request.ChartPath = req.Header.Get("Chart-Path") + valueOpts := &values.Options{} + + status, releaseStatus, err := h.UpgradeRelease(request.ReleaseName, request.ReleaseNamespace, request.ChartPath, valueOpts) + if err != nil { + fmt.Printf("error in request: %v", err) + return + } + + response := UpgradeResponse{Status: status, ReleaseStatus: releaseStatus} + payload, err := json.Marshal(response) + if err != nil { + fmt.Printf("error parsing response %v", err) + return + } + + res.Write(payload) +} + +func (h UpgradeHandler) UpgradeRelease(releaseName, releaseNamespace, chartPath string, valueOpts *values.Options) (bool, string, error) { + upgrade := action.NewUpgrade(servercontext.App().ActionConfig) + upgrade.Namespace = releaseNamespace + + vals, err := valueOpts.MergeValues(getter.All(servercontext.App().Config)) + if err != nil { + return false, "", err + } + + chartPath, err = upgrade.ChartPathOptions.LocateChart(chartPath, servercontext.App().Config) + if err != nil { + return false, "", err + } + if upgrade.Install { + history := action.NewHistory(servercontext.App().ActionConfig) + history.Max = 1 + if _, err := history.Run(releaseName); err == driver.ErrReleaseNotFound { + fmt.Printf("Release %q does not exist. Installing it now.\n", releaseName) + + //TODO: yet to accomodate namespace and releasename, just refactoring + release, err := h.service.Install(context.TODO(), chartPath, vals) + if err != nil { + fmt.Printf("error in request: %v", err) + return false, "", err + } + return true, release.status, nil + } + } + + ch, err := loader.Load(chartPath) + if err != nil { + fmt.Printf("error in loading: %v", err) + return false, "", err + } + if req := ch.Metadata.Dependencies; req != nil { + if err := action.CheckDependencies(ch, req); err != nil { + fmt.Printf("error in dependencies: %v", err) + return false, "", err + } + } + + if ch.Metadata.Deprecated { + fmt.Printf("WARNING: This chart is deprecated") + } + + release, err := upgrade.Run(releaseName, ch, vals) + if err != nil { + fmt.Printf("error in installing chart: %v", err) + return false, "", err + } + + return true, release.Info.Status.String(), nil +} diff --git a/pkg/http/api/upgrade/upgradecontract.go b/pkg/http/api/upgrade/upgradecontract.go deleted file mode 100644 index c5557a850..000000000 --- a/pkg/http/api/upgrade/upgradecontract.go +++ /dev/null @@ -1,13 +0,0 @@ -package upgrade - -type UpgradeRequest struct { - RequestID string - ReleaseName string - ReleaseNamespace string - ChartPath string -} - -type UpgradeResponse struct { - Status bool - ReleaseStatus string -} diff --git a/pkg/http/api/upgrade/upgradehandler.go b/pkg/http/api/upgrade/upgradehandler.go deleted file mode 100644 index 5949459eb..000000000 --- a/pkg/http/api/upgrade/upgradehandler.go +++ /dev/null @@ -1,105 +0,0 @@ -package upgrade - -import ( - "encoding/json" - "fmt" - "net/http" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/cli/values" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/http/api/install" - "helm.sh/helm/v3/pkg/servercontext" - "helm.sh/helm/v3/pkg/storage/driver" -) - -func Handler() http.Handler { - return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Header().Set("Content-Type", "application/json") - defer req.Body.Close() - - var request UpgradeRequest - decoder := json.NewDecoder(req.Body) - decoder.UseNumber() - - if err := decoder.Decode(&request); err != nil { - fmt.Printf("error in request: %v", err) - return - } - - request.RequestID = req.Header.Get("Request-Id") - request.ReleaseName = req.Header.Get("Release-Name") - request.ReleaseNamespace = req.Header.Get("Release-Namespace") - request.ChartPath = req.Header.Get("Chart-Path") - valueOpts := &values.Options{} - - status, releaseStatus, err := UpgradeRelease(request.ReleaseName, request.ReleaseNamespace, request.ChartPath, valueOpts) - if err != nil { - fmt.Printf("error in request: %v", err) - return - } - - response := UpgradeResponse{Status: status, ReleaseStatus: releaseStatus} - payload, err := json.Marshal(response) - if err != nil { - fmt.Printf("error parsing response %v", err) - return - } - - res.Write(payload) - }) -} - -func UpgradeRelease(releaseName, releaseNamespace, chartPath string, valueOpts *values.Options) (bool, string, error) { - upgrade := action.NewUpgrade(servercontext.App().ActionConfig) - upgrade.Namespace = releaseNamespace - - vals, err := valueOpts.MergeValues(getter.All(servercontext.App().Config)) - if err != nil { - return false, "", err - } - - chartPath, err = upgrade.ChartPathOptions.LocateChart(chartPath, servercontext.App().Config) - if err != nil { - return false, "", err - } - if upgrade.Install { - history := action.NewHistory(servercontext.App().ActionConfig) - history.Max = 1 - if _, err := history.Run(releaseName); err == driver.ErrReleaseNotFound { - fmt.Printf("Release %q does not exist. Installing it now.\n", releaseName) - - status, releaseStatus, err := install.InstallChart(releaseName, releaseNamespace, chartPath, valueOpts) - if err != nil { - fmt.Printf("error in request: %v", err) - return false, "", err - } - return status, releaseStatus, nil - } - } - - ch, err := loader.Load(chartPath) - if err != nil { - fmt.Printf("error in loading: %v", err) - return false, "", err - } - if req := ch.Metadata.Dependencies; req != nil { - if err := action.CheckDependencies(ch, req); err != nil { - fmt.Printf("error in dependencies: %v", err) - return false, "", err - } - } - - if ch.Metadata.Deprecated { - fmt.Printf("WARNING: This chart is deprecated") - } - - release, err := upgrade.Run(releaseName, ch, vals) - if err != nil { - fmt.Printf("error in installing chart: %v", err) - return false, "", err - } - - return true, release.Info.Status.String(), nil -} diff --git a/pkg/servercontext/context.go b/pkg/servercontext/context.go index c8867a0c1..1d72d980d 100644 --- a/pkg/servercontext/context.go +++ b/pkg/servercontext/context.go @@ -9,6 +9,7 @@ import ( "helm.sh/helm/v3/pkg/cli" ) +//TODO: rename package var app Application type Application struct {