Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Repo feature #11

Merged
merged 18 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
135 changes: 135 additions & 0 deletions api/repository/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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) (AddResponse, error)
}

// AddResponse common response body of add api
// swagger:model addRepoResponseBody
type AddResponse struct {
Error string `json:"error,omitempty"`
// Release release meta data, field is available only when status code is 2xx
Repository *Entry `json:"repository,omitempty"`
kaustubhkurve marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 NAME string = "repository_name"

// AddHandler handles a repo add/update request
// swagger:operation PUT /repositories/{repository_name} repository addOperation
punit-kulal marked this conversation as resolved.
Show resolved Hide resolved
//
// 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/addRepoOkResponseBody"
// '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[NAME]

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 := AddResponse{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
}
135 changes: 135 additions & 0 deletions api/repository/add_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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) (AddResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(AddResponse), 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}", NAME)
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 := AddResponse{
Repository: &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 := &AddResponse{}
err = json.Unmarshal(respBody, parsedResponse)
require.NoError(s.T(), err)
assert.Equal(s.T(), mockAddResponse.Repository.Name, parsedResponse.Repository.Name)
assert.Equal(s.T(), mockAddResponse.Repository.URL, parsedResponse.Repository.URL)
assert.Equal(s.T(), mockAddResponse.Repository.Username, parsedResponse.Repository.Username)
assert.Equal(s.T(), mockAddResponse.Repository.Password, parsedResponse.Repository.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(AddResponse{}, 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))
}
49 changes: 49 additions & 0 deletions api/repository/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package repository

import (
"context"

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

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

type Service struct {
cli repository.Client
}

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

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

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

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

func getEntry(entry *repo.Entry) *Entry {
if entry != nil {
return &Entry{
Name: entry.Name,
URL: entry.URL,
Username: entry.Username,
Password: entry.Password,
}
}
return nil
}
Loading