diff --git a/backend/pkg/helm/repository.go b/backend/pkg/helm/repository.go index 13b484c7f8f..2ebbb93cd15 100644 --- a/backend/pkg/helm/repository.go +++ b/backend/pkg/helm/repository.go @@ -45,13 +45,17 @@ var ( errRepositoryLockNotAcquired = errors.New("repository lock not acquired") ) -// add repository. +// CertFile, KeyFile, and CAFile are intentionally omitted from this struct. +// Accepting filesystem paths from HTTP clients would allow arbitrary local +// file reads on the server. These values are preserved from the existing +// repo entry only (server-side state). type AddUpdateRepoRequest struct { - Name string `json:"name"` - URL string `json:"url"` - // TODO: Figure out how to support auth - // like username, password, certfile etc - // https://github.com/helm/helm/blob/39ca699ca790e02ba36753dec6ba4177cc68d417/cmd/helm/repo_add.go#L169 + Name string `json:"name"` + URL string `json:"url"` + Username *string `json:"username"` + Password *string `json:"password"` + InsecureSkipTLSverify *bool `json:"insecureSkipTLSverify"` + PassCredentialsAll *bool `json:"passCredentialsAll"` } func (r AddUpdateRepoRequest) Validate() error { @@ -66,19 +70,20 @@ func (r AddUpdateRepoRequest) Validate() error { return nil } -// Creates a filename if it's not there, including any missing directories. func createFileIfNotThere(fileName string) error { _, err := os.Stat(fileName) if os.IsNotExist(err) { - // create changes - _, err = createFullPath(fileName) - return err + file, err := createFullPath(fileName) + if err != nil { + return err + } + + return file.Close() } - return nil + return err } -// Uses a file lock like the helm tool. func lockRepositoryFile(lockCtx context.Context, repositoryConfig string) (bool, *flock.Flock, error) { var lockPath string @@ -111,8 +116,29 @@ func ensureRepositoryFileLocked(locked bool, err error) error { const timeoutForLock = 30 * time.Second -// Adds a repository with name, url to the helm config. Returns error if there is one. -func addRepository(name string, url string, settings *cli.EnvSettings) error { +// applyRequestFields applies non-nil fields from request onto entry, +// leaving existing entry values unchanged for any field not set in the request. +func applyRequestFields(entry *repo.Entry, request AddUpdateRepoRequest) { + if request.Username != nil { + entry.Username = *request.Username + } + + if request.Password != nil { + entry.Password = *request.Password + } + + if request.InsecureSkipTLSverify != nil { + entry.InsecureSkipTLSverify = *request.InsecureSkipTLSverify + } + + if request.PassCredentialsAll != nil { + entry.PassCredentialsAll = *request.PassCredentialsAll + } +} + +// addRepository adds a repository with the given request fields to the helm config. +// Returns an error if the repository cannot be created or its index downloaded. +func addRepository(request AddUpdateRepoRequest, settings *cli.EnvSettings) error { err := createFileIfNotThere(settings.RepositoryConfig) if err != nil { logger.Log(logger.LevelError, nil, err, "creating empty RepositoryConfig file") @@ -129,39 +155,36 @@ func addRepository(name string, url string, settings *cli.EnvSettings) error { } defer func() { - err := fileLock.Unlock() - if err != nil { + if err := fileLock.Unlock(); err != nil { logger.Log(logger.LevelError, nil, err, "unlocking repository config file") } }() - // read repo file repoFile, err := repo.LoadFile(settings.RepositoryConfig) if err != nil { logger.Log(logger.LevelError, nil, err, "reading repo file") return err } - // add repo newRepo := &repo.Entry{ - Name: name, - URL: url, + Name: request.Name, + URL: request.URL, } - repo, err := repo.NewChartRepository(newRepo, getter.All(settings)) + applyRequestFields(newRepo, request) + + r, err := repo.NewChartRepository(newRepo, getter.All(settings)) if err != nil { logger.Log(logger.LevelError, nil, err, "creating chart repository") return err } - // download chart repo index - _, err = repo.DownloadIndexFile() + _, err = r.DownloadIndexFile() if err != nil { logger.Log(logger.LevelError, nil, err, "downloading index file") return err } - // write repo file repoFile.Update(newRepo) err = repoFile.WriteFile(settings.RepositoryConfig, defaultNewConfigFileMode) @@ -174,7 +197,6 @@ func addRepository(name string, url string, settings *cli.EnvSettings) error { } func (h *Handler) AddRepo(w http.ResponseWriter, r *http.Request) { - // parse request var request AddUpdateRepoRequest err := json.NewDecoder(r.Body).Decode(&request) @@ -185,47 +207,49 @@ func (h *Handler) AddRepo(w http.ResponseWriter, r *http.Request) { return } - err = request.Validate() - if err != nil { + if err = request.Validate(); err != nil { logger.Log(logger.LevelError, nil, err, "validating request") http.Error(w, err.Error(), http.StatusBadRequest) return } - err = addRepository(request.Name, request.URL, h.EnvSettings) + err = addRepository(request, h.EnvSettings) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - // respond response := map[string]string{ "message": "success", } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) + var buf bytes.Buffer - err = json.NewEncoder(w).Encode(response) - if err != nil { + if err = json.NewEncoder(&buf).Encode(response); err != nil { logger.Log(logger.LevelError, nil, err, "encoding response") http.Error(w, err.Error(), http.StatusInternalServerError) return } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if _, err = w.Write(buf.Bytes()); err != nil { + logger.Log(logger.LevelError, nil, err, "writing response") + } } -// List repository. type repositoryInfo struct { Name string `json:"name"` URL string `json:"url"` } + type ListRepoResponse struct { Repositories []repositoryInfo `json:"repositories"` } -// Create a full path, including directories if it does not exist. func createFullPath(p string) (*os.File, error) { if err := os.MkdirAll(filepath.Dir(p), defaultNewConfigFolderMode); err != nil { return nil, err @@ -241,14 +265,12 @@ func listRepositories(settings *cli.EnvSettings) ([]repositoryInfo, error) { return nil, err } - // read repo file repoFile, err := repo.LoadFile(settings.RepositoryConfig) if err != nil { logger.Log(logger.LevelError, nil, err, "reading repo file") return nil, err } - // response repositories := make([]repositoryInfo, 0, len(repoFile.Repositories)) for _, repo := range repoFile.Repositories { @@ -274,8 +296,7 @@ func (h *Handler) ListRepo(w http.ResponseWriter, r *http.Request) { var buf bytes.Buffer - err = json.NewEncoder(&buf).Encode(response) - if err != nil { + if err = json.NewEncoder(&buf).Encode(response); err != nil { logger.Log(logger.LevelError, nil, err, "encoding response") http.Error(w, err.Error(), http.StatusInternalServerError) @@ -285,8 +306,8 @@ func (h *Handler) ListRepo(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if _, err := w.Write(buf.Bytes()); err != nil { - logger.Log(logger.LevelError, nil, err, "writing repo list response") + if _, err = w.Write(buf.Bytes()); err != nil { + logger.Log(logger.LevelError, nil, err, "writing response") } } @@ -321,11 +342,10 @@ func RemoveRepository(name string, settings *cli.EnvSettings) error { isRemoved := repoFile.Remove(name) if !isRemoved { - logger.Log(logger.LevelError, nil, errRepositoryNotFound, "repository not found") + logger.Log(logger.LevelError, map[string]string{"repository": name}, errRepositoryNotFound, "repository not found") return errRepositoryNotFound } - // write repo file err = repoFile.WriteFile(settings.RepositoryConfig, defaultNewConfigFileMode) if err != nil { logger.Log(logger.LevelError, nil, err, "writing repo file") @@ -335,7 +355,6 @@ func RemoveRepository(name string, settings *cli.EnvSettings) error { return nil } -// Remove repository name. func (h *Handler) RemoveRepo(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { @@ -358,7 +377,27 @@ func (h *Handler) RemoveRepo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func copyExistingRepoFields(updated, existing *repo.Entry) { + updated.Username = existing.Username + updated.Password = existing.Password + updated.CertFile = existing.CertFile + updated.KeyFile = existing.KeyFile + updated.CAFile = existing.CAFile + updated.InsecureSkipTLSverify = existing.InsecureSkipTLSverify + updated.PassCredentialsAll = existing.PassCredentialsAll +} + +// UpdateRepository updates an existing repository entry. +// It returns errRepositoryNotFound if the named repository does not exist. +// Callers that need create-or-update behaviour should use AddRepo instead. func UpdateRepository(name, url string, settings *cli.EnvSettings) error { + return updateRepositoryWithRequest(AddUpdateRepoRequest{ + Name: name, + URL: url, + }, settings) +} + +func updateRepositoryWithRequest(request AddUpdateRepoRequest, settings *cli.EnvSettings) error { err := createFileIfNotThere(settings.RepositoryConfig) if err != nil { logger.Log(logger.LevelError, nil, err, "creating empty RepositoryConfig file") @@ -387,11 +426,27 @@ func UpdateRepository(name, url string, settings *cli.EnvSettings) error { return err } - // update repo - repoFile.Update(&repo.Entry{ - Name: name, - URL: url, - }) + updated := &repo.Entry{ + Name: request.Name, + URL: request.URL, + } + + existing := repoFile.Get(request.Name) + if existing == nil { + logger.Log( + logger.LevelError, + map[string]string{"repository": request.Name}, + errRepositoryNotFound, + "repository not found", + ) + + return errRepositoryNotFound + } + + copyExistingRepoFields(updated, existing) + + applyRequestFields(updated, request) + repoFile.Update(updated) err = repoFile.WriteFile(settings.RepositoryConfig, defaultNewConfigFileMode) if err != nil { @@ -402,9 +457,7 @@ func UpdateRepository(name, url string, settings *cli.EnvSettings) error { return nil } -// Update repository name. func (h *Handler) UpdateRepository(w http.ResponseWriter, r *http.Request) { - // parse request var request AddUpdateRepoRequest err := json.NewDecoder(r.Body).Decode(&request) @@ -415,17 +468,22 @@ func (h *Handler) UpdateRepository(w http.ResponseWriter, r *http.Request) { return } - err = request.Validate() - if err != nil { + if err = request.Validate(); err != nil { logger.Log(logger.LevelError, nil, err, "validating request") http.Error(w, err.Error(), http.StatusBadRequest) return } - err = UpdateRepository(request.Name, request.URL, h.EnvSettings) + err = updateRepositoryWithRequest(request, h.EnvSettings) if err != nil { + if errors.Is(err, errRepositoryNotFound) { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return } diff --git a/backend/pkg/helm/repository_test.go b/backend/pkg/helm/repository_test.go index 3e254a9bd03..1c9f3311e94 100644 --- a/backend/pkg/helm/repository_test.go +++ b/backend/pkg/helm/repository_test.go @@ -22,6 +22,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" "path/filepath" "testing" @@ -33,6 +34,11 @@ import ( "helm.sh/helm/v3/pkg/repo" ) +const ( + testUsername = "testuser" + testPassword = "testpass" +) + func newHelmHandler(t *testing.T) *helm.Handler { t.Helper() @@ -45,15 +51,57 @@ func newHelmHandler(t *testing.T) *helm.Handler { return helmHandler } +// newIsolatedHelmHandler creates a Handler with its own temporary repository config, +// isolated from the shared test state used by newHelmHandler. +func newIsolatedHelmHandler(t *testing.T) *helm.Handler { + t.Helper() + + customSettings := cli.New() + customSettings.RepositoryConfig = filepath.Join(t.TempDir(), "repositories.yaml") + + c := cache.New[interface{}]() + require.NotNil(t, c) + + helmHandler, err := helm.NewHandlerWithSettings(c, customSettings) + require.NoError(t, err) + + return helmHandler +} + +// mustJSONBody marshals v to JSON and returns a buffer. Safe to use with test credentials. +func mustJSONBody(t *testing.T, v any) *bytes.Buffer { + t.Helper() + + b, err := json.Marshal(v) + require.NoError(t, err) + + return bytes.NewBuffer(b) +} + +// newAuthRepoIndexServer starts a test HTTP server that requires basic auth. +func newAuthRepoIndexServer(t *testing.T) *httptest.Server { + t.Helper() + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || user != testUsername || pass != testPassword { + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"apiVersion":"v1","entries":{},"generated":"2025-01-01T00:00:00Z"}`)) + })) +} + func checkRepoExists(t *testing.T, helmHandler *helm.Handler, repoName string) bool { t.Helper() - // list repositories listRepoReq, err := http.NewRequestWithContext(context.Background(), "GET", "/clusters/minikube/helm/repositories", nil) require.NoError(t, err) - // response recorder rr := httptest.NewRecorder() helmHandler.ListRepo(rr, listRepoReq) @@ -77,26 +125,20 @@ func checkRepoExists(t *testing.T, helmHandler *helm.Handler, repoName string) b func testAddRepo(t *testing.T, helmHandler *helm.Handler, repoName, repoURL string) { t.Helper() - // add headlmap repo addRepo := helm.AddUpdateRepoRequest{ Name: "headlamp_test_repo", URL: "https://kubernetes-sigs.github.io/headlamp/", } - addRepoRequestJSON, err := json.Marshal(addRepo) - require.NoError(t, err) - addRepoRequest, err := http.NewRequestWithContext(context.Background(), "POST", - "/clusters/minikube/helm/repositories/charts", bytes.NewBuffer(addRepoRequestJSON)) + "/clusters/minikube/helm/repositories/charts", mustJSONBody(t, addRepo)) require.NoError(t, err) - // response recorder rr := httptest.NewRecorder() helmHandler.AddRepo(rr, addRepoRequest) assert.Equal(t, http.StatusOK, rr.Code) - // check if repository exists in list assert.True(t, checkRepoExists(t, helmHandler, "headlamp_test_repo")) } @@ -114,35 +156,32 @@ func TestAddRepository(t *testing.T) { bytes.NewBufferString("some invalid request string")) require.NoError(t, err) - // response recorder rr := httptest.NewRecorder() helmHandler.AddRepo(rr, addRepoRequest) assert.Equal(t, http.StatusBadRequest, rr.Code) }) - t.Run("missing_add_repo_name", func(t *testing.T) { - addRepoRequest, err := http.NewRequestWithContext(context.Background(), - "POST", "/clusters/minikube/helm/repositories/charts", + t.Run("missing_name", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), "POST", + "/clusters/minikube/helm/repositories/charts", bytes.NewBufferString(`{"url":"https://kubernetes-sigs.github.io/headlamp/"}`)) require.NoError(t, err) rr := httptest.NewRecorder() - helmHandler.AddRepo(rr, addRepoRequest) - + helmHandler.AddRepo(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Contains(t, rr.Body.String(), "name is required") }) - t.Run("missing_add_repo_url", func(t *testing.T) { - addRepoRequest, err := http.NewRequestWithContext(context.Background(), - "POST", "/clusters/minikube/helm/repositories/charts", + t.Run("missing_url", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), "POST", + "/clusters/minikube/helm/repositories/charts", bytes.NewBufferString(`{"name":"headlamp_test_repo"}`)) require.NoError(t, err) rr := httptest.NewRecorder() - helmHandler.AddRepo(rr, addRepoRequest) - + helmHandler.AddRepo(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Contains(t, rr.Body.String(), "url is required") }) @@ -155,12 +194,10 @@ func TestRemoveRepository(t *testing.T) { t.Run("remove_repo_success", func(t *testing.T) { testAddRepo(t, helmHandler, "headlamp_test_repo", "https://kubernetes-sigs.github.io/headlamp/") - // remove repository removeRepoRequest, err := http.NewRequestWithContext(context.Background(), "DELETE", "/clusters/minikube/helm/repositories/?name=headlamp_test_repo", nil) require.NoError(t, err) - // response recorder rr := httptest.NewRecorder() helmHandler.RemoveRepo(rr, removeRepoRequest) @@ -184,99 +221,55 @@ func TestUpdateRepo(t *testing.T) { helmHandler := newHelmHandler(t) t.Run("update_repo_success", func(t *testing.T) { - testAddRepo(t, helmHandler, "headlamp_test_repo", "https://kubernetes-sigs.github.io/headlamp/") - - // update repository request - updateRepo := helm.AddUpdateRepoRequest{ - Name: "headlamp_test_repo", - URL: "https://kubernetes-sigs-update-url.github.io/headlamp/", - } - - updateRepoRequestJSON, err := json.Marshal(updateRepo) - require.NoError(t, err) + testUpdateRepo(t, helmHandler) + }) - updateRepoRequest, err := http.NewRequestWithContext(context.Background(), - "PUT", "/clusters/minikube/helm/repositories", - bytes.NewBuffer(updateRepoRequestJSON)) + t.Run("invalid_update_repo_request", func(t *testing.T) { + updateRepoRequest, err := http.NewRequestWithContext(context.Background(), "PUT", + "/clusters/minikube/helm/repositories", bytes.NewBufferString("some invalid request string")) require.NoError(t, err) - // response recorder rr := httptest.NewRecorder() helmHandler.UpdateRepository(rr, updateRepoRequest) - assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) - // check if repository exists in list - // list repositories - listRepoReq, err := http.NewRequestWithContext(context.Background(), - "GET", "/clusters/minikube/helm/repositories", nil) + t.Run("missing_name", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), "PUT", + "/clusters/minikube/helm/repositories", + bytes.NewBufferString(`{"url":"https://kubernetes-sigs-update-url.github.io/headlamp/"}`)) require.NoError(t, err) - // response recorder - rr = httptest.NewRecorder() - - helmHandler.ListRepo(rr, listRepoReq) - - var listRepoResponse helm.ListRepoResponse - - err = json.Unmarshal(rr.Body.Bytes(), &listRepoResponse) - assert.NoError(t, err) - - for _, repo := range listRepoResponse.Repositories { - if repo.Name == "headlamp_test_repo" { - assert.Equal(t, "https://kubernetes-sigs-update-url.github.io/headlamp/", repo.URL) - } - } - }) - - t.Run("invalid_update_repo_request", func(t *testing.T) { - testInvalidUpdateRepoRequest(t, helmHandler, "some invalid request string", "") + rr := httptest.NewRecorder() + helmHandler.UpdateRepository(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Contains(t, rr.Body.String(), "name is required") }) - t.Run("missing_update_repo_name", func(t *testing.T) { - testInvalidUpdateRepoRequest(t, helmHandler, - `{"url":"https://kubernetes-sigs-update-url.github.io/headlamp/"}`, "name is required") - }) + t.Run("missing_url", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), "PUT", + "/clusters/minikube/helm/repositories", + bytes.NewBufferString(`{"name":"headlamp_test_repo"}`)) + require.NoError(t, err) - t.Run("missing_update_repo_url", func(t *testing.T) { - testInvalidUpdateRepoRequest(t, helmHandler, `{"name":"headlamp_test_repo"}`, "url is required") + rr := httptest.NewRecorder() + helmHandler.UpdateRepository(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Contains(t, rr.Body.String(), "url is required") }) } -func testInvalidUpdateRepoRequest( - t *testing.T, - helmHandler *helm.Handler, - requestBody string, - expectedError string, -) { - t.Helper() - - updateRepoRequest, err := http.NewRequestWithContext(context.Background(), "PUT", - "/clusters/minikube/helm/repositories", bytes.NewBufferString(requestBody)) - require.NoError(t, err) - - rr := httptest.NewRecorder() - helmHandler.UpdateRepository(rr, updateRepoRequest) - - assert.Equal(t, http.StatusBadRequest, rr.Code) - - if expectedError != "" { - assert.Contains(t, rr.Body.String(), expectedError) - } -} - // TestListRepositories. func TestListRepositories(t *testing.T) { helmHandler := newHelmHandler(t) testAddRepo(t, helmHandler, "headlamp_test_repo", "https://kubernetes-sigs.github.io/headlamp/") - // list repositories listRepoReq, err := http.NewRequestWithContext(context.Background(), "GET", "/clusters/minikube/helm/repositories", nil) require.NoError(t, err) - // response recorder rr := httptest.NewRecorder() helmHandler.ListRepo(rr, listRepoReq) @@ -330,3 +323,157 @@ func TestListRepoSetsJSONContentType(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Contains(t, rr.Header().Get("Content-Type"), "application/json") } + +func TestAddRepositoryWithAuth(t *testing.T) { + helmHandler := newIsolatedHelmHandler(t) + + ts := newAuthRepoIndexServer(t) + defer ts.Close() + + username := testUsername + password := testPassword + + addRepo := helm.AddUpdateRepoRequest{ + Name: "auth_test_repo", + URL: ts.URL, + Username: &username, + Password: &password, + } + + req, err := http.NewRequestWithContext(context.Background(), "POST", + "/clusters/minikube/helm/repositories/charts", mustJSONBody(t, addRepo)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + helmHandler.AddRepo(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + + repoFile, err := repo.LoadFile(helmHandler.RepositoryConfig) + require.NoError(t, err) + + addedEntry := repoFile.Get("auth_test_repo") + require.NotNil(t, addedEntry) + assert.Equal(t, testUsername, addedEntry.Username) + assert.Equal(t, testPassword, addedEntry.Password) +} + +func testUpdateRepo(t *testing.T, helmHandler *helm.Handler) { + t.Helper() + + testAddRepo(t, helmHandler, "headlamp_test_repo", "https://kubernetes-sigs.github.io/headlamp/") + + updateRepo := helm.AddUpdateRepoRequest{ + Name: "headlamp_test_repo", + URL: "https://kubernetes-sigs-update-url.github.io/headlamp/", + } + + updateRepoRequest, err := http.NewRequestWithContext(context.Background(), + "PUT", "/clusters/minikube/helm/repositories", + mustJSONBody(t, updateRepo)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + helmHandler.UpdateRepository(rr, updateRepoRequest) + assert.Equal(t, http.StatusOK, rr.Code) + + listRepoReq, err := http.NewRequestWithContext(context.Background(), + "GET", "/clusters/minikube/helm/repositories", nil) + require.NoError(t, err) + + rr = httptest.NewRecorder() + helmHandler.ListRepo(rr, listRepoReq) + + var listRepoResponse helm.ListRepoResponse + + err = json.Unmarshal(rr.Body.Bytes(), &listRepoResponse) + assert.NoError(t, err) + + for _, repo := range listRepoResponse.Repositories { + if repo.Name == "headlamp_test_repo" { + assert.Equal(t, "https://kubernetes-sigs-update-url.github.io/headlamp/", repo.URL) + } + } +} + +func TestUpdateRepositoryPreservesAuth(t *testing.T) { + helmHandler := newIsolatedHelmHandler(t) + + ts := newAuthRepoIndexServer(t) + defer ts.Close() + + username := testUsername + password := testPassword + + addRepo := helm.AddUpdateRepoRequest{ + Name: "auth_update_repo", + URL: ts.URL, + Username: &username, + Password: &password, + } + + req, err := http.NewRequestWithContext(context.Background(), "POST", + "/clusters/minikube/helm/repositories/charts", mustJSONBody(t, addRepo)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + helmHandler.AddRepo(rr, req) + require.Equal(t, http.StatusOK, rr.Code) + + updateRepo := helm.AddUpdateRepoRequest{ + Name: "auth_update_repo", + URL: ts.URL, + } + + updateReq, err := http.NewRequestWithContext(context.Background(), "PUT", + "/clusters/minikube/helm/repositories", mustJSONBody(t, updateRepo)) + require.NoError(t, err) + + rr = httptest.NewRecorder() + helmHandler.UpdateRepository(rr, updateReq) + assert.Equal(t, http.StatusOK, rr.Code) + + repoFile, err := repo.LoadFile(helmHandler.RepositoryConfig) + require.NoError(t, err) + + updatedEntry := repoFile.Get("auth_update_repo") + require.NotNil(t, updatedEntry) + + assert.Equal(t, testUsername, updatedEntry.Username) + assert.Equal(t, testPassword, updatedEntry.Password) +} + +func TestCreateFileIfNotThere(t *testing.T) { + t.Run("creates_missing_file_and_directories", func(t *testing.T) { + dir := t.TempDir() + + customSettings := cli.New() + customSettings.RepositoryConfig = filepath.Join(dir, "nonexistent", "subdir", "repositories.yaml") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"apiVersion":"v1","entries":{},"generated":"2025-01-01T00:00:00Z"}`)) + })) + defer ts.Close() + + c := cache.New[interface{}]() + helmHandler, err := helm.NewHandlerWithSettings(c, customSettings) + require.NoError(t, err) + + addRepo := helm.AddUpdateRepoRequest{ + Name: "file_create_test_repo", + URL: ts.URL, + } + + req, err := http.NewRequestWithContext(context.Background(), "POST", + "/clusters/minikube/helm/repositories/charts", mustJSONBody(t, addRepo)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + helmHandler.AddRepo(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + + _, statErr := os.Stat(customSettings.RepositoryConfig) + assert.NoError(t, statErr, "repository config file should have been created by createFileIfNotThere") + }) +}