Skip to content

Commit

Permalink
Add Repo feature (#11)
Browse files Browse the repository at this point in the history
* add API for helm repositories

* added documentation for update repository info

* updated api endpoint
remove flag for deprecated repos

* update gojektech-incubatror url

* added repository package under helmcli for better package hierarchy

* removed username and password from request

* return repo entry as response during successful add repo action

* updated swagger to reflect correct request and response payload

* convert NAME placeholder from kebab case to snakecase

* fixed add repo service test

* added info about force_update param in case of existing repository name

* move repo file validation to a different function

* fix cyclometric complexity for the function.

* update repository.NAME

* made successful response of add api simple repository instead of nested fields
Updated swagger docs to reflect the same

* renamed check function to checkFileUnlock

* Bump version to v1.1.0
  • Loading branch information
punit-kulal authored Oct 21, 2021
1 parent 60b7ecc commit e81aced
Show file tree
Hide file tree
Showing 17 changed files with 1,063 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ _dist/
dist/
bin/
vendor/
*.swp
cmd/albatross/__debug_bin
pkg/helmcli/repository/testdata/config.lock
coverage.txt
133 changes: 133 additions & 0 deletions api/repository/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package repository

import (
"context"
"encoding/json"
"errors"
"net/http"

"github.com/gojekfarm/albatross/pkg/logger"

"github.com/gorilla/mux"
)

// AddRequest is the body for PUT request to repository
// swagger:model addRepoRequestBody
type AddRequest struct {
Name string `json:"-"`
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`

// example: false
ForceUpdate bool `json:"force_update"`

// example: false
InsecureSkipTLSverify bool `json:"skip_tls_verify"`
}

type addService interface {
Add(context.Context, AddRequest) (Entry, error)
}

// AddErrorResponse body of non 2xx response
// swagger:model addRepoErrorResponseBody
type AddErrorResponse struct {
Error string `json:"error"`
}

// Entry contains metadata about a helm repository entry object
// swagger:model addRepoEntry
type Entry struct {
Name string `json:"name"`
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
}

const URLNamePlaceholder string = "repository_name"

// AddHandler handles a repo add/update request
// swagger:operation PUT /repositories/{repository_name} repository addOperation
//
// Add/Update a chart repository to the server.
// The endpoint is idempotent and a repository can be updated by using the force_update parameter to true
// ---
// produces:
// - application/json
// parameters:
// - name: repository_name
// in: path
// required: true
// type: string
// format: string
// - name: Body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/addRepoRequestBody"
// schemes:
// - http
// responses:
// '200':
// description: "The repository was added successfully"
// schema:
// $ref: "#/definitions/addRepoEntry"
// '400':
// description: "Invalid Request"
// schema:
// $ref: "#/definitions/addRepoErrorResponseBody"
// '500':
// description: "Something went with the server"
// schema:
// $ref: "#/definitions/addRepoErrorResponseBody"
func AddHandler(s addService) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

vars := mux.Vars(r)
var req AddRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logger.Errorf("[RepoAdd] error decoding request: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}

if err := req.isValid(); err != nil {
logger.Errorf("[RepoAdd] error validating request %v", err)
respondAddError(w, "error adding repo", err, http.StatusBadRequest)
return
}

req.Name = vars[URLNamePlaceholder]

resp, err := s.Add(r.Context(), req)

if err != nil {
logger.Errorf("[RepoAdd] error adding repo: %v", err)
respondAddError(w, "error adding repo", err, http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(&resp); err != nil {
respondAddError(w, "error writing response: %v", err, http.StatusInternalServerError)
return
}
})
}

func respondAddError(w http.ResponseWriter, logprefix string, err error, errorCode int) {
response := AddErrorResponse{Error: err.Error()}
w.WriteHeader(errorCode)
if err := json.NewEncoder(w).Encode(&response); err != nil {
logger.Errorf("[AddRepo] %s %v", logprefix, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

func (req AddRequest) isValid() error {
if req.URL == "" {
return errors.New("url cannot be empty")
}
return nil
}
133 changes: 133 additions & 0 deletions api/repository/add_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package repository

import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/gojekfarm/albatross/pkg/logger"

"github.com/gorilla/mux"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gotest.tools/assert"
)

type mockService struct {
mock.Mock
}

func (m *mockService) Add(ctx context.Context, req AddRequest) (Entry, error) {
args := m.Called(ctx, req)
return args.Get(0).(Entry), args.Error(1)
}

type RepoAddTestSuite struct {
suite.Suite
recorder *httptest.ResponseRecorder
server *httptest.Server
mockService *mockService
}

func (s *RepoAddTestSuite) SetupSuite() {
logger.Setup("default")
}

func (s *RepoAddTestSuite) SetupTest() {
s.recorder = httptest.NewRecorder()
s.mockService = new(mockService)
router := mux.NewRouter()
path := fmt.Sprintf("/repositories/{%s}", URLNamePlaceholder)
router.Handle(path, AddHandler(s.mockService)).Methods(http.MethodPut)
s.server = httptest.NewServer(router)
}

func (s *RepoAddTestSuite) TestRepoAddSuccessFul() {
repoName := "gojek-incubator"
urlName := "https://gojek.github.io/charts/incubator/"
body := fmt.Sprintf(`{"url":"%s", "username":"admin", "password":"123",
"allow_deprecated_repos":true, "force_update": true, "skip_tls_verify": true}`, urlName)

req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/repositories/%s", s.server.URL, repoName), strings.NewReader(body))
request := AddRequest{
Name: repoName,
URL: urlName,
Username: "admin",
Password: "123",
ForceUpdate: true,
InsecureSkipTLSverify: true,
}

mockAddResponse := Entry{
Name: request.Name,
URL: request.URL,
Username: request.Username,
Password: request.Password,
}
s.mockService.On("Add", mock.Anything, request).Return(mockAddResponse, nil)

resp, err := http.DefaultClient.Do(req)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)

respBody, _ := ioutil.ReadAll(resp.Body)
require.NoError(s.T(), err)

parsedResponse := &Entry{}
err = json.Unmarshal(respBody, parsedResponse)
require.NoError(s.T(), err)
assert.Equal(s.T(), mockAddResponse.Name, parsedResponse.Name)
assert.Equal(s.T(), mockAddResponse.URL, parsedResponse.URL)
assert.Equal(s.T(), mockAddResponse.Username, parsedResponse.Username)
assert.Equal(s.T(), mockAddResponse.Password, parsedResponse.Password)
s.mockService.AssertExpectations(s.T())
}

func (s *RepoAddTestSuite) TestRepoAddInvalidRequest() {
repoName := "gojek-incubator"
body := `{"username":"admin", "password":"123"}`

req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/repositories/%s", s.server.URL, repoName), strings.NewReader(body))

resp, err := http.DefaultClient.Do(req)
assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
expectedResponse := `{"error":"url cannot be empty"}` + "\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 *RepoAddTestSuite) TestRepoAddFailure() {
repoName := "gojek-incubator"
urlName := "https://gojek.github.io/charts/incubator/"
body := fmt.Sprintf(`{"url":"%s", "username":"admin", "password":"123"}`, urlName)

req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/repositories/%s", s.server.URL, repoName), strings.NewReader(body))
request := AddRequest{
Name: repoName,
URL: urlName,
Username: "admin",
Password: "123",
}

s.mockService.On("Add", mock.Anything, request).Return(Entry{}, errors.New("error adding repository"))

resp, err := http.DefaultClient.Do(req)
assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode)
expectedResponse := `{"error":"error adding repository"}` + "\n"
respBody, _ := ioutil.ReadAll(resp.Body)
assert.Equal(s.T(), expectedResponse, string(respBody))
require.NoError(s.T(), err)
s.mockService.AssertExpectations(s.T())
}

func TestRepoAddAPI(t *testing.T) {
suite.Run(t, new(RepoAddTestSuite))
}
53 changes: 53 additions & 0 deletions api/repository/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package repository

import (
"context"
"fmt"

"github.com/gojekfarm/albatross/pkg/helmcli/flags"
"github.com/gojekfarm/albatross/pkg/helmcli/repository"
"github.com/gojekfarm/albatross/pkg/logger"

"helm.sh/helm/v3/pkg/repo"
)

type Service struct {
cli repository.Client
}

func (s Service) Add(ctx context.Context, req AddRequest) (Entry, error) {
addFlags := flags.AddFlags{
Name: req.Name,
URL: req.URL,
ForceUpdate: req.ForceUpdate,
}

adder, err := s.cli.NewAdder(addFlags)
if err != nil {
return Entry{}, err
}

entry, err := adder.Add(ctx)
if err != nil {
return Entry{}, err
}
return getEntry(entry)
}

func NewService(cli repository.Client) Service {
return Service{cli}
}

func getEntry(entry *repo.Entry) (Entry, error) {
if entry != nil {
logger.Infof("Repository %s with URL: %s has been added", entry.Name, entry.URL)
return Entry{
Name: entry.Name,
URL: entry.URL,
Username: entry.Username,
Password: entry.Password,
}, nil
}

return Entry{}, fmt.Errorf("couldn't get repository from user")
}
Loading

0 comments on commit e81aced

Please sign in to comment.