From caf382b64c135368469b9f47a67d1788f8cc66cf Mon Sep 17 00:00:00 2001 From: Devon Steenberg Date: Tue, 22 Jul 2025 08:36:08 +1200 Subject: [PATCH] feat(git): support bearer token auth for git [BE-11770] (#879) --- api/git/azure_integration_test.go | 78 +++++++- api/git/backup.go | 11 +- api/git/git.go | 42 +++- api/git/git_integration_test.go | 189 ++++++++++++++++-- api/git/git_test.go | 22 +- api/git/service.go | 88 +++++++- api/git/types/types.go | 18 +- api/git/update/update.go | 31 ++- .../customtemplate_git_fetch_test.go | 38 +++- .../customtemplates/customtemplate_update.go | 27 ++- .../edgestacks/edgestack_create_git.go | 19 +- .../handler/gitops/git_repo_file_preview.go | 19 +- api/http/handler/stacks/stack_update_git.go | 31 ++- .../stacks/stack_update_git_redeploy.go | 20 +- .../handler/stacks/update_kubernetes_stack.go | 27 ++- api/http/handler/templates/template_file.go | 11 +- api/http/proxy/factory/docker/transport.go | 10 +- api/internal/testhelpers/git_service.go | 45 ++++- api/portainer.go | 40 +++- api/stacks/stackutils/gitops.go | 21 +- 20 files changed, 670 insertions(+), 117 deletions(-) diff --git a/api/git/azure_integration_test.go b/api/git/azure_integration_test.go index 3e297a129..5de18b303 100644 --- a/api/git/azure_integration_test.go +++ b/api/git/azure_integration_test.go @@ -58,7 +58,15 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) { t.Run(tt.name, func(t *testing.T) { dst := t.TempDir() repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password) - err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "", false) + err := service.CloneRepository( + dst, + repositoryUrl, + tt.args.referenceName, + "", + "", + gittypes.GitCredentialAuthType_Basic, + false, + ) assert.NoError(t, err) assert.FileExists(t, filepath.Join(dst, "README.md")) }) @@ -73,7 +81,15 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) { dst := t.TempDir() - err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat, false) + err := service.CloneRepository( + dst, + privateAzureRepoURL, + "refs/heads/main", + "", + pat, + gittypes.GitCredentialAuthType_Basic, + false, + ) assert.NoError(t, err) assert.FileExists(t, filepath.Join(dst, "README.md")) } @@ -84,7 +100,14 @@ func TestService_LatestCommitID_Azure(t *testing.T) { pat := getRequiredValue(t, "AZURE_DEVOPS_PAT") service := NewService(context.TODO()) - id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat, false) + id, err := service.LatestCommitID( + privateAzureRepoURL, + "refs/heads/main", + "", + pat, + gittypes.GitCredentialAuthType_Basic, + false, + ) assert.NoError(t, err) assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty") } @@ -96,7 +119,14 @@ func TestService_ListRefs_Azure(t *testing.T) { username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") service := NewService(context.TODO()) - refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false, false) + refs, err := service.ListRefs( + privateAzureRepoURL, + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + ) assert.NoError(t, err) assert.GreaterOrEqual(t, len(refs), 1) } @@ -108,8 +138,8 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) { username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond) - go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false) - service.ListRefs(privateAzureRepoURL, username, accessToken, false, false) + go service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) + service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) time.Sleep(2 * time.Second) } @@ -247,7 +277,17 @@ func TestService_ListFiles_Azure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, false, tt.extensions, false) + paths, err := service.ListFiles( + tt.args.repositoryUrl, + tt.args.referenceName, + tt.args.username, + tt.args.password, + gittypes.GitCredentialAuthType_Basic, + false, + false, + tt.extensions, + false, + ) if tt.expect.shouldFail { assert.Error(t, err) if tt.expect.err != nil { @@ -270,8 +310,28 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) { username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond) - go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, false, []string{}, false) - service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, false, []string{}, false) + go service.ListFiles( + privateAzureRepoURL, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) + service.ListFiles( + privateAzureRepoURL, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) time.Sleep(2 * time.Second) } diff --git a/api/git/backup.go b/api/git/backup.go index 286b51876..6928f521a 100644 --- a/api/git/backup.go +++ b/api/git/backup.go @@ -19,6 +19,7 @@ type CloneOptions struct { ReferenceName string Username string Password string + AuthType gittypes.GitCredentialAuthType // TLSSkipVerify skips SSL verification when cloning the Git repository TLSSkipVerify bool `example:"false"` } @@ -42,7 +43,15 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File cleanUp = true - if err := gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify); err != nil { + if err := gitService.CloneRepository( + options.ProjectPath, + options.URL, + options.ReferenceName, + options.Username, + options.Password, + options.AuthType, + options.TLSSkipVerify, + ); err != nil { cleanUp = false if err := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false); err != nil { log.Warn().Err(err).Msg("failed restoring backup folder") diff --git a/api/git/git.go b/api/git/git.go index 6c2835815..cf0c9f478 100644 --- a/api/git/git.go +++ b/api/git/git.go @@ -7,12 +7,14 @@ import ( "strings" gittypes "github.com/portainer/portainer/api/git/types" + "github.com/rs/zerolog/log" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage/memory" "github.com/pkg/errors" @@ -33,7 +35,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e URL: opt.repositoryUrl, Depth: opt.depth, InsecureSkipTLS: opt.tlsSkipVerify, - Auth: getAuth(opt.username, opt.password), + Auth: getAuth(opt.authType, opt.username, opt.password), Tags: git.NoTags, } @@ -51,7 +53,10 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e } if !c.preserveGitDirectory { - os.RemoveAll(filepath.Join(dst, ".git")) + err := os.RemoveAll(filepath.Join(dst, ".git")) + if err != nil { + log.Error().Err(err).Msg("failed to remove .git directory") + } } return nil @@ -64,7 +69,7 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string }) listOptions := &git.ListOptions{ - Auth: getAuth(opt.username, opt.password), + Auth: getAuth(opt.authType, opt.username, opt.password), InsecureSkipTLS: opt.tlsSkipVerify, } @@ -94,7 +99,23 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string return "", errors.Errorf("could not find ref %q in the repository", opt.referenceName) } -func getAuth(username, password string) *githttp.BasicAuth { +func getAuth(authType gittypes.GitCredentialAuthType, username, password string) transport.AuthMethod { + if password == "" { + return nil + } + + switch authType { + case gittypes.GitCredentialAuthType_Basic: + return getBasicAuth(username, password) + case gittypes.GitCredentialAuthType_Token: + return getTokenAuth(password) + default: + log.Warn().Msg("unknown git credentials authorization type, defaulting to None") + return nil + } +} + +func getBasicAuth(username, password string) *githttp.BasicAuth { if password != "" { if username == "" { username = "token" @@ -108,6 +129,15 @@ func getAuth(username, password string) *githttp.BasicAuth { return nil } +func getTokenAuth(token string) *githttp.TokenAuth { + if token != "" { + return &githttp.TokenAuth{ + Token: token, + } + } + return nil +} + func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, error) { rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ Name: "origin", @@ -115,7 +145,7 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err }) listOptions := &git.ListOptions{ - Auth: getAuth(opt.username, opt.password), + Auth: getAuth(opt.authType, opt.username, opt.password), InsecureSkipTLS: opt.tlsSkipVerify, } @@ -143,7 +173,7 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e Depth: 1, SingleBranch: true, ReferenceName: plumbing.ReferenceName(opt.referenceName), - Auth: getAuth(opt.username, opt.password), + Auth: getAuth(opt.authType, opt.username, opt.password), InsecureSkipTLS: opt.tlsSkipVerify, Tags: git.NoTags, } diff --git a/api/git/git_integration_test.go b/api/git/git_integration_test.go index add10afd6..6cb10253a 100644 --- a/api/git/git_integration_test.go +++ b/api/git/git_integration_test.go @@ -2,6 +2,8 @@ package git import ( "context" + "net/http" + "net/http/httptest" "path/filepath" "testing" "time" @@ -24,7 +26,15 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) { dst := t.TempDir() repositoryUrl := privateGitRepoURL - err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken, false) + err := service.CloneRepository( + dst, + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + ) assert.NoError(t, err) assert.FileExists(t, filepath.Join(dst, "README.md")) } @@ -37,7 +47,14 @@ func TestService_LatestCommitID_GitHub(t *testing.T) { service := newService(context.TODO(), 0, 0) repositoryUrl := privateGitRepoURL - id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken, false) + id, err := service.LatestCommitID( + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + ) assert.NoError(t, err) assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty") } @@ -50,7 +67,7 @@ func TestService_ListRefs_GitHub(t *testing.T) { service := newService(context.TODO(), 0, 0) repositoryUrl := privateGitRepoURL - refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false) + refs, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) assert.NoError(t, err) assert.GreaterOrEqual(t, len(refs), 1) } @@ -63,8 +80,8 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) { service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond) repositoryUrl := privateGitRepoURL - go service.ListRefs(repositoryUrl, username, accessToken, false, false) - service.ListRefs(repositoryUrl, username, accessToken, false, false) + go service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) + service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) time.Sleep(2 * time.Second) } @@ -202,7 +219,17 @@ func TestService_ListFiles_GitHub(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, false, tt.extensions, false) + paths, err := service.ListFiles( + tt.args.repositoryUrl, + tt.args.referenceName, + tt.args.username, + tt.args.password, + gittypes.GitCredentialAuthType_Basic, + false, + false, + tt.extensions, + false, + ) if tt.expect.shouldFail { assert.Error(t, err) if tt.expect.err != nil { @@ -226,8 +253,28 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) { username := getRequiredValue(t, "GITHUB_USERNAME") service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond) - go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) - service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) + go service.ListFiles( + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) + service.ListFiles( + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) time.Sleep(2 * time.Second) } @@ -240,8 +287,18 @@ func TestService_purgeCache_Github(t *testing.T) { username := getRequiredValue(t, "GITHUB_USERNAME") service := NewService(context.TODO()) - service.ListRefs(repositoryUrl, username, accessToken, false, false) - service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) + service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) + service.ListFiles( + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoFileCache.Len()) @@ -261,8 +318,18 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) { // 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result service := newService(context.TODO(), 2, 40*timeout) - service.ListRefs(repositoryUrl, username, accessToken, false, false) - service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) + service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) + service.ListFiles( + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoFileCache.Len()) @@ -293,12 +360,12 @@ func TestService_HardRefresh_ListRefs_GitHub(t *testing.T) { service := newService(context.TODO(), 2, 0) repositoryUrl := privateGitRepoURL - refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false) + refs, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) assert.NoError(t, err) assert.GreaterOrEqual(t, len(refs), 1) assert.Equal(t, 1, service.repoRefCache.Len()) - _, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false) + _, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, false, false) assert.Error(t, err) assert.Equal(t, 1, service.repoRefCache.Len()) } @@ -311,26 +378,46 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) { service := newService(context.TODO(), 2, 0) repositoryUrl := privateGitRepoURL - refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false) + refs, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false) assert.NoError(t, err) assert.GreaterOrEqual(t, len(refs), 1) assert.Equal(t, 1, service.repoRefCache.Len()) - files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) + files, err := service.ListFiles( + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) assert.NoError(t, err) assert.GreaterOrEqual(t, len(files), 1) assert.Equal(t, 1, service.repoFileCache.Len()) - files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, false, []string{}, false) + files, err = service.ListFiles( + repositoryUrl, + "refs/heads/test", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) assert.NoError(t, err) assert.GreaterOrEqual(t, len(files), 1) assert.Equal(t, 2, service.repoFileCache.Len()) - _, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false) + _, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, false, false) assert.Error(t, err) assert.Equal(t, 1, service.repoRefCache.Len()) - _, err = service.ListRefs(repositoryUrl, username, "fake-token", true, false) + _, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, true, false) assert.Error(t, err) assert.Equal(t, 1, service.repoRefCache.Len()) // The relevant file caches should be removed too @@ -344,12 +431,72 @@ func TestService_HardRefresh_ListFiles_GitHub(t *testing.T) { accessToken := getRequiredValue(t, "GITHUB_PAT") username := getRequiredValue(t, "GITHUB_USERNAME") repositoryUrl := privateGitRepoURL - files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) + files, err := service.ListFiles( + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{}, + false, + ) assert.NoError(t, err) assert.GreaterOrEqual(t, len(files), 1) assert.Equal(t, 1, service.repoFileCache.Len()) - _, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", false, true, []string{}, false) + _, err = service.ListFiles( + repositoryUrl, + "refs/heads/main", + username, + "fake-token", + gittypes.GitCredentialAuthType_Basic, + false, + true, + []string{}, + false, + ) assert.Error(t, err) assert.Equal(t, 0, service.repoFileCache.Len()) } + +func TestService_CloneRepository_TokenAuth(t *testing.T) { + ensureIntegrationTest(t) + + service := newService(context.TODO(), 2, 0) + var requests []*http.Request + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests = append(requests, r) + })) + accessToken := "test_access_token" + username := "test_username" + repositoryUrl := testServer.URL + + // Since we aren't hitting a real git server we ignore the error + _ = service.CloneRepository( + "test_dir", + repositoryUrl, + "refs/heads/main", + username, + accessToken, + gittypes.GitCredentialAuthType_Token, + false, + ) + + testServer.Close() + + if len(requests) != 1 { + t.Fatalf("expected 1 request sent but got %d", len(requests)) + } + + gotAuthHeader := requests[0].Header.Get("Authorization") + if gotAuthHeader == "" { + t.Fatal("no Authorization header in git request") + } + + expectedAuthHeader := "Bearer test_access_token" + if gotAuthHeader != expectedAuthHeader { + t.Fatalf("expected Authorization header %q but got %q", expectedAuthHeader, gotAuthHeader) + } +} diff --git a/api/git/git_test.go b/api/git/git_test.go index 81efa2688..fc0db196d 100644 --- a/api/git/git_test.go +++ b/api/git/git_test.go @@ -38,7 +38,7 @@ func Test_ClonePublicRepository_Shallow(t *testing.T) { dir := t.TempDir() t.Logf("Cloning into %s", dir) - err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false) + err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false) assert.NoError(t, err) assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth") } @@ -50,7 +50,7 @@ func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) { dir := t.TempDir() t.Logf("Cloning into %s", dir) - err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false) + err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false) assert.NoError(t, err) assert.NoDirExists(t, filepath.Join(dir, ".git")) } @@ -84,7 +84,7 @@ func Test_latestCommitID(t *testing.T) { repositoryURL := setup(t) referenceName := "refs/heads/main" - id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", false) + id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false) assert.NoError(t, err) assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id) @@ -95,7 +95,7 @@ func Test_ListRefs(t *testing.T) { repositoryURL := setup(t) - fs, err := service.ListRefs(repositoryURL, "", "", false, false) + fs, err := service.ListRefs(repositoryURL, "", "", gittypes.GitCredentialAuthType_Basic, false, false) assert.NoError(t, err) assert.Equal(t, []string{"refs/heads/main"}, fs) @@ -107,7 +107,17 @@ func Test_ListFiles(t *testing.T) { repositoryURL := setup(t) referenceName := "refs/heads/main" - fs, err := service.ListFiles(repositoryURL, referenceName, "", "", false, false, []string{".yml"}, false) + fs, err := service.ListFiles( + repositoryURL, + referenceName, + "", + "", + gittypes.GitCredentialAuthType_Basic, + false, + false, + []string{".yml"}, + false, + ) assert.NoError(t, err) assert.Equal(t, []string{"docker-compose.yml"}, fs) @@ -255,7 +265,7 @@ func Test_listFilesPrivateRepository(t *testing.T) { name: "list tree with real repository and head ref but no credential", args: fetchOption{ baseOption: baseOption{ - repositoryUrl: privateGitRepoURL + "fake", + repositoryUrl: privateGitRepoURL, username: "", password: "", }, diff --git a/api/git/service.go b/api/git/service.go index 3e995eccd..834e0c827 100644 --- a/api/git/service.go +++ b/api/git/service.go @@ -8,6 +8,7 @@ import ( "time" lru "github.com/hashicorp/golang-lru" + gittypes "github.com/portainer/portainer/api/git/types" "github.com/rs/zerolog/log" "golang.org/x/sync/singleflight" ) @@ -22,6 +23,7 @@ type baseOption struct { repositoryUrl string username string password string + authType gittypes.GitCredentialAuthType tlsSkipVerify bool } @@ -123,13 +125,22 @@ func (service *Service) timerHasStopped() bool { // CloneRepository clones a git repository using the specified URL in the specified // destination folder. -func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error { +func (service *Service) CloneRepository( + destination, + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) error { options := cloneOption{ fetchOption: fetchOption{ baseOption: baseOption{ repositoryUrl: repositoryURL, username: username, password: password, + authType: authType, tlsSkipVerify: tlsSkipVerify, }, referenceName: referenceName, @@ -155,12 +166,20 @@ func (service *Service) cloneRepository(destination string, options cloneOption) } // LatestCommitID returns SHA1 of the latest commit of the specified reference -func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) { +func (service *Service) LatestCommitID( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) (string, error) { options := fetchOption{ baseOption: baseOption{ repositoryUrl: repositoryURL, username: username, password: password, + authType: authType, tlsSkipVerify: tlsSkipVerify, }, referenceName: referenceName, @@ -170,7 +189,14 @@ func (service *Service) LatestCommitID(repositoryURL, referenceName, username, p } // ListRefs will list target repository's references without cloning the repository -func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) { +func (service *Service) ListRefs( + repositoryURL, + username, + password string, + authType gittypes.GitCredentialAuthType, + hardRefresh bool, + tlsSkipVerify bool, +) ([]string, error) { refCacheKey := generateCacheKey(repositoryURL, username, password, strconv.FormatBool(tlsSkipVerify)) if service.cacheEnabled && hardRefresh { // Should remove the cache explicitly, so that the following normal list can show the correct result @@ -196,6 +222,7 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR repositoryUrl: repositoryURL, username: username, password: password, + authType: authType, tlsSkipVerify: tlsSkipVerify, } @@ -215,18 +242,62 @@ var singleflightGroup = &singleflight.Group{} // ListFiles will list all the files of the target repository with specific extensions. // If extension is not provided, it will list all the files under the target repository -func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) { - repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly)) +func (service *Service) ListFiles( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + dirOnly, + hardRefresh bool, + includedExts []string, + tlsSkipVerify bool, +) ([]string, error) { + repoKey := generateCacheKey( + repositoryURL, + referenceName, + username, + password, + strconv.FormatBool(tlsSkipVerify), + strconv.Itoa(int(authType)), + strconv.FormatBool(dirOnly), + ) fs, err, _ := singleflightGroup.Do(repoKey, func() (any, error) { - return service.listFiles(repositoryURL, referenceName, username, password, dirOnly, hardRefresh, tlsSkipVerify) + return service.listFiles( + repositoryURL, + referenceName, + username, + password, + authType, + dirOnly, + hardRefresh, + tlsSkipVerify, + ) }) return filterFiles(fs.([]string), includedExts), err } -func (service *Service) listFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, tlsSkipVerify bool) ([]string, error) { - repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly)) +func (service *Service) listFiles( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + dirOnly, + hardRefresh bool, + tlsSkipVerify bool, +) ([]string, error) { + repoKey := generateCacheKey( + repositoryURL, + referenceName, + username, + password, + strconv.FormatBool(tlsSkipVerify), + strconv.Itoa(int(authType)), + strconv.FormatBool(dirOnly), + ) if service.cacheEnabled && hardRefresh { // Should remove the cache explicitly, so that the following normal list can show the correct result @@ -247,6 +318,7 @@ func (service *Service) listFiles(repositoryURL, referenceName, username, passwo repositoryUrl: repositoryURL, username: username, password: password, + authType: authType, tlsSkipVerify: tlsSkipVerify, }, referenceName: referenceName, diff --git a/api/git/types/types.go b/api/git/types/types.go index 12d95e093..cb9d7cf03 100644 --- a/api/git/types/types.go +++ b/api/git/types/types.go @@ -1,12 +1,21 @@ package gittypes -import "errors" +import ( + "errors" +) var ( ErrIncorrectRepositoryURL = errors.New("git repository could not be found, please ensure that the URL is correct") ErrAuthenticationFailure = errors.New("authentication failed, please ensure that the git credentials are correct") ) +type GitCredentialAuthType int + +const ( + GitCredentialAuthType_Basic GitCredentialAuthType = iota + GitCredentialAuthType_Token +) + // RepoConfig represents a configuration for a repo type RepoConfig struct { // The repo url @@ -24,10 +33,11 @@ type RepoConfig struct { } type GitAuthentication struct { - Username string - Password string + Username string + Password string + AuthorizationType GitCredentialAuthType // Git credentials identifier when the value is not 0 - // When the value is 0, Username and Password are set without using saved credential + // When the value is 0, Username, Password, and Authtype are set without using saved credential // This is introduced since 2.15.0 GitCredentialID int `example:"0"` } diff --git a/api/git/update/update.go b/api/git/update/update.go index 203e361dd..780d6e046 100644 --- a/api/git/update/update.go +++ b/api/git/update/update.go @@ -29,7 +29,14 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g return false, "", errors.WithMessagef(err, "failed to get credentials for %v", objId) } - newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password, gitConfig.TLSSkipVerify) + newHash, err := gitService.LatestCommitID( + gitConfig.URL, + gitConfig.ReferenceName, + username, + password, + gittypes.GitCredentialAuthType_Basic, + gitConfig.TLSSkipVerify, + ) if err != nil { return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId) } @@ -62,6 +69,7 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g cloneParams.auth = &gitAuth{ username: username, password: password, + authType: gitConfig.Authentication.AuthorizationType, } } @@ -89,14 +97,31 @@ type cloneRepositoryParameters struct { } type gitAuth struct { + authType gittypes.GitCredentialAuthType username string password string } func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error { if cloneParams.auth != nil { - return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password, cloneParams.tlsSkipVerify) + return gitService.CloneRepository( + cloneParams.toDir, + cloneParams.url, + cloneParams.ref, + cloneParams.auth.username, + cloneParams.auth.password, + cloneParams.auth.authType, + cloneParams.tlsSkipVerify, + ) } - return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "", cloneParams.tlsSkipVerify) + return gitService.CloneRepository( + cloneParams.toDir, + cloneParams.url, + cloneParams.ref, + "", + "", + gittypes.GitCredentialAuthType_Basic, + cloneParams.tlsSkipVerify, + ) } diff --git a/api/http/handler/customtemplates/customtemplate_git_fetch_test.go b/api/http/handler/customtemplates/customtemplate_git_fetch_test.go index 60ed1666f..b63db356d 100644 --- a/api/http/handler/customtemplates/customtemplate_git_fetch_test.go +++ b/api/http/handler/customtemplates/customtemplate_git_fetch_test.go @@ -33,13 +33,28 @@ type TestGitService struct { targetFilePath string } -func (g *TestGitService) CloneRepository(destination string, repositoryURL, referenceName string, username, password string, tlsSkipVerify bool) error { +func (g *TestGitService) CloneRepository( + destination string, + repositoryURL, + referenceName string, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) error { time.Sleep(100 * time.Millisecond) return createTestFile(g.targetFilePath) } -func (g *TestGitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) { +func (g *TestGitService) LatestCommitID( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) (string, error) { return "", nil } @@ -56,11 +71,26 @@ type InvalidTestGitService struct { targetFilePath string } -func (g *InvalidTestGitService) CloneRepository(dest, repoUrl, refName, username, password string, tlsSkipVerify bool) error { +func (g *InvalidTestGitService) CloneRepository( + dest, + repoUrl, + refName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) error { return errors.New("simulate network error") } -func (g *InvalidTestGitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) { +func (g *InvalidTestGitService) LatestCommitID( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) (string, error) { return "", nil } diff --git a/api/http/handler/customtemplates/customtemplate_update.go b/api/http/handler/customtemplates/customtemplate_update.go index f14d228f3..f12eeb2e1 100644 --- a/api/http/handler/customtemplates/customtemplate_update.go +++ b/api/http/handler/customtemplates/customtemplate_update.go @@ -37,14 +37,16 @@ type customTemplateUpdatePayload struct { RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"` // Reference name of a Git repository hosting the Stack file RepositoryReferenceName string `example:"refs/heads/master"` - // Use basic authentication to clone the Git repository + // Use authentication to clone the Git repository RepositoryAuthentication bool `example:"true"` // Username used in basic authentication. Required when RepositoryAuthentication is true - // and RepositoryGitCredentialID is 0 + // and RepositoryGitCredentialID is 0. Ignored if RepositoryAuthType is token RepositoryUsername string `example:"myGitUsername"` - // Password used in basic authentication. Required when RepositoryAuthentication is true - // and RepositoryGitCredentialID is 0 + // Password used in basic authentication or token used in token authentication. + // Required when RepositoryAuthentication is true and RepositoryGitCredentialID is 0 RepositoryPassword string `example:"myGitPassword"` + // RepositoryAuthorizationType is the authorization type to use + RepositoryAuthorizationType gittypes.GitCredentialAuthType `example:"0"` // GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication // is true and RepositoryUsername/RepositoryPassword are not provided RepositoryGitCredentialID int `example:"0"` @@ -182,12 +184,15 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ repositoryUsername := "" repositoryPassword := "" + repositoryAuthType := gittypes.GitCredentialAuthType_Basic if payload.RepositoryAuthentication { repositoryUsername = payload.RepositoryUsername repositoryPassword = payload.RepositoryPassword + repositoryAuthType = payload.RepositoryAuthorizationType gitConfig.Authentication = &gittypes.GitAuthentication{ - Username: payload.RepositoryUsername, - Password: payload.RepositoryPassword, + Username: payload.RepositoryUsername, + Password: payload.RepositoryPassword, + AuthorizationType: payload.RepositoryAuthorizationType, } } @@ -197,6 +202,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ ReferenceName: gitConfig.ReferenceName, Username: repositoryUsername, Password: repositoryPassword, + AuthType: repositoryAuthType, TLSSkipVerify: gitConfig.TLSSkipVerify, }) if err != nil { @@ -205,7 +211,14 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ defer cleanBackup() - commitHash, err := handler.GitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, repositoryUsername, repositoryPassword, gitConfig.TLSSkipVerify) + commitHash, err := handler.GitService.LatestCommitID( + gitConfig.URL, + gitConfig.ReferenceName, + repositoryUsername, + repositoryPassword, + repositoryAuthType, + gitConfig.TLSSkipVerify, + ) if err != nil { return httperror.InternalServerError("Unable get latest commit id", fmt.Errorf("failed to fetch latest commit id of the template %v: %w", customTemplate.ID, err)) } diff --git a/api/http/handler/edgestacks/edgestack_create_git.go b/api/http/handler/edgestacks/edgestack_create_git.go index d20e5b5c2..a8775495d 100644 --- a/api/http/handler/edgestacks/edgestack_create_git.go +++ b/api/http/handler/edgestacks/edgestack_create_git.go @@ -33,6 +33,8 @@ type edgeStackFromGitRepositoryPayload struct { RepositoryUsername string `example:"myGitUsername"` // Password used in basic authentication. Required when RepositoryAuthentication is true. RepositoryPassword string `example:"myGitPassword"` + // RepositoryAuthorizationType is the authorization type to use + RepositoryAuthorizationType gittypes.GitCredentialAuthType `example:"0"` // Path to the Stack file inside the Git repository FilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` // List of identifiers of EdgeGroups @@ -125,8 +127,9 @@ func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dat if payload.RepositoryAuthentication { repoConfig.Authentication = &gittypes.GitAuthentication{ - Username: payload.RepositoryUsername, - Password: payload.RepositoryPassword, + Username: payload.RepositoryUsername, + Password: payload.RepositoryPassword, + AuthorizationType: payload.RepositoryAuthorizationType, } } @@ -145,12 +148,22 @@ func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStore projectPath = handler.FileService.GetEdgeStackProjectPath(stackFolder) repositoryUsername := "" repositoryPassword := "" + repositoryAuthType := gittypes.GitCredentialAuthType_Basic if repositoryConfig.Authentication != nil && repositoryConfig.Authentication.Password != "" { repositoryUsername = repositoryConfig.Authentication.Username repositoryPassword = repositoryConfig.Authentication.Password + repositoryAuthType = repositoryConfig.Authentication.AuthorizationType } - if err := handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify); err != nil { + if err := handler.GitService.CloneRepository( + projectPath, + repositoryConfig.URL, + repositoryConfig.ReferenceName, + repositoryUsername, + repositoryPassword, + repositoryAuthType, + repositoryConfig.TLSSkipVerify, + ); err != nil { return "", "", "", err } diff --git a/api/http/handler/gitops/git_repo_file_preview.go b/api/http/handler/gitops/git_repo_file_preview.go index 1eaa52716..43c08c870 100644 --- a/api/http/handler/gitops/git_repo_file_preview.go +++ b/api/http/handler/gitops/git_repo_file_preview.go @@ -17,10 +17,11 @@ type fileResponse struct { } type repositoryFilePreviewPayload struct { - Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"` - Reference string `json:"reference" example:"refs/heads/master"` - Username string `json:"username" example:"myGitUsername"` - Password string `json:"password" example:"myGitPassword"` + Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"` + Reference string `json:"reference" example:"refs/heads/master"` + Username string `json:"username" example:"myGitUsername"` + Password string `json:"password" example:"myGitPassword"` + AuthorizationType gittypes.GitCredentialAuthType `json:"authorizationType"` // Path to file whose content will be read TargetFile string `json:"targetFile" example:"docker-compose.yml"` // TLSSkipVerify skips SSL verification when cloning the Git repository @@ -68,7 +69,15 @@ func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *ht return httperror.InternalServerError("Unable to create temporary folder", err) } - err = handler.gitService.CloneRepository(projectPath, payload.Repository, payload.Reference, payload.Username, payload.Password, payload.TLSSkipVerify) + err = handler.gitService.CloneRepository( + projectPath, + payload.Repository, + payload.Reference, + payload.Username, + payload.Password, + payload.AuthorizationType, + payload.TLSSkipVerify, + ) if err != nil { if errors.Is(err, gittypes.ErrAuthenticationFailure) { return httperror.BadRequest("Invalid git credential", err) diff --git a/api/http/handler/stacks/stack_update_git.go b/api/http/handler/stacks/stack_update_git.go index 8d0687694..2bdf2b71f 100644 --- a/api/http/handler/stacks/stack_update_git.go +++ b/api/http/handler/stacks/stack_update_git.go @@ -19,14 +19,15 @@ import ( ) type stackGitUpdatePayload struct { - AutoUpdate *portainer.AutoUpdateSettings - Env []portainer.Pair - Prune bool - RepositoryReferenceName string - RepositoryAuthentication bool - RepositoryUsername string - RepositoryPassword string - TLSSkipVerify bool + AutoUpdate *portainer.AutoUpdateSettings + Env []portainer.Pair + Prune bool + RepositoryReferenceName string + RepositoryAuthentication bool + RepositoryUsername string + RepositoryPassword string + RepositoryAuthorizationType gittypes.GitCredentialAuthType + TLSSkipVerify bool } func (payload *stackGitUpdatePayload) Validate(r *http.Request) error { @@ -151,11 +152,19 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) * } stack.GitConfig.Authentication = &gittypes.GitAuthentication{ - Username: payload.RepositoryUsername, - Password: password, + Username: payload.RepositoryUsername, + Password: password, + AuthorizationType: payload.RepositoryAuthorizationType, } - if _, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify); err != nil { + if _, err := handler.GitService.LatestCommitID( + stack.GitConfig.URL, + stack.GitConfig.ReferenceName, + stack.GitConfig.Authentication.Username, + stack.GitConfig.Authentication.Password, + stack.GitConfig.Authentication.AuthorizationType, + stack.GitConfig.TLSSkipVerify, + ); err != nil { return httperror.InternalServerError("Unable to fetch git repository", err) } } else { diff --git a/api/http/handler/stacks/stack_update_git_redeploy.go b/api/http/handler/stacks/stack_update_git_redeploy.go index e65e1e70c..c595808aa 100644 --- a/api/http/handler/stacks/stack_update_git_redeploy.go +++ b/api/http/handler/stacks/stack_update_git_redeploy.go @@ -6,6 +6,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/git" + gittypes "github.com/portainer/portainer/api/git/types" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" k "github.com/portainer/portainer/api/kubernetes" @@ -19,12 +20,13 @@ import ( ) type stackGitRedployPayload struct { - RepositoryReferenceName string - RepositoryAuthentication bool - RepositoryUsername string - RepositoryPassword string - Env []portainer.Pair - Prune bool + RepositoryReferenceName string + RepositoryAuthentication bool + RepositoryUsername string + RepositoryPassword string + RepositoryAuthorizationType gittypes.GitCredentialAuthType + Env []portainer.Pair + Prune bool // Force a pulling to current image with the original tag though the image is already the latest PullImage bool `example:"false"` @@ -135,13 +137,16 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) repositoryUsername := "" repositoryPassword := "" + repositoryAuthType := gittypes.GitCredentialAuthType_Basic if payload.RepositoryAuthentication { repositoryPassword = payload.RepositoryPassword + repositoryAuthType = payload.RepositoryAuthorizationType // When the existing stack is using the custom username/password and the password is not updated, // the stack should keep using the saved username/password if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil { repositoryPassword = stack.GitConfig.Authentication.Password + repositoryAuthType = stack.GitConfig.Authentication.AuthorizationType } repositoryUsername = payload.RepositoryUsername } @@ -152,6 +157,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) ReferenceName: stack.GitConfig.ReferenceName, Username: repositoryUsername, Password: repositoryPassword, + AuthType: repositoryAuthType, TLSSkipVerify: stack.GitConfig.TLSSkipVerify, } @@ -166,7 +172,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) return err } - newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, stack.GitConfig.TLSSkipVerify) + newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryAuthType, stack.GitConfig.TLSSkipVerify) if err != nil { return httperror.InternalServerError("Unable get latest commit id", errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID)) } diff --git a/api/http/handler/stacks/update_kubernetes_stack.go b/api/http/handler/stacks/update_kubernetes_stack.go index 95195bb10..42ecbaa04 100644 --- a/api/http/handler/stacks/update_kubernetes_stack.go +++ b/api/http/handler/stacks/update_kubernetes_stack.go @@ -27,12 +27,13 @@ type kubernetesFileStackUpdatePayload struct { } type kubernetesGitStackUpdatePayload struct { - RepositoryReferenceName string - RepositoryAuthentication bool - RepositoryUsername string - RepositoryPassword string - AutoUpdate *portainer.AutoUpdateSettings - TLSSkipVerify bool + RepositoryReferenceName string + RepositoryAuthentication bool + RepositoryUsername string + RepositoryPassword string + RepositoryAuthorizationType gittypes.GitCredentialAuthType + AutoUpdate *portainer.AutoUpdateSettings + TLSSkipVerify bool } func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error { @@ -76,11 +77,19 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer. } stack.GitConfig.Authentication = &gittypes.GitAuthentication{ - Username: payload.RepositoryUsername, - Password: password, + Username: payload.RepositoryUsername, + Password: password, + AuthorizationType: payload.RepositoryAuthorizationType, } - if _, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify); err != nil { + if _, err := handler.GitService.LatestCommitID( + stack.GitConfig.URL, + stack.GitConfig.ReferenceName, + stack.GitConfig.Authentication.Username, + stack.GitConfig.Authentication.Password, + stack.GitConfig.Authentication.AuthorizationType, + stack.GitConfig.TLSSkipVerify, + ); err != nil { return httperror.InternalServerError("Unable to fetch git repository", err) } } diff --git a/api/http/handler/templates/template_file.go b/api/http/handler/templates/template_file.go index b834eeed9..f9ec0135c 100644 --- a/api/http/handler/templates/template_file.go +++ b/api/http/handler/templates/template_file.go @@ -5,6 +5,7 @@ import ( "slices" portainer "github.com/portainer/portainer/api" + gittypes "github.com/portainer/portainer/api/git/types" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/response" @@ -71,7 +72,15 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht defer handler.cleanUp(projectPath) - if err := handler.GitService.CloneRepository(projectPath, template.Repository.URL, "", "", "", false); err != nil { + if err := handler.GitService.CloneRepository( + projectPath, + template.Repository.URL, + "", + "", + "", + gittypes.GitCredentialAuthType_Basic, + false, + ); err != nil { return httperror.InternalServerError("Unable to clone git repository", err) } diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 49f1cd501..dae72ecc1 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -15,6 +15,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" + gittypes "github.com/portainer/portainer/api/git/types" "github.com/portainer/portainer/api/http/proxy/factory/utils" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -418,7 +419,14 @@ func (transport *Transport) updateDefaultGitBranch(request *http.Request) error } repositoryURL := remote[:len(remote)-4] - latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "", false) + latestCommitID, err := transport.gitService.LatestCommitID( + repositoryURL, + "", + "", + "", + gittypes.GitCredentialAuthType_Basic, + false, + ) if err != nil { return err } diff --git a/api/internal/testhelpers/git_service.go b/api/internal/testhelpers/git_service.go index 6af1b6459..6b1a352ee 100644 --- a/api/internal/testhelpers/git_service.go +++ b/api/internal/testhelpers/git_service.go @@ -1,6 +1,9 @@ package testhelpers -import portainer "github.com/portainer/portainer/api" +import ( + portainer "github.com/portainer/portainer/api" + gittypes "github.com/portainer/portainer/api/git/types" +) type gitService struct { cloneErr error @@ -15,18 +18,50 @@ func NewGitService(cloneErr error, id string) portainer.GitService { } } -func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error { +func (g *gitService) CloneRepository( + destination, + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) error { return g.cloneErr } -func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) { +func (g *gitService) LatestCommitID( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, +) (string, error) { return g.id, nil } -func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) { +func (g *gitService) ListRefs( + repositoryURL, + username, + password string, + authType gittypes.GitCredentialAuthType, + hardRefresh bool, + tlsSkipVerify bool, +) ([]string, error) { return nil, nil } -func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) { +func (g *gitService) ListFiles( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + dirOnly, + hardRefresh bool, + includedExts []string, + tlsSkipVerify bool, +) ([]string, error) { return nil, nil } diff --git a/api/portainer.go b/api/portainer.go index 8172f08fa..7c72f9b01 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1538,10 +1538,42 @@ type ( // GitService represents a service for managing Git GitService interface { - CloneRepository(destination string, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error - LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) - ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) - ListFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, includeExts []string, tlsSkipVerify bool) ([]string, error) + CloneRepository( + destination string, + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, + ) error + LatestCommitID( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + tlsSkipVerify bool, + ) (string, error) + ListRefs( + repositoryURL, + username, + password string, + authType gittypes.GitCredentialAuthType, + hardRefresh bool, + tlsSkipVerify bool, + ) ([]string, error) + ListFiles( + repositoryURL, + referenceName, + username, + password string, + authType gittypes.GitCredentialAuthType, + dirOnly, + hardRefresh bool, + includeExts []string, + tlsSkipVerify bool, + ) ([]string, error) } // OpenAMTService represents a service for managing OpenAMT diff --git a/api/stacks/stackutils/gitops.go b/api/stacks/stackutils/gitops.go index d035b783d..566a2a2e2 100644 --- a/api/stacks/stackutils/gitops.go +++ b/api/stacks/stackutils/gitops.go @@ -19,13 +19,23 @@ var ( func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitService, getProjectPath func() string) (string, error) { username := "" password := "" + authType := gittypes.GitCredentialAuthType_Basic if config.Authentication != nil { username = config.Authentication.Username password = config.Authentication.Password + authType = config.Authentication.AuthorizationType } projectPath := getProjectPath() - err := gitService.CloneRepository(projectPath, config.URL, config.ReferenceName, username, password, config.TLSSkipVerify) + err := gitService.CloneRepository( + projectPath, + config.URL, + config.ReferenceName, + username, + password, + authType, + config.TLSSkipVerify, + ) if err != nil { if errors.Is(err, gittypes.ErrAuthenticationFailure) { newErr := git.ErrInvalidGitCredential @@ -36,7 +46,14 @@ func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitS return "", newErr } - commitID, err := gitService.LatestCommitID(config.URL, config.ReferenceName, username, password, config.TLSSkipVerify) + commitID, err := gitService.LatestCommitID( + config.URL, + config.ReferenceName, + username, + password, + authType, + config.TLSSkipVerify, + ) if err != nil { newErr := fmt.Errorf("unable to fetch git repository id: %w", err) return "", newErr