diff --git a/cmd/service/service.go b/cmd/service/service.go index a9138ee4e..879ee449b 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/http/api" "helm.sh/helm/v3/pkg/http/api/list" "helm.sh/helm/v3/pkg/http/api/logger" @@ -22,7 +23,9 @@ func startServer(appconfig *servercontext.Application) { //TODO: use gorilla mux and add middleware to write content type and other headers app := servercontext.App() logger.Setup("debug") - service := api.NewService(app.Config, app.ActionConfig) + actionInstall := action.NewInstall(app.ActionConfig) + + service := api.NewService(app.Config, new(action.ChartPathOptions), api.NewInstaller(actionInstall)) router.Handle("/ping", ping.Handler()) router.Handle("/list", list.Handler()) router.Handle("/install", api.Install(service)) diff --git a/go.mod b/go.mod index d53bb76b1..c4d4c0214 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( google.golang.org/appengine v1.6.5 // indirect google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 // indirect google.golang.org/grpc v1.24.0 // indirect + gotest.tools v2.2.0+incompatible k8s.io/api v0.17.1 k8s.io/apiextensions-apiserver v0.17.1 k8s.io/apimachinery v0.17.1 diff --git a/go.sum b/go.sum index 814bac222..5bda28ecd 100644 --- a/go.sum +++ b/go.sum @@ -469,6 +469,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/pkg/http/api/install.go b/pkg/http/api/install.go index b5f76de29..b9945aac4 100644 --- a/pkg/http/api/install.go +++ b/pkg/http/api/install.go @@ -16,7 +16,7 @@ type InstallRequest struct { type InstallResponse struct { Error string `json:"error,omitempty"` - Status string + Status string `json:"status"` } // RODO: we could use interface as well if everything's in same package @@ -40,7 +40,7 @@ func Install(svc Service) http.Handler { return } response.Status = res.status - if err := json.NewEncoder(w).Encode(response); err != nil { + if err := json.NewEncoder(w).Encode(&response); err != nil { logger.Errorf("[Install] error writing response %v", err) w.WriteHeader(http.StatusInternalServerError) return diff --git a/pkg/http/api/install_api_test.go b/pkg/http/api/install_api_test.go new file mode 100644 index 000000000..d9d9b4f7c --- /dev/null +++ b/pkg/http/api/install_api_test.go @@ -0,0 +1,98 @@ +package api_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "gotest.tools/assert" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/http/api" + "helm.sh/helm/v3/pkg/http/api/logger" + "helm.sh/helm/v3/pkg/release" +) + +type InstallerTestSuite struct { + suite.Suite + recorder *httptest.ResponseRecorder + server *httptest.Server + mockInstaller *mockInstaller + mockChartLoader *mockChartLoader + appConfig *cli.EnvSettings +} + +func (s *InstallerTestSuite) SetupSuite() { + logger.Setup("default") +} + +func (s *InstallerTestSuite) SetupTest() { + s.recorder = httptest.NewRecorder() + s.mockInstaller = new(mockInstaller) + s.mockChartLoader = new(mockChartLoader) + s.appConfig = &cli.EnvSettings{ + RepositoryConfig: "./testdata/helm", + PluginsDirectory: "./testdata/helm/plugin", + } + service := api.NewService(s.appConfig, s.mockChartLoader, s.mockInstaller) + handler := api.Install(service) + s.server = httptest.NewServer(handler) +} + +func (s *InstallerTestSuite) TestShouldReturnDeployedStatusOnSuccessfulInstall() { + chartName := "stable/redis-ha" + body := fmt.Sprintf(`{ + "chart":"%s", + "name": "redis-v5", + "namespace": "something"}`, chartName) + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/install", s.server.URL), strings.NewReader(body)) + s.mockChartLoader.On("LocateChart", chartName, s.appConfig).Return("./testdata/albatross", nil) + icfg := api.InstallConfig{ChartName: chartName, Name: "redis-v5", Namespace: "something"} + s.mockInstaller.On("SetConfig", icfg) + release := &release.Release{Info: &release.Info{Status: release.StatusDeployed}} + var vals map[string]interface{} + //TODO: pass chart object and verify values present testdata chart yml + s.mockInstaller.On("Run", mock.AnythingOfType("*chart.Chart"), vals).Return(release, nil) + + resp, err := http.DefaultClient.Do(req) + + assert.Equal(s.T(), 200, resp.StatusCode) + expectedResponse := `{"status":"deployed"}` + "\n" + respBody, _ := ioutil.ReadAll(resp.Body) + assert.Equal(s.T(), expectedResponse, string(respBody)) + require.NoError(s.T(), err) + s.mockInstaller.AssertExpectations(s.T()) + s.mockChartLoader.AssertExpectations(s.T()) +} + +func (s *InstallerTestSuite) TearDownTest() { + s.server.Close() +} + +func TestInstallAPI(t *testing.T) { + suite.Run(t, new(InstallerTestSuite)) +} + +type mockInstaller struct{ mock.Mock } + +func (m *mockInstaller) SetConfig(cfg api.InstallConfig) { + m.Called(cfg) +} + +func (m *mockInstaller) Run(c *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + args := m.Called(c, vals) + return args.Get(0).(*release.Release), args.Error(1) +} + +type mockChartLoader struct{ mock.Mock } + +func (m *mockChartLoader) LocateChart(name string, settings *cli.EnvSettings) (string, error) { + args := m.Called(name, settings) + return args.String(0), args.Error(1) +} diff --git a/pkg/http/api/installer.go b/pkg/http/api/installer.go new file mode 100644 index 000000000..07a6402c1 --- /dev/null +++ b/pkg/http/api/installer.go @@ -0,0 +1,24 @@ +package api + +import ( + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" +) + +type Installer struct { + *action.Install +} + +type runner interface { + Run(*chart.Chart, map[string]interface{}) (*release.Release, error) +} + +func (i *Installer) SetConfig(cfg InstallConfig) { + i.ReleaseName = cfg.Name + i.Namespace = cfg.Namespace +} + +func NewInstaller(ai *action.Install) *Installer { + return &Installer{ai} +} diff --git a/pkg/http/api/service.go b/pkg/http/api/service.go index 8496525a4..af3b069d7 100644 --- a/pkg/http/api/service.go +++ b/pkg/http/api/service.go @@ -4,21 +4,27 @@ 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/http/api/logger" - "helm.sh/helm/v3/pkg/servercontext" + "helm.sh/helm/v3/pkg/release" _ "k8s.io/client-go/plugin/pkg/client/auth" ) +type installer interface { + SetConfig(InstallConfig) + Run(*chart.Chart, map[string]interface{}) (*release.Release, error) +} + +type chartloader interface { + LocateChart(name string, settings *cli.EnvSettings) (string, error) +} + type Service struct { - settings *cli.EnvSettings - actionConfig *action.Configuration - chartloader *action.ChartPathOptions + settings *cli.EnvSettings + installer + chartloader } type InstallConfig struct { @@ -34,10 +40,11 @@ type installResult struct { } func (s Service) getValues(vals chartValues) (chartValues, error) { - valueOpts := &values.Options{} + // valueOpts := &values.Options{} //valueOpts.Values = append(valueOpts.Values, vals) //TODO: we need to make this as Provider, so it'll be able to merge - return valueOpts.MergeValues(getter.All(servercontext.App().Config)) + // why do we need getter.ALl? + return vals, nil } func (s Service) Install(ctx context.Context, cfg InstallConfig, values chartValues) (*installResult, error) { @@ -66,26 +73,22 @@ func (s Service) loadChart(chartName string) (*chart.Chart, error) { } func (s Service) installChart(icfg InstallConfig, ch *chart.Chart, vals chartValues) (*installResult, error) { - install := action.NewInstall(s.actionConfig) - install.Namespace = icfg.Namespace - install.ReleaseName = icfg.Name - - release, err := install.Run(ch, vals) + s.installer.SetConfig(icfg) + release, err := s.installer.Run(ch, vals) if err != nil { return nil, fmt.Errorf("error in installing chart: %v", err) } result := new(installResult) - fmt.Println(result) if release.Info != nil { result.status = release.Info.Status.String() } return result, nil } -func NewService(settings *cli.EnvSettings, actionConfig *action.Configuration) Service { +func NewService(settings *cli.EnvSettings, cl chartloader, i installer) Service { return Service{ - settings: settings, - actionConfig: actionConfig, - chartloader: new(action.ChartPathOptions), + settings: settings, + chartloader: cl, + installer: i, } } diff --git a/pkg/http/api/testdata/albatross/Chart.yaml b/pkg/http/api/testdata/albatross/Chart.yaml new file mode 100644 index 000000000..eeef737ff --- /dev/null +++ b/pkg/http/api/testdata/albatross/Chart.yaml @@ -0,0 +1,4 @@ +name: albatross +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/http/api/testdata/albatross/values.yaml b/pkg/http/api/testdata/albatross/values.yaml new file mode 100644 index 000000000..3121cd7ce --- /dev/null +++ b/pkg/http/api/testdata/albatross/values.yaml @@ -0,0 +1,4 @@ +albatross: "true" + +global: + author: Coleridge diff --git a/pkg/http/api/testdata/helm/repositories.yaml b/pkg/http/api/testdata/helm/repositories.yaml new file mode 100644 index 000000000..e69de29bb