diff --git a/.golangci.yml b/.golangci.yml index 2c3b623..9e9707e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,6 +22,6 @@ linters-settings: gofmt: simplify: true goimports: - local-prefixes: helm.sh/helm/v3 + local-prefixes: github.com/gojekfarm/albatross dupl: threshold: 400 diff --git a/api/chartloader.go b/api/chartloader.go deleted file mode 100644 index e935ca7..0000000 --- a/api/chartloader.go +++ /dev/null @@ -1,9 +0,0 @@ -package api - -import ( - "helm.sh/helm/v3/pkg/cli" -) - -type chartloader interface { - LocateChart(name string, settings *cli.EnvSettings) (string, error) -} diff --git a/api/install.go b/api/install.go deleted file mode 100644 index 71d49e8..0000000 --- a/api/install.go +++ /dev/null @@ -1,57 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gojekfarm/albatross/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 `json:"status,omitempty"` -} - -// Install return an http handler that handles the install request -// TODO: 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 - cfg := ReleaseConfig{ChartName: req.Chart, Name: req.Name, Namespace: req.Namespace} - res, err := svc.Install(r.Context(), cfg, req.Values) - if err != nil { - respondInstallError(w, "error while installing chart: %v", err) - return - } - response.Status = res.Status - if err := json.NewEncoder(w).Encode(&response); err != nil { - respondInstallError(w, "error writing response: %v", err) - return - } - }) -} - -func respondInstallError(w http.ResponseWriter, logprefix string, err error) { - response := InstallResponse{Error: err.Error()} - w.WriteHeader(http.StatusInternalServerError) - if err := json.NewEncoder(w).Encode(&response); err != nil { - logger.Errorf("[Install] %s %v", logprefix, err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/api/install/install.go b/api/install/install.go new file mode 100644 index 0000000..8fe75b4 --- /dev/null +++ b/api/install/install.go @@ -0,0 +1,77 @@ +package install + +import ( + "encoding/json" + "net/http" + "time" + + "helm.sh/helm/v3/pkg/release" + + "github.com/gojekfarm/albatross/pkg/helmcli/flags" + "github.com/gojekfarm/albatross/pkg/logger" +) + +type Request struct { + Name string + Chart string + Values map[string]interface{} + Flags Flags +} + +type Flags struct { + DryRun bool `json:"dry_run"` + Version string + flags.GlobalFlags +} + +type Release struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Version int `json:"version"` + Updated time.Time `json:"updated_at,omitempty"` + Status release.Status `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` +} + +type Response struct { + Error string `json:"error,omitempty"` + Status string `json:"status,omitempty"` + Data string `json:"data,omitempty"` + Release `json:"-"` +} + +func Handler(service service) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var req Request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Errorf("[Install] error decoding request: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + resp, err := service.Install(r.Context(), req) + if err != nil { + respondInstallError(w, "error while installing chart: %v", err) + return + } + + if err := json.NewEncoder(w).Encode(&resp); err != nil { + respondInstallError(w, "error writing response: %v", err) + return + } + }) +} + +// TODO: This does not handle different status codes. +func respondInstallError(w http.ResponseWriter, logprefix string, err error) { + response := Response{Error: err.Error()} + w.WriteHeader(http.StatusInternalServerError) + if err := json.NewEncoder(w).Encode(&response); err != nil { + logger.Errorf("[Install] %s %v", logprefix, err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/api/install/install_api_test.go b/api/install/install_api_test.go new file mode 100644 index 0000000..9bfdb74 --- /dev/null +++ b/api/install/install_api_test.go @@ -0,0 +1,106 @@ +package install + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/gojekfarm/albatross/pkg/logger" + + "helm.sh/helm/v3/pkg/release" +) + +type mockService struct { + mock.Mock +} + +func (s *mockService) Install(ctx context.Context, req Request) (Response, error) { + args := s.Called(ctx, req) + return args.Get(0).(Response), args.Error(1) +} + +type InstallerTestSuite struct { + suite.Suite + recorder *httptest.ResponseRecorder + server *httptest.Server + mockService *mockService +} + +func (s *InstallerTestSuite) SetupSuite() { + logger.Setup("default") +} + +func (s *InstallerTestSuite) SetupTest() { + s.recorder = httptest.NewRecorder() + s.mockService = new(mockService) + handler := Handler(s.mockService) + s.server = httptest.NewServer(handler) +} + +func (s *InstallerTestSuite) TestShouldReturnDeployedStatusOnSuccessfulInstall() { + chartName := "stable/redis-ha" + body := fmt.Sprintf(`{"chart":"%s", "name": "redis-v5", "values": {"replicas": 2}, "flags": {"namespace": "albatross"}}`, chartName) + + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/install", s.server.URL), strings.NewReader(body)) + response := Response{ + Status: release.StatusDeployed.String(), + } + + s.mockService.On("Install", mock.Anything, mock.AnythingOfType("Request")).Return(response, nil) + + resp, err := http.DefaultClient.Do(req) + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + expectedResponse := `{"status":"deployed"}` + "\n" + respBody, _ := ioutil.ReadAll(resp.Body) + assert.Equal(s.T(), expectedResponse, string(respBody)) + require.NoError(s.T(), err) + s.mockService.AssertExpectations(s.T()) +} + +func (s *InstallerTestSuite) TestShouldReturnInternalServerErrorOnFailure() { + chartName := "stable/redis-ha" + body := fmt.Sprintf(`{"chart":"%s", "name": "redis-v5"}`, chartName) + + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/install", s.server.URL), strings.NewReader(body)) + s.mockService.On("Install", mock.Anything, mock.AnythingOfType("Request")).Return(Response{}, errors.New("Invalid chart")) + + resp, err := http.DefaultClient.Do(req) + + assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode) + expectedResponse := `{"error":"Invalid chart"}` + "\n" + respBody, _ := ioutil.ReadAll(resp.Body) + assert.Equal(s.T(), expectedResponse, string(respBody)) + require.NoError(s.T(), err) + s.mockService.AssertExpectations(s.T()) +} + +func (s *InstallerTestSuite) TestReturnShouldBadRequestOnInvalidRequest() { + chartName := "stable/redis-ha" + body := fmt.Sprintf(`{"chart":"%s", "name": "redis-v5}`, chartName) + + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/install", s.server.URL), strings.NewReader(body)) + s.mockService.On("Install", mock.Anything, mock.AnythingOfType("Request")).Return(Release{}, nil) + + resp, err := http.DefaultClient.Do(req) + assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) + require.NoError(s.T(), err) + s.mockService.AssertNotCalled(s.T(), "Install") +} + +func (s *InstallerTestSuite) TearDownTest() { + s.server.Close() +} + +func TestInstallAPI(t *testing.T) { + suite.Run(t, new(InstallerTestSuite)) +} diff --git a/api/install/service.go b/api/install/service.go new file mode 100644 index 0000000..a50590c --- /dev/null +++ b/api/install/service.go @@ -0,0 +1,47 @@ +package install + +import ( + "context" + + "helm.sh/helm/v3/pkg/release" + + "github.com/gojekfarm/albatross/pkg/helmcli" + "github.com/gojekfarm/albatross/pkg/helmcli/flags" +) + +// TODO: Move the service interface to a common place for all apis +type service interface { + Install(ctx context.Context, req Request) (Response, error) +} + +type Service struct{} + +func (s Service) Install(ctx context.Context, req Request) (Response, error) { + installflags := flags.InstallFlags{ + DryRun: req.Flags.DryRun, + Version: req.Flags.Version, + GlobalFlags: req.Flags.GlobalFlags, + } + icli := helmcli.NewInstaller(installflags) + release, err := icli.Install(ctx, req.Name, req.Chart, req.Values) + if err != nil { + return Response{}, err + } + resp := Response{Status: release.Info.Status.String(), Release: releaseInfo(release)} + if req.Flags.DryRun { + resp.Data = release.Manifest + } + return resp, nil +} + +func releaseInfo(release *release.Release) Release { + return Release{ + Name: release.Name, + Namespace: release.Namespace, + Version: release.Version, + Updated: release.Info.FirstDeployed.Local().Time, + Status: release.Info.Status, + Chart: release.Chart.ChartFullPath(), + AppVersion: release.Chart.AppVersion(), + } +} diff --git a/api/install_api_test.go b/api/install_api_test.go deleted file mode 100644 index 9da5a5a..0000000 --- a/api/install_api_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package api_test - -import ( - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - "github.com/gojekfarm/albatross/api/logger" - - "github.com/gojekfarm/albatross/api" - - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/release" -) - -type InstallerTestSuite struct { - suite.Suite - recorder *httptest.ResponseRecorder - server *httptest.Server - mockInstall *mockInstall - mockChartLoader *mockChartLoader - mockList *mockList - appConfig *cli.EnvSettings -} - -func (s *InstallerTestSuite) SetupSuite() { - logger.Setup("default") -} - -func (s *InstallerTestSuite) SetupTest() { - s.recorder = httptest.NewRecorder() - s.mockInstall = new(mockInstall) - s.mockChartLoader = new(mockChartLoader) - s.mockList = new(mockList) - s.appConfig = &cli.EnvSettings{ - RepositoryConfig: "./testdata/helm", - PluginsDirectory: "./testdata/helm/plugin", - } - service := api.NewService(s.appConfig, s.mockChartLoader, nil, s.mockInstall, nil, nil) - 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.ReleaseConfig{ChartName: chartName, Name: "redis-v5", Namespace: "something"} - s.mockInstall.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.mockInstall.On("Run", mock.AnythingOfType("*chart.Chart"), vals).Return(release, nil) - - resp, err := http.DefaultClient.Do(req) - - assert.Equal(s.T(), http.StatusOK, resp.StatusCode) - expectedResponse := `{"status":"deployed"}` + "\n" - respBody, _ := ioutil.ReadAll(resp.Body) - assert.Equal(s.T(), expectedResponse, string(respBody)) - require.NoError(s.T(), err) - s.mockInstall.AssertExpectations(s.T()) - s.mockChartLoader.AssertExpectations(s.T()) -} - -func (s *InstallerTestSuite) TestShouldReturnInternalServerErrorOnFailure() { - 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", errors.New("Invalid chart")) - - resp, err := http.DefaultClient.Do(req) - - assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode) - expectedResponse := `{"error":"error in locating chart: Invalid chart"}` + "\n" - respBody, _ := ioutil.ReadAll(resp.Body) - assert.Equal(s.T(), expectedResponse, string(respBody)) - require.NoError(s.T(), err) - s.mockInstall.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)) -} diff --git a/api/installer.go b/api/installer.go deleted file mode 100644 index 518167b..0000000 --- a/api/installer.go +++ /dev/null @@ -1,25 +0,0 @@ -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 InstallRunner interface { - Run(*chart.Chart, map[string]interface{}) (*release.Release, error) - SetConfig(ReleaseConfig) -} - -func (i *Installer) SetConfig(cfg ReleaseConfig) { - i.ReleaseName = cfg.Name - i.Namespace = cfg.Namespace -} - -func NewInstall(ai *action.Install) *Installer { - return &Installer{ai} -} diff --git a/api/list.go b/api/list/list.go similarity index 57% rename from api/list.go rename to api/list/list.go index 15cc376..c402555 100644 --- a/api/list.go +++ b/api/list/list.go @@ -1,58 +1,64 @@ -package api +package list import ( "encoding/json" "io" "net/http" - - "github.com/gojekfarm/albatross/api/logger" + "time" "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/time" + + "github.com/gojekfarm/albatross/pkg/helmcli/flags" + "github.com/gojekfarm/albatross/pkg/logger" ) -type ListRequest struct { - NameSpace string `json:"namespace"` - ReleaseStatus string `json:"release_status"` +type Request struct { + Flags } -type ListResponse struct { - Error string `json:"error,omitempty"` - Releases []Release `json:"releases,omitempty"` +type Flags struct { + AllNamespaces bool `json:"all-namespaces,omitempty"` + Deployed bool `json:"deployed,omitempty"` + Failed bool `json:"failed,omitempty"` + Pending bool `json:"pending,omitempty"` + Uninstalled bool `json:"uninstalled,omitempty"` + Uninstalling bool `json:"uninstalling,omitempty"` + flags.GlobalFlags } type Release struct { Name string `json:"name"` Namespace string `json:"namespace"` - Revision int `json:"revision"` + Version int `json:"version"` Updated time.Time `json:"updated_at,omitempty"` Status release.Status `json:"status"` Chart string `json:"chart"` AppVersion string `json:"app_version"` } -func List(svc Service) http.Handler { +type Response struct { + Error string `json:"error,omitempty"` + Releases []Release `json:"releases,omitempty"` +} + +func Handler(service service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var response ListResponse - var request ListRequest - if err := json.NewDecoder(r.Body).Decode(&request); err == io.EOF || err != nil { + defer r.Body.Close() + + var req Request + if err := json.NewDecoder(r.Body).Decode(&req); err == io.EOF || err != nil { logger.Errorf("[List] error decoding request: %v", err.Error()) w.WriteHeader(http.StatusBadRequest) - response.Error = err.Error() - json.NewEncoder(w).Encode(response) return } - defer r.Body.Close() - - helmReleases, err := svc.List(request.ReleaseStatus, request.NameSpace) + resp, err := service.List(r.Context(), req) if err != nil { respondListError(w, "error while listing charts: %v", err) + return } - response = ListResponse{"", helmReleases} - err = json.NewEncoder(w).Encode(response) - if err != nil { + if err = json.NewEncoder(w).Encode(resp); err != nil { respondListError(w, "error writing response: %v", err) return } @@ -60,7 +66,7 @@ func List(svc Service) http.Handler { } func respondListError(w http.ResponseWriter, logprefix string, err error) { - response := ListResponse{Error: err.Error()} + response := Response{Error: err.Error()} w.WriteHeader(http.StatusInternalServerError) if err := json.NewEncoder(w).Encode(&response); err != nil { logger.Errorf("[List] %s %v", logprefix, err) diff --git a/api/list/list_api_test.go b/api/list/list_api_test.go new file mode 100644 index 0000000..21f77fb --- /dev/null +++ b/api/list/list_api_test.go @@ -0,0 +1,105 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "gotest.tools/assert" + + "github.com/gojekfarm/albatross/pkg/logger" + + "helm.sh/helm/v3/pkg/release" +) + +type mockService struct { + mock.Mock +} + +func (m *mockService) List(ctx context.Context, req Request) (Response, error) { + args := m.Called(ctx, req) + return args.Get(0).(Response), args.Error(1) +} + +type ListTestSuite struct { + suite.Suite + recorder *httptest.ResponseRecorder + server *httptest.Server + mockService *mockService +} + +func (s *ListTestSuite) SetupSuite() { + logger.Setup("default") +} + +func (s *ListTestSuite) SetupTest() { + s.recorder = httptest.NewRecorder() + s.mockService = new(mockService) + handler := Handler(s.mockService) + s.server = httptest.NewServer(handler) +} + +func (s *ListTestSuite) TestShouldReturnReleasesWhenSuccessfulAPICall() { + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeFromStr, _ := time.Parse(layout, str) + body := `{"release_status":"deployed"}` + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/list", s.server.URL), strings.NewReader(body)) + + response := Response{ + Releases: []Release{ + { + Name: "test-release", + Namespace: "test", + Version: 1, + Updated: timeFromStr, + Status: release.StatusDeployed, + AppVersion: "0.1", + }, + }, + } + s.mockService.On("List", mock.Anything, mock.AnythingOfType("Request")).Return(response, nil) + + res, err := http.DefaultClient.Do(req) + assert.Equal(s.T(), 200, res.StatusCode) + require.NoError(s.T(), err) + + var actualResponse Response + err = json.NewDecoder(res.Body).Decode(&actualResponse) + + expectedResponse := Response{ + Error: "", + Releases: response.Releases, + } + + assert.Equal(s.T(), expectedResponse.Releases[0], actualResponse.Releases[0]) + require.NoError(s.T(), err) + s.mockService.AssertExpectations(s.T()) +} + +func (s *ListTestSuite) TestShouldReturnBadRequestErrorIfItHasInvalidCharacter() { + body := `{"release_status":"unknown""""}` + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/list", s.server.URL), strings.NewReader(body)) + + res, err := http.DefaultClient.Do(req) + require.NoError(s.T(), err) + + assert.Equal(s.T(), 400, res.StatusCode) + require.NoError(s.T(), err) +} + +func (s *ListTestSuite) TearDownTest() { + s.server.Close() +} + +func TestListAPI(t *testing.T) { + suite.Run(t, new(ListTestSuite)) +} diff --git a/api/list/service.go b/api/list/service.go new file mode 100644 index 0000000..ae665ab --- /dev/null +++ b/api/list/service.go @@ -0,0 +1,47 @@ +package list + +import ( + "context" + + "helm.sh/helm/v3/pkg/release" + + "github.com/gojekfarm/albatross/pkg/helmcli" + "github.com/gojekfarm/albatross/pkg/helmcli/flags" +) + +type service interface { + List(ctx context.Context, req Request) (Response, error) +} + +type Service struct{} + +func (s Service) List(ctx context.Context, req Request) (Response, error) { + listflags := flags.ListFlags{ + GlobalFlags: req.Flags.GlobalFlags, + } + lcli := helmcli.NewLister(listflags) + releases, err := lcli.List(ctx) + if err != nil { + return Response{}, err + } + + respReleases := []Release{} + for _, release := range releases { + respReleases = append(respReleases, releaseInfo(release)) + } + + resp := Response{Releases: respReleases} + return resp, nil +} + +func releaseInfo(release *release.Release) Release { + return Release{ + Name: release.Name, + Namespace: release.Namespace, + Version: release.Version, + Updated: release.Info.FirstDeployed.Local().Time, + Status: release.Info.Status, + Chart: release.Chart.ChartFullPath(), + AppVersion: release.Chart.AppVersion(), + } +} diff --git a/api/list_api_test.go b/api/list_api_test.go deleted file mode 100644 index b27ecd3..0000000 --- a/api/list_api_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package api_test - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/time" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "gotest.tools/assert" - - "github.com/gojekfarm/albatross/api/logger" - - "github.com/gojekfarm/albatross/api" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/release" -) - -type ListTestSuite struct { - suite.Suite - recorder *httptest.ResponseRecorder - server *httptest.Server - mockList *mockList - appConfig *cli.EnvSettings -} - -type mockList struct{ mock.Mock } - -func (m *mockList) Run() ([]*release.Release, error) { - args := m.Called() - return args.Get(0).([]*release.Release), args.Error(1) -} - -func (m *mockList) SetStateMask() { - m.Called() -} - -func (m *mockList) SetConfig(state action.ListStates, allNameSpaces bool) { - m.Called(state, allNameSpaces) -} - -func (s *ListTestSuite) SetupSuite() { - logger.Setup("default") -} - -func (s *ListTestSuite) SetupTest() { - s.recorder = httptest.NewRecorder() - s.mockList = new(mockList) - s.appConfig = &cli.EnvSettings{ - RepositoryConfig: "./testdata/helm", - PluginsDirectory: "./testdata/helm/plugin", - } - service := api.NewService(s.appConfig, nil, s.mockList, nil, nil, nil) - handler := api.List(service) - s.server = httptest.NewServer(handler) -} - -func (s *ListTestSuite) TestShouldReturnReleasesWhenSuccessfulAPICall() { - layout := "2006-01-02T15:04:05.000Z" - str := "2014-11-12T11:45:26.371Z" - timeFromStr, _ := time.Parse(layout, str) - body := `{"release_status":"deployed"}` - req, _ := http.NewRequest("POST", fmt.Sprintf("%s/list", s.server.URL), strings.NewReader(body)) - - releases := []*release.Release{{Name: "test-release", - Namespace: "test-namespace", - Info: &release.Info{Status: release.StatusDeployed, LastDeployed: timeFromStr}, - Version: 1, - Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "test-release", Version: "0.1", AppVersion: "0.1"}}, - }} - - s.mockList.On("SetStateMask") - s.mockList.On("SetConfig", action.ListDeployed, true) - s.mockList.On("Run").Return(releases, nil) - - res, err := http.DefaultClient.Do(req) - assert.Equal(s.T(), 200, res.StatusCode) - require.NoError(s.T(), err) - - var actualResponse api.ListResponse - err = json.NewDecoder(res.Body).Decode(&actualResponse) - - expectedResponse := api.ListResponse{Error: "", - Releases: []api.Release{{"test-release", - "test-namespace", - 1, - timeFromStr, - release.StatusDeployed, - "test-release-0.1", - "0.1", - }}} - - assert.Equal(s.T(), expectedResponse.Releases[0], actualResponse.Releases[0]) - require.NoError(s.T(), err) - s.mockList.AssertExpectations(s.T()) -} - -func (s *ListTestSuite) TestShouldReturnBadRequestErrorIfItHasInvalidCharacter() { - body := `{"release_status":"unknown""""}` - req, _ := http.NewRequest("POST", fmt.Sprintf("%s/list", s.server.URL), strings.NewReader(body)) - - res, err := http.DefaultClient.Do(req) - require.NoError(s.T(), err) - - assert.Equal(s.T(), 400, res.StatusCode) - expectedResponse := "invalid character '\"' after object key:value pair" - var actualResponse api.ListResponse - err = json.NewDecoder(res.Body).Decode(&actualResponse) - require.NoError(s.T(), err) - assert.Equal(s.T(), expectedResponse, actualResponse.Error) -} - -func (s *ListTestSuite) TearDownTest() { - s.server.Close() -} - -func TestListAPI(t *testing.T) { - suite.Run(t, new(ListTestSuite)) -} diff --git a/api/lister.go b/api/lister.go deleted file mode 100644 index 29e5dc1..0000000 --- a/api/lister.go +++ /dev/null @@ -1,25 +0,0 @@ -package api - -import ( - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/release" -) - -type Lister struct { - *action.List -} - -type ListRunner interface { - Run() ([]*release.Release, error) - SetStateMask() - SetConfig(state action.ListStates, allNameSpaces bool) -} - -func NewList(action *action.List) *Lister { - return &Lister{action} -} - -func (l *Lister) SetConfig(state action.ListStates, allNameSpaces bool) { - l.StateMask = state - l.AllNamespaces = allNameSpaces -} diff --git a/api/ping.go b/api/ping.go index 2db3351..3251c82 100644 --- a/api/ping.go +++ b/api/ping.go @@ -4,14 +4,16 @@ import ( "encoding/json" "net/http" - "github.com/gojekfarm/albatross/api/logger" + "github.com/gojekfarm/albatross/pkg/logger" ) +// PingResponse represents the API response for the ping request type PingResponse struct { Error string `json:"error,omitempty"` Data string `json:"data,omitempty"` } +// Ping returns a http handler that handles the ping api request func Ping() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/api/service.go b/api/service.go deleted file mode 100644 index 40f7a12..0000000 --- a/api/service.go +++ /dev/null @@ -1,167 +0,0 @@ -package api - -import ( - "context" - "errors" - "fmt" - "strings" - - "helm.sh/helm/v3/pkg/action" - - "github.com/gojekfarm/albatross/api/logger" - - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/storage/driver" -) - -type upgrader interface { - SetConfig(ReleaseConfig) - GetInstall() bool - upgraderunner -} - -type history interface { - historyrunner -} - -type Service struct { - settings *cli.EnvSettings - chartloader - ListRunner - InstallRunner - upgrader - history -} - -type ReleaseConfig struct { - Name string - Namespace string - ChartName string - Version string - Install bool -} - -type ChartValues map[string]interface{} - -type ReleaseResult struct { - Status string -} - -func (s Service) Install(ctx context.Context, cfg ReleaseConfig, values ChartValues) (*ReleaseResult, error) { - if err := s.validate(cfg, values); err != nil { - return nil, fmt.Errorf("error request validation: %v", err) - } - chart, err := s.loadChart(cfg.ChartName) - if err != nil { - return nil, err - } - - return s.installChart(cfg, chart, values) -} - -func (s Service) Upgrade(ctx context.Context, cfg ReleaseConfig, values ChartValues) (*ReleaseResult, error) { - s.upgrader.SetConfig(cfg) - if err := s.validate(cfg, values); err != nil { - return nil, fmt.Errorf("error request validation: %v", err) - } - chart, err := s.loadChart(cfg.ChartName) - if err != nil { - return nil, err - } - - if s.upgrader.GetInstall() { - if _, err := s.history.Run(cfg.Name); err == driver.ErrReleaseNotFound { - logger.Debugf("release %q does not exist. Installing it now.\n", cfg.Name) - return s.installChart(cfg, chart, values) - } - } - return s.upgradeRelease(cfg, chart, values) -} - -func (s Service) loadChart(chartName string) (*chart.Chart, error) { - logger.Debugf("[install/upgrade] chart name: %s", chartName) - cp, err := s.LocateChart(chartName, s.settings) - 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 (s Service) installChart(icfg ReleaseConfig, ch *chart.Chart, vals ChartValues) (*ReleaseResult, error) { - s.InstallRunner.SetConfig(icfg) - release, err := s.InstallRunner.Run(ch, vals) - if err != nil { - return nil, fmt.Errorf("error in installing chart: %v", err) - } - result := new(ReleaseResult) - if release.Info != nil { - result.Status = release.Info.Status.String() - } - return result, nil -} - -func (s Service) upgradeRelease(ucfg ReleaseConfig, ch *chart.Chart, vals ChartValues) (*ReleaseResult, error) { - release, err := s.upgrader.Run(ucfg.Name, ch, vals) - if err != nil { - return nil, fmt.Errorf("error in upgrading chart: %v", err) - } - result := new(ReleaseResult) - if release.Info != nil { - result.Status = release.Info.Status.String() - } - return result, nil -} - -func (s Service) validate(icfg ReleaseConfig, values ChartValues) error { - if strings.HasPrefix(icfg.ChartName, ".") || - strings.HasPrefix(icfg.ChartName, "/") { - return errors.New("cannot refer local chart") - } - return nil -} - -func (s Service) List(releaseStatus string, namespace string) ([]Release, error) { - listStates := new(action.ListStates) - - state := action.ListAll - if releaseStatus != "" { - state = listStates.FromName(releaseStatus) - } - - if state == action.ListUnknown { - return nil, errors.New("invalid release status") - } - - s.ListRunner.SetConfig(state, namespace == "") - s.ListRunner.SetStateMask() - - releases, err := s.ListRunner.Run() - if err != nil { - return nil, err - } - - var helmReleases []Release - for _, r := range releases { - helmRelease := Release{Name: r.Name, - Namespace: r.Namespace, - Revision: r.Version, - Updated: r.Info.LastDeployed, - Status: r.Info.Status, - Chart: fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version), - AppVersion: r.Chart.Metadata.AppVersion, - } - helmReleases = append(helmReleases, helmRelease) - } - - return helmReleases, nil -} - -func NewService(settings *cli.EnvSettings, cl chartloader, l ListRunner, i InstallRunner, u upgrader, h history) Service { - return Service{settings, cl, l, i, u, h} -} diff --git a/api/service_test.go b/api/service_test.go deleted file mode 100644 index c20f36d..0000000 --- a/api/service_test.go +++ /dev/null @@ -1,395 +0,0 @@ -package api_test - -import ( - "context" - "errors" - "testing" - - "helm.sh/helm/v3/pkg/time" - - "github.com/gojekfarm/albatross/api" - - "helm.sh/helm/v3/pkg/action" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - "github.com/gojekfarm/albatross/api/logger" - - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/storage/driver" -) - -type ServiceTestSuite struct { - suite.Suite - ctx context.Context - upgrader *mockUpgrader - history *mockHistory - installer *mockInstall - chartloader *mockChartLoader - lister *mockList - svc api.Service - settings *cli.EnvSettings -} - -func (s *ServiceTestSuite) SetupTest() { - logger.Setup("") - s.settings = &cli.EnvSettings{} - s.chartloader = new(mockChartLoader) - s.lister = new(mockList) - s.installer = new(mockInstall) - s.upgrader = new(mockUpgrader) - s.history = new(mockHistory) - s.ctx = context.Background() - s.svc = api.NewService(s.settings, s.chartloader, s.lister, s.installer, s.upgrader, s.history) -} - -func (s *ServiceTestSuite) TestInstallShouldReturnErrorOnInvalidChart() { - var vals api.ChartValues - chartName := "stable/invalid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - - s.chartloader.On("LocateChart", chartName, s.settings).Return("", errors.New("Unable to find chart")) - - res, err := s.svc.Install(s.ctx, cfg, vals) - - t := s.T() - assert.Nil(t, res) - assert.EqualError(t, err, "error in locating chart: Unable to find chart") - s.chartloader.AssertExpectations(t) - s.installer.AssertNotCalled(t, "SetConfig") - s.installer.AssertNotCalled(t, "Run") -} - -func (s *ServiceTestSuite) TestInstallShouldReturnErrorOnLocalChartReference() { - var vals api.ChartValues - chartName := "./some/local-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - - res, err := s.svc.Install(s.ctx, cfg, vals) - - t := s.T() - assert.Nil(t, res) - assert.EqualError(t, err, "error request validation: cannot refer local chart") - s.chartloader.AssertNotCalled(t, "LocateChart") - s.installer.AssertNotCalled(t, "SetConfig") - s.installer.AssertNotCalled(t, "Run") -} - -func (s *ServiceTestSuite) TestInstallShouldReturnErrorOnFailedInstallRun() { - var release *release.Release - vals := map[string]interface{}{} - chartName := "stable/valid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - - s.chartloader.On("LocateChart", chartName, s.settings).Return("testdata/albatross", nil) - s.installer.On("SetConfig", cfg) - s.installer.On("Run", mock.AnythingOfType("*chart.Chart"), vals).Return(release, errors.New("cluster issue")) - - res, err := s.svc.Install(s.ctx, cfg, vals) - - t := s.T() - assert.Nil(t, res) - assert.EqualError(t, err, "error in installing chart: cluster issue") - s.chartloader.AssertExpectations(t) - s.installer.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestInstallShouldReturnResultOnSuccess() { - vals := map[string]interface{}{} - chartName := "stable/valid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - - s.chartloader.On("LocateChart", chartName, s.settings).Return("testdata/albatross", nil) - s.installer.On("SetConfig", cfg) - release := &release.Release{Name: "some-comp-release", Info: &release.Info{Status: release.StatusDeployed}} - s.installer.On("Run", mock.AnythingOfType("*chart.Chart"), vals).Return(release, nil) - - res, err := s.svc.Install(s.ctx, cfg, vals) - - t := s.T() - assert.NoError(t, err) - require.NotNil(t, res) - assert.Equal(t, res.Status, "deployed") - s.chartloader.AssertExpectations(t) - s.installer.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestUpgradeInstallTrueShouldInstallChart() { - vals := map[string]interface{}{} - chartName := "stable/valid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - s.upgrader.On("SetConfig", cfg) - s.chartloader.On("LocateChart", chartName, s.settings).Return("testdata/albatross", nil) - s.upgrader.On("GetInstall").Return(true) - s.installer.On("SetConfig", cfg) - s.history.On("Run", "some-component").Return([]*release.Release{}, driver.ErrReleaseNotFound) - release := &release.Release{Name: "some-comp-release", Info: &release.Info{Status: release.StatusDeployed}} - s.installer.On("Run", mock.AnythingOfType("*chart.Chart"), vals).Return(release, nil) - - res, err := s.svc.Upgrade(s.ctx, cfg, vals) - - t := s.T() - assert.NoError(t, err) - require.NotNil(t, res) - assert.Equal(t, res.Status, "deployed") - s.upgrader.AssertNotCalled(t, "Run") - s.chartloader.AssertExpectations(t) - s.upgrader.AssertExpectations(t) - s.history.AssertExpectations(t) - s.installer.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestUpgradeInstallFalseShouldNotInstallChart() { - chartName := "stable/valid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - vals := map[string]interface{}{} - s.chartloader.On("LocateChart", chartName, s.settings).Return("testdata/albatross", nil) - s.upgrader.On("GetInstall").Return(false) - s.upgrader.On("SetConfig", cfg) - release := &release.Release{Name: "some-comp-release", Info: &release.Info{Status: release.StatusDeployed}} - s.upgrader.On("Run", "some-component", mock.AnythingOfType("*chart.Chart"), vals).Return(release, nil) - - res, err := s.svc.Upgrade(s.ctx, cfg, vals) - - t := s.T() - assert.NoError(t, err) - require.NotNil(t, res) - s.installer.AssertNotCalled(t, "Run") - s.history.AssertNotCalled(t, "Run") - assert.Equal(t, res.Status, "deployed") - s.chartloader.AssertExpectations(t) - s.installer.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestUpgradeShouldReturnErrorOnFailedUpgradeRun() { - chartName := "stable/valid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - vals := map[string]interface{}{} - s.chartloader.On("LocateChart", chartName, s.settings).Return("testdata/albatross", nil) - s.upgrader.On("GetInstall").Return(false) - s.upgrader.On("SetConfig", cfg) - release := &release.Release{Name: "some-comp-release", Info: &release.Info{Status: release.StatusDeployed}} - s.upgrader.On("Run", "some-component", mock.AnythingOfType("*chart.Chart"), vals).Return(release, errors.New("cluster issue")) - - res, err := s.svc.Upgrade(s.ctx, cfg, vals) - - t := s.T() - assert.Nil(t, res) - assert.EqualError(t, err, "error in upgrading chart: cluster issue") - s.chartloader.AssertExpectations(t) - s.installer.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestUpgradeShouldReturnResultOnSuccess() { - chartName := "stable/valid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - vals := map[string]interface{}{} - s.chartloader.On("LocateChart", chartName, s.settings).Return("testdata/albatross", nil) - s.upgrader.On("GetInstall").Return(false) - s.upgrader.On("SetConfig", cfg) - release := &release.Release{Name: "some-comp-release", Info: &release.Info{Status: release.StatusDeployed}} - s.upgrader.On("Run", "some-component", mock.AnythingOfType("*chart.Chart"), vals).Return(release, nil) - - res, err := s.svc.Upgrade(s.ctx, cfg, vals) - - t := s.T() - assert.NoError(t, err) - require.NotNil(t, res) - assert.Equal(t, res.Status, "deployed") - s.chartloader.AssertExpectations(t) - s.upgrader.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestUpgradeValidateFailShouldResultFailure() { - var vals api.ChartValues - chartName := "./some/local-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - s.upgrader.On("SetConfig", cfg) - res, err := s.svc.Upgrade(s.ctx, cfg, vals) - - t := s.T() - assert.Nil(t, res) - assert.EqualError(t, err, "error request validation: cannot refer local chart") - s.chartloader.AssertNotCalled(t, "LocateChart") - s.upgrader.AssertNotCalled(t, "SetConfig") - s.upgrader.AssertNotCalled(t, "Run") -} - -func (s *ServiceTestSuite) TestUpgradeShouldReturnErrorOnInvalidChart() { - chartName := "stable/invalid-chart" - cfg := api.ReleaseConfig{ - Name: "some-component", - Namespace: "hermes", - ChartName: chartName, - } - var vals api.ChartValues - s.chartloader.On("LocateChart", chartName, s.settings).Return("", errors.New("Unable to find chart")) - s.upgrader.On("SetConfig", cfg) - res, err := s.svc.Upgrade(s.ctx, cfg, vals) - - t := s.T() - assert.Nil(t, res) - assert.EqualError(t, err, "error in locating chart: Unable to find chart") - s.chartloader.AssertExpectations(t) - s.upgrader.AssertNotCalled(t, "SetConfig") - s.upgrader.AssertNotCalled(t, "Run") -} - -func (s *ServiceTestSuite) TestListShouldReturnErrorOnFailureOfListRun() { - var releases []*release.Release - releaseStatus := "deployed" - s.lister.On("SetConfig", action.ListDeployed, false) - s.lister.On("SetStateMask") - s.lister.On("Run").Return(releases, errors.New("cluster issue")) - - res, err := s.svc.List(releaseStatus, "test-namespace") - - t := s.T() - assert.Error(t, err, "cluster issue") - assert.Nil(t, res) - s.lister.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestListShouldReturnAllReleasesIfNoFilterIsPassed() { - layout := "2006-01-02T15:04:05.000Z" - str := "2014-11-12T11:45:26.371Z" - releaseStatus := "" - timeFromStr, _ := time.Parse(layout, str) - - releases := []*release.Release{{Name: "test-release", - Namespace: "test-namespace", - Info: &release.Info{Status: release.StatusDeployed, LastDeployed: timeFromStr}, - Version: 1, - Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "test-release", Version: "0.1", AppVersion: "0.1"}}, - }} - - s.lister.On("SetConfig", action.ListAll, false) - s.lister.On("SetStateMask") - s.lister.On("Run").Return(releases, nil) - - res, err := s.svc.List(releaseStatus, "test-namespace") - - t := s.T() - assert.NoError(t, err) - require.NotNil(t, res) - - response := []api.Release{{"test-release", - "test-namespace", - 1, - timeFromStr, - release.StatusDeployed, - "test-release-0.1", - "0.1", - }} - - assert.Equal(t, response, res) - s.lister.AssertExpectations(t) -} - -func (s *ServiceTestSuite) TestListShouldReturnErrorIfInvalidStatusIsPassedAsFilter() { - releaseStatus := "invalid" - _, err := s.svc.List(releaseStatus, "test-namespace") - - t := s.T() - assert.EqualError(t, err, "invalid release status") -} - -func (s *ServiceTestSuite) TestListShouldReturnDeployedReleasesIfDeployedIsPassedAsFilter() { - var releases []*release.Release - releaseStatus := "deployed" - s.lister.On("SetConfig", action.ListDeployed, false) - s.lister.On("SetStateMask") - s.lister.On("Run").Return(releases, nil) - - _, err := s.svc.List(releaseStatus, "test-namespace") - - t := s.T() - assert.NoError(t, err) - s.lister.AssertExpectations(t) -} - -type mockInstall struct{ mock.Mock } - -func (m *mockInstall) SetConfig(cfg api.ReleaseConfig) { - m.Called(cfg) -} - -func (m *mockInstall) 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) -} - -type mockUpgrader struct{ mock.Mock } - -func (m *mockUpgrader) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { - args := m.Called(name, chart, vals) - return args.Get(0).(*release.Release), args.Error(1) -} - -func (m *mockUpgrader) SetConfig(cfg api.ReleaseConfig) { - _ = m.Called(cfg) -} - -func (m *mockUpgrader) GetInstall() bool { - args := m.Called() - return args.Get(0).(bool) -} - -type mockHistory struct{ mock.Mock } - -func (m *mockHistory) Run(name string) ([]*release.Release, error) { - args := m.Called(name) - return args.Get(0).([]*release.Release), args.Error(1) -} - -func TestServiceSuite(t *testing.T) { - suite.Run(t, new(ServiceTestSuite)) -} diff --git a/api/upgrade.go b/api/upgrade.go deleted file mode 100644 index ec8c2e0..0000000 --- a/api/upgrade.go +++ /dev/null @@ -1,76 +0,0 @@ -package api - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/gojekfarm/albatross/api/logger" -) - -type UpgradeRequest struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Chart string `json:"chart"` - Values map[string]interface{} `json:"values,omitempty"` - Flags map[string]interface{} `json:"flags,omitempty"` -} - -type UpgradeResponse struct { - Error string `json:"error,omitempty"` - Status string `json:"status,omitempty"` -} - -func Upgrade(svc Service) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - var req UpgradeRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err == io.EOF || err != nil { - w.WriteHeader(http.StatusBadRequest) - logger.Errorf("[Upgrade] error decoding request: %v", err) - return - } - defer r.Body.Close() - - var install bool - var version string - for key, value := range req.Flags { - if key == "install" { - install = value.(bool) - } - if key == "version" { - version = value.(string) - } - } - - if version == "" { - logger.Debugf("setting version to >0.0.0-0") - version = ">0.0.0-0" - } - - var response UpgradeResponse - cfg := ReleaseConfig{ChartName: req.Chart, Name: req.Name, Namespace: req.Namespace, Version: version, Install: install} - - res, err := svc.Upgrade(r.Context(), cfg, req.Values) - if err != nil { - respondUpgradeError(w, "error while upgrading chart: %v", err) - return - } - response.Status = res.Status - if err := json.NewEncoder(w).Encode(&response); err != nil { - respondUpgradeError(w, "error writing response: %v", err) - return - } - }) -} - -func respondUpgradeError(w http.ResponseWriter, logprefix string, err error) { - response := UpgradeResponse{Error: err.Error()} - w.WriteHeader(http.StatusInternalServerError) - if err := json.NewEncoder(w).Encode(&response); err != nil { - logger.Errorf("[Upgrade] %s %v", logprefix, err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/api/upgrade/service.go b/api/upgrade/service.go new file mode 100644 index 0000000..dad4c82 --- /dev/null +++ b/api/upgrade/service.go @@ -0,0 +1,48 @@ +package upgrade + +import ( + "context" + + "helm.sh/helm/v3/pkg/release" + + "github.com/gojekfarm/albatross/pkg/helmcli" + "github.com/gojekfarm/albatross/pkg/helmcli/flags" +) + +type service interface { + Upgrade(ctx context.Context, req Request) (Response, error) +} + +type Service struct{} + +func (s Service) Upgrade(ctx context.Context, req Request) (Response, error) { + upgradeflags := flags.UpgradeFlags{ + DryRun: req.Flags.DryRun, + Version: req.Flags.Version, + Install: req.Flags.Install, + GlobalFlags: req.Flags.GlobalFlags, + } + + ucli := helmcli.NewUpgrader(upgradeflags) + release, err := ucli.Upgrade(ctx, req.Name, req.Chart, req.Values) + if err != nil { + return Response{}, err + } + resp := Response{Status: release.Info.Status.String(), Release: releaseInfo(release)} + if req.Flags.DryRun { + resp.Data = release.Manifest + } + return resp, nil +} + +func releaseInfo(release *release.Release) Release { + return Release{ + Name: release.Name, + Namespace: release.Namespace, + Version: release.Version, + Updated: release.Info.FirstDeployed.Local().Time, + Status: release.Info.Status, + Chart: release.Chart.ChartFullPath(), + AppVersion: release.Chart.AppVersion(), + } +} diff --git a/api/upgrade/upgrade.go b/api/upgrade/upgrade.go new file mode 100644 index 0000000..eed3747 --- /dev/null +++ b/api/upgrade/upgrade.go @@ -0,0 +1,80 @@ +package upgrade + +import ( + "encoding/json" + "io" + "net/http" + "time" + + "helm.sh/helm/v3/pkg/release" + + "github.com/gojekfarm/albatross/pkg/helmcli/flags" + "github.com/gojekfarm/albatross/pkg/logger" +) + +type Request struct { + Name string + Chart string + Values map[string]interface{} + Flags Flags +} + +type Flags struct { + DryRun bool `json:"dry_run"` + Version string + Install bool + flags.GlobalFlags +} + +type Release struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Version int `json:"version"` + Updated time.Time `json:"updated_at,omitempty"` + Status release.Status `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` +} + +// Response represents the api response for upgrade request +type Response struct { + Error string `json:"error,omitempty"` + Status string `json:"status,omitempty"` + Data string `json:"data,omitempty"` + Release `json:"-"` +} + +func Handler(service service) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var req Request + if err := json.NewDecoder(r.Body).Decode(&req); err == io.EOF || err != nil { + w.WriteHeader(http.StatusBadRequest) + logger.Errorf("[Upgrade] error decoding request: %v", err) + return + } + defer r.Body.Close() + + resp, err := service.Upgrade(r.Context(), req) + if err != nil { + respondUpgradeError(w, "error while upgrading release: %v", err) + return + } + + if err := json.NewEncoder(w).Encode(&resp); err != nil { + respondUpgradeError(w, "error writing response: %v", err) + return + } + }) +} + +func respondUpgradeError(w http.ResponseWriter, logprefix string, err error) { + response := Response{Error: err.Error()} + w.WriteHeader(http.StatusInternalServerError) + if err := json.NewEncoder(w).Encode(&response); err != nil { + logger.Errorf("[Upgrade] %s %v", logprefix, err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/api/upgrade/upgrade_api_test.go b/api/upgrade/upgrade_api_test.go new file mode 100644 index 0000000..b101802 --- /dev/null +++ b/api/upgrade/upgrade_api_test.go @@ -0,0 +1,115 @@ +package upgrade + +import ( + "context" + "errors" + "fmt" + "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" + + "github.com/gojekfarm/albatross/pkg/logger" + + "helm.sh/helm/v3/pkg/release" +) + +type mockService struct { + mock.Mock +} + +func (m *mockService) Upgrade(ctx context.Context, req Request) (Response, error) { + args := m.Called(ctx, req) + return args.Get(0).(Response), args.Error(1) +} + +type UpgradeTestSuite struct { + suite.Suite + recorder *httptest.ResponseRecorder + server *httptest.Server + mockService *mockService +} + +func (s *UpgradeTestSuite) SetupSuite() { + logger.Setup("default") +} + +func (s *UpgradeTestSuite) SetupTest() { + s.recorder = httptest.NewRecorder() + s.mockService = new(mockService) + handler := Handler(s.mockService) + s.server = httptest.NewServer(handler) +} + +func (s *UpgradeTestSuite) TestShouldReturnDeployedStatusOnSuccessfulUpgrade() { + chartName := "stable/redis-ha" + body := fmt.Sprintf(`{ + "chart":"%s", + "name": "redis-v5", + "flags": { + "install": false, + "namespace": "something" + }, + "values": { + "usePassword": false + }}`, chartName) + + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/upgrade", s.server.URL), strings.NewReader(body)) + response := Response{ + Status: release.StatusDeployed.String(), + } + s.mockService.On("Upgrade", mock.Anything, mock.AnythingOfType("Request")).Return(response, nil) + + resp, err := http.DefaultClient.Do(req) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + require.NoError(s.T(), err) + s.mockService.AssertExpectations(s.T()) +} + +func (s *UpgradeTestSuite) TestShouldReturnInternalServerErrorOnFailure() { + chartName := "stable/redis-ha" + body := fmt.Sprintf(`{ + "chart":"%s", + "name": "redis-v5", + "flags": { + "install": true, "namespace": "something", "version": "7.5.4" + }}`, chartName) + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/install", s.server.URL), strings.NewReader(body)) + s.mockService.On("Upgrade", mock.Anything, mock.AnythingOfType("Request")).Return(Response{}, errors.New("Invalid Chart")) + + resp, err := http.DefaultClient.Do(req) + + assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode) + require.NoError(s.T(), err) +} + +func (s *UpgradeTestSuite) TestShouldBadRequestOnInvalidRequest() { + chartName := "stable/redis-ha" + body := fmt.Sprintf(`{ + "chart":"%s", + "name": "redis-v5", + "flags": { + "install": true, "namespace": true, "version": 7.5.4 + }}`, chartName) + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/install", s.server.URL), strings.NewReader(body)) + s.mockService.On("Upgrade", mock.Anything, mock.AnythingOfType("Request")).Return(Response{}, nil) + + resp, err := http.DefaultClient.Do(req) + + assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) + require.NoError(s.T(), err) +} + +func (s *UpgradeTestSuite) TearDownTest() { + s.server.Close() +} + +func TestUpgradeAPI(t *testing.T) { + suite.Run(t, new(UpgradeTestSuite)) +} diff --git a/api/upgrade_api_test.go b/api/upgrade_api_test.go deleted file mode 100644 index b366c7a..0000000 --- a/api/upgrade_api_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package api_test - -import ( - "errors" - "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" - - "github.com/gojekfarm/albatross/api" - "github.com/gojekfarm/albatross/api/logger" - - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/release" -) - -type UpgradeTestSuite struct { - suite.Suite - recorder *httptest.ResponseRecorder - server *httptest.Server - mockUpgrader *mockUpgrader - mockHistory *mockHistory - mockChartLoader *mockChartLoader - appConfig *cli.EnvSettings -} - -func (s *UpgradeTestSuite) SetupSuite() { - logger.Setup("default") -} - -func (s *UpgradeTestSuite) SetupTest() { - s.recorder = httptest.NewRecorder() - s.mockUpgrader = new(mockUpgrader) - s.mockHistory = new(mockHistory) - s.mockChartLoader = new(mockChartLoader) - s.appConfig = &cli.EnvSettings{ - RepositoryConfig: "./testdata/helm", - PluginsDirectory: "./testdata/helm/plugin", - } - service := api.NewService(s.appConfig, s.mockChartLoader, nil, nil, s.mockUpgrader, s.mockHistory) - handler := api.Upgrade(service) - s.server = httptest.NewServer(handler) -} - -func (s *UpgradeTestSuite) TestShouldReturnDeployedStatusOnSuccessfulUpgrade() { - chartName := "stable/redis-ha" - body := fmt.Sprintf(`{ - "chart":"%s", - "name": "redis-v5", - "flags": { - "install": false - }, - "values": { - "usePassword": false - }, - "namespace": "something"}`, chartName) - req, _ := http.NewRequest("POST", fmt.Sprintf("%s/upgrade", s.server.URL), strings.NewReader(body)) - s.mockChartLoader.On("LocateChart", chartName, s.appConfig).Return("./testdata/albatross", nil) - ucfg := api.ReleaseConfig{ChartName: chartName, Name: "redis-v5", Namespace: "something", Version: ">0.0.0-0"} - s.mockUpgrader.On("GetInstall").Return(false) - s.mockUpgrader.On("SetConfig", ucfg) - release := &release.Release{Info: &release.Info{Status: release.StatusDeployed}} - vals := map[string]interface{}{"usePassword": false} - //TODO: pass chart object and verify values present testdata chart yml - s.mockUpgrader.On("Run", "redis-v5", mock.AnythingOfType("*chart.Chart"), vals).Return(release, nil) - - resp, err := http.DefaultClient.Do(req) - - assert.Equal(s.T(), http.StatusOK, resp.StatusCode) - expectedResponse := `{"status":"deployed"}` + "\n" - respBody, _ := ioutil.ReadAll(resp.Body) - - assert.Equal(s.T(), expectedResponse, string(respBody)) - require.NoError(s.T(), err) - s.mockUpgrader.AssertExpectations(s.T()) - s.mockChartLoader.AssertExpectations(s.T()) -} - -func (s *UpgradeTestSuite) TestShouldReturnInternalServerErrorOnFailure() { - chartName := "stable/redis-ha" - body := fmt.Sprintf(`{ - "chart":"%s", - "name": "redis-v5", - "flags": { - "install": true, - "version": "7.5.4" - }, - "namespace": "something"}`, chartName) - req, _ := http.NewRequest("POST", fmt.Sprintf("%s/install", s.server.URL), strings.NewReader(body)) - ucfg := api.ReleaseConfig{ChartName: chartName, Name: "redis-v5", Namespace: "something", Version: "7.5.4", Install: true} - s.mockUpgrader.On("SetConfig", ucfg) - s.mockChartLoader.On("LocateChart", chartName, s.appConfig).Return("./testdata/albatross", errors.New("Invalid chart")) - - resp, err := http.DefaultClient.Do(req) - - assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode) - expectedResponse := `{"error":"error in locating chart: Invalid chart"}` + "\n" - respBody, _ := ioutil.ReadAll(resp.Body) - assert.Equal(s.T(), expectedResponse, string(respBody)) - require.NoError(s.T(), err) - s.mockUpgrader.AssertExpectations(s.T()) - s.mockChartLoader.AssertExpectations(s.T()) -} - -func (s *UpgradeTestSuite) TearDownTest() { - s.server.Close() -} - -func TestUpgradeAPI(t *testing.T) { - suite.Run(t, new(UpgradeTestSuite)) -} diff --git a/api/upgrader.go b/api/upgrader.go deleted file mode 100644 index feab0be..0000000 --- a/api/upgrader.go +++ /dev/null @@ -1,45 +0,0 @@ -package api - -import ( - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/release" -) - -type Upgrader struct { - *action.Upgrade -} - -type History struct { - *action.History -} - -type upgraderunner interface { - Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) -} - -type historyrunner interface { - Run(name string) ([]*release.Release, error) -} - -func (u *Upgrader) SetConfig(cfg ReleaseConfig) { - u.Namespace = cfg.Namespace - u.ChartPathOptions.Version = cfg.Version - u.Install = cfg.Install -} - -func (u *Upgrader) GetInstall() bool { - return u.Install -} - -func (h *History) SetConfig() { - h.Max = 1 -} - -func NewUpgrader(au *action.Upgrade) *Upgrader { - return &Upgrader{au} -} - -func NewHistory(ah *action.History) *History { - return &History{ah} -} diff --git a/cmd/albatross/albatross.go b/cmd/albatross/albatross.go index 501a811..6149723 100644 --- a/cmd/albatross/albatross.go +++ b/cmd/albatross/albatross.go @@ -7,14 +7,15 @@ import ( "github.com/gorilla/mux" "github.com/gojekfarm/albatross/api" - "github.com/gojekfarm/albatross/api/logger" - "github.com/gojekfarm/albatross/servercontext" + "github.com/gojekfarm/albatross/api/install" + "github.com/gojekfarm/albatross/api/list" + "github.com/gojekfarm/albatross/api/upgrade" + "github.com/gojekfarm/albatross/pkg/logger" - "helm.sh/helm/v3/pkg/action" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ) func main() { - servercontext.NewApp() startServer() } @@ -27,26 +28,16 @@ func ContentTypeMiddle(next http.Handler) http.Handler { func startServer() { router := mux.NewRouter() - - app := servercontext.App() logger.Setup("debug") - actionList := action.NewList(app.ActionConfig) - actionInstall := action.NewInstall(app.ActionConfig) - actionUpgrade := action.NewUpgrade(app.ActionConfig) - actionHistory := action.NewHistory(app.ActionConfig) - - service := api.NewService(app.Config, - new(action.ChartPathOptions), - api.NewList(actionList), - api.NewInstall(actionInstall), - api.NewUpgrader(actionUpgrade), - api.NewHistory(actionHistory)) + installHandler := install.Handler(install.Service{}) + upgradeHandler := upgrade.Handler(upgrade.Service{}) + listHandler := list.Handler(list.Service{}) router.Handle("/ping", ContentTypeMiddle(api.Ping())).Methods(http.MethodGet) - router.Handle("/list", ContentTypeMiddle(api.List(service))).Methods(http.MethodGet) - router.Handle("/install", ContentTypeMiddle(api.Install(service))).Methods(http.MethodPut) - router.Handle("/upgrade", ContentTypeMiddle(api.Upgrade(service))).Methods(http.MethodPost) + router.Handle("/list", ContentTypeMiddle(listHandler)).Methods(http.MethodGet) + router.Handle("/install", ContentTypeMiddle(installHandler)).Methods(http.MethodPut) + router.Handle("/upgrade", ContentTypeMiddle(upgradeHandler)).Methods(http.MethodPost) err := http.ListenAndServe(fmt.Sprintf(":%d", 8080), router) if err != nil { diff --git a/go.mod b/go.mod index 9eb0bd1..194872e 100644 --- a/go.mod +++ b/go.mod @@ -8,5 +8,7 @@ require ( go.uber.org/zap v1.10.0 gotest.tools v2.2.0+incompatible helm.sh/helm/v3 v3.2.4 + k8s.io/cli-runtime v0.18.0 + k8s.io/client-go v0.18.0 rsc.io/letsencrypt v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index bf7e158..6d6688f 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= diff --git a/pkg/helmcli/config/config.go b/pkg/helmcli/config/config.go new file mode 100644 index 0000000..9a64b9c --- /dev/null +++ b/pkg/helmcli/config/config.go @@ -0,0 +1,62 @@ +package config + +import ( + "os" + + "github.com/gojekfarm/albatross/pkg/helmcli/flags" + "github.com/gojekfarm/albatross/pkg/logger" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/kube" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// ActionConfig acts as a proxy to helm package's action configuration. +// It defines methods to set the default/common action config members +type ActionConfig struct { + *action.Configuration +} + +// NewActionConfig returns a new instance of actionconfig +func NewActionConfig(envconfig *EnvConfig, flg *flags.GlobalFlags) *ActionConfig { + config := &ActionConfig{ + new(action.Configuration), + } + + config.setFlags(envconfig, flg) + return config +} + +// kubeClientConfig returns a kube config that is scoped to a namespace. +// Context: The EnvSetting struct does not expose any way to set the namespace, +// so we cannot set it directly. However, it is used to create kubeclients. +// So in order to configure the kubeclient with the proper namespace, we define a custom getter +// here that sets the correct namespace in the kubeconfig +func kubeClientConfig(envconfig *EnvConfig, namespace string) genericclioptions.RESTClientGetter { + clientConfig := kube.GetConfig(envconfig.KubeConfig, envconfig.KubeContext, namespace) + + if envconfig.KubeToken != "" { + clientConfig.BearerToken = &envconfig.KubeToken + } + + if envconfig.KubeAPIServer != "" { + clientConfig.APIServer = &envconfig.KubeAPIServer + } + + return clientConfig +} + +// setFlags initializes the action configuration with proper config flags +func (ac *ActionConfig) setFlags(envconfig *EnvConfig, flg *flags.GlobalFlags) { + actionNamespace := envconfig.Namespace() + if flg.Namespace != "" { + actionNamespace = flg.Namespace + } + + ac.Configuration.Init( + kubeClientConfig(envconfig, actionNamespace), + actionNamespace, + os.Getenv("HELM_DRIVER"), + logger.Debugf, + ) +} diff --git a/pkg/helmcli/config/environment.go b/pkg/helmcli/config/environment.go new file mode 100644 index 0000000..a2dd8fa --- /dev/null +++ b/pkg/helmcli/config/environment.go @@ -0,0 +1,31 @@ +package config + +import ( + "github.com/gojekfarm/albatross/pkg/helmcli/flags" + + "helm.sh/helm/v3/pkg/cli" +) + +// EnvConfig serves as a proxy to cli.EnvSettings. +// The methods on this struct take care of updating the EnvSettings struct +// with appropriate values +type EnvConfig struct { + *cli.EnvSettings +} + +func NewEnvConfig(flg *flags.GlobalFlags) *EnvConfig { + envconfig := &EnvConfig{ + cli.New(), + } + + envconfig.setEnvFlags(flg) + return envconfig +} + +// setEnvFlags sets the appropriate config members corresponding to the flags argument +// There is gotacha here, the EnvSettings does not expose the namespace as a publicly +// writable field and takes it from the environment. The problem here is that we cannot +// set the namespace here, which means that the namespace needs to be set in individual actions. +func (config *EnvConfig) setEnvFlags(flg *flags.GlobalFlags) { + config.KubeContext = flg.KubeCtx +} diff --git a/pkg/helmcli/flags/flags.go b/pkg/helmcli/flags/flags.go new file mode 100644 index 0000000..68239b6 --- /dev/null +++ b/pkg/helmcli/flags/flags.go @@ -0,0 +1,31 @@ +package flags + +type GlobalFlags struct { + KubeCtx string + KubeToken string + KubeAPIServer string + Namespace string +} + +type UpgradeFlags struct { + DryRun bool + Install bool + Version string + GlobalFlags +} + +type InstallFlags struct { + DryRun bool + Version string + GlobalFlags +} + +type ListFlags struct { + AllNamespaces bool + Deployed bool + Failed bool + Pending bool + Uninstalled bool + Uninstalling bool + GlobalFlags +} diff --git a/pkg/helmcli/install.go b/pkg/helmcli/install.go new file mode 100644 index 0000000..1253ee3 --- /dev/null +++ b/pkg/helmcli/install.go @@ -0,0 +1,54 @@ +package helmcli + +import ( + "context" + + "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/release" + + "github.com/gojekfarm/albatross/pkg/helmcli/config" + "github.com/gojekfarm/albatross/pkg/helmcli/flags" +) + +type Installer struct { + action *action.Install + envSettings *cli.EnvSettings +} + +// NewInstaller returns a new instance of Installer struct +func NewInstaller(flg flags.InstallFlags) *Installer { + envconfig := config.NewEnvConfig(&flg.GlobalFlags) + actionconfig := config.NewActionConfig(envconfig, &flg.GlobalFlags) + + install := action.NewInstall(actionconfig.Configuration) + install.Namespace = flg.Namespace + install.DryRun = flg.DryRun + + return &Installer{ + action: install, + envSettings: envconfig.EnvSettings, + } +} + +func (i *Installer) Install(ctx context.Context, relName, chartName string, values map[string]interface{}) (*release.Release, error) { + i.action.ReleaseName = relName + + chart, err := i.loadChart(chartName) + if err != nil { + return nil, err + } + + return i.action.Run(chart, values) +} + +func (i *Installer) loadChart(chartName string) (*chart.Chart, error) { + cp, err := i.action.LocateChart(chartName, i.envSettings) + if err != nil { + return nil, err + } + + return loader.Load(cp) +} diff --git a/pkg/helmcli/list.go b/pkg/helmcli/list.go new file mode 100644 index 0000000..fa3f04c --- /dev/null +++ b/pkg/helmcli/list.go @@ -0,0 +1,42 @@ +package helmcli + +import ( + "context" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" + + "github.com/gojekfarm/albatross/pkg/helmcli/config" + "github.com/gojekfarm/albatross/pkg/helmcli/flags" +) + +// Lister acts as an entrypoint for the list action +type Lister struct { + action *action.List + envSettings *cli.EnvSettings +} + +// NewLister returns a new Lister instance +func NewLister(flg flags.ListFlags) *Lister { + envconfig := config.NewEnvConfig(&flg.GlobalFlags) + actionconfig := config.NewActionConfig(envconfig, &flg.GlobalFlags) + + list := action.NewList(actionconfig.Configuration) + list.AllNamespaces = flg.AllNamespaces + list.Deployed = flg.Deployed + list.Failed = flg.Failed + list.Pending = flg.Pending + list.Uninstalling = flg.Uninstalling + list.Uninstalled = flg.Uninstalled + + return &Lister{ + action: list, + envSettings: envconfig.EnvSettings, + } +} + +// List runs the list operation +func (l *Lister) List(ctx context.Context) ([]*release.Release, error) { + return l.action.Run() +} diff --git a/pkg/helmcli/upgrade.go b/pkg/helmcli/upgrade.go new file mode 100644 index 0000000..a0aced9 --- /dev/null +++ b/pkg/helmcli/upgrade.go @@ -0,0 +1,86 @@ +package helmcli + +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/release" + "helm.sh/helm/v3/pkg/storage/driver" + + "github.com/gojekfarm/albatross/pkg/helmcli/config" + "github.com/gojekfarm/albatross/pkg/helmcli/flags" +) + +type Upgrader struct { + action *action.Upgrade + history *action.History + envSettings *cli.EnvSettings + installer installer +} + +type installer interface { + Install(ctx context.Context, relName string, chartName string, values map[string]interface{}) (*release.Release, error) +} + +func NewUpgrader(flg flags.UpgradeFlags) *Upgrader { + //TODO: ifpossible envconfig could be moved to actionconfig new, remove pointer usage of globalflags + envconfig := config.NewEnvConfig(&flg.GlobalFlags) + actionconfig := config.NewActionConfig(envconfig, &flg.GlobalFlags) + + upgrade := action.NewUpgrade(actionconfig.Configuration) + history := action.NewHistory(actionconfig.Configuration) + installer := NewInstaller(flags.InstallFlags{ + DryRun: flg.DryRun, + Version: flg.Version, + GlobalFlags: flg.GlobalFlags, + }) + + upgrade.Namespace = flg.Namespace + upgrade.Install = flg.Install + upgrade.DryRun = flg.DryRun + + return &Upgrader{ + action: upgrade, + envSettings: envconfig.EnvSettings, + history: history, + installer: installer, + } +} + +// Upgrade executes the upgrade action +func (u *Upgrader) Upgrade(ctx context.Context, relName, chartName string, values map[string]interface{}) (*release.Release, error) { + // Install the release first if install is set to true + if u.action.Install { + u.history.Max = 1 + if _, err := u.history.Run(relName); err == driver.ErrReleaseNotFound { + release, err := u.installer.Install(ctx, relName, chartName, values) + if err != nil { + return nil, err + } + + return release, nil + } else if err != nil { + return nil, err + } + } + + chart, err := u.loadChart(chartName) + if err != nil { + return nil, fmt.Errorf("error loading chart: %w", err) + } + + return u.action.Run(relName, chart, values) +} + +func (u *Upgrader) loadChart(chartName string) (*chart.Chart, error) { + cp, err := u.action.LocateChart(chartName, u.envSettings) + if err != nil { + return nil, err + } + + return loader.Load(cp) +} diff --git a/api/logger/logger.go b/pkg/logger/logger.go similarity index 100% rename from api/logger/logger.go rename to pkg/logger/logger.go diff --git a/servercontext/context.go b/servercontext/context.go deleted file mode 100644 index 1d72d98..0000000 --- a/servercontext/context.go +++ /dev/null @@ -1,50 +0,0 @@ -package servercontext - -import ( - "fmt" - "log" - "os" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/cli" -) - -//TODO: rename package -var app Application - -type Application struct { - Config *cli.EnvSettings - ActionConfig *action.Configuration -} - -func App() *Application { - return &app -} - -func NewApp() *Application { - app.Config = envSettings() - app.ActionConfig = boostrapActionConfig() - return &app -} - -func envSettings() *cli.EnvSettings { - envSettings := cli.New() - for k, v := range envSettings.EnvVars() { - fmt.Println(k, v) - } - return envSettings -} - -func debug(format string, v ...interface{}) { - format = fmt.Sprintf("[debug] %s\n", format) - log.Output(2, fmt.Sprintf(format, v...)) -} - -func boostrapActionConfig() *action.Configuration { - actionConfig := new(action.Configuration) - if err := actionConfig.Init(app.Config.RESTClientGetter(), app.Config.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil { - log.Fatalf("error getting configuration: %v", err) - return nil - } - return actionConfig -}