Skip to content

Commit

Permalink
Add support for helm cli flags
Browse files Browse the repository at this point in the history
Currently, only a subset of available helm flags is implemented.
Adding new flags should be trivial.
  • Loading branch information
kaustubhkurve committed Aug 21, 2020
1 parent ee343a0 commit e6f5259
Show file tree
Hide file tree
Showing 33 changed files with 979 additions and 1,246 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 0 additions & 9 deletions api/chartloader.go

This file was deleted.

57 changes: 0 additions & 57 deletions api/install.go

This file was deleted.

77 changes: 77 additions & 0 deletions api/install/install.go
Original file line number Diff line number Diff line change
@@ -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
}
}
106 changes: 106 additions & 0 deletions api/install/install_api_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
47 changes: 47 additions & 0 deletions api/install/service.go
Original file line number Diff line number Diff line change
@@ -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(),
}
}
Loading

0 comments on commit e6f5259

Please sign in to comment.