1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 15:29:42 +02:00

feat(git): support bearer token auth for git [BE-11770] (#879)

This commit is contained in:
Devon Steenberg 2025-07-22 08:36:08 +12:00 committed by GitHub
parent 55cc250d2e
commit caf382b64c
20 changed files with 670 additions and 117 deletions

View file

@ -58,7 +58,15 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
dst := t.TempDir() dst := t.TempDir()
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password) 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.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md")) assert.FileExists(t, filepath.Join(dst, "README.md"))
}) })
@ -73,7 +81,15 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
dst := t.TempDir() 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.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md")) 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") pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService(context.TODO()) 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.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty") 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") username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := NewService(context.TODO()) 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.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) assert.GreaterOrEqual(t, len(refs), 1)
} }
@ -108,8 +138,8 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond) service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false) go service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, false, false) service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
@ -247,7 +277,17 @@ func TestService_ListFiles_Azure(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if tt.expect.shouldFail {
assert.Error(t, err) assert.Error(t, err)
if tt.expect.err != nil { if tt.expect.err != nil {
@ -270,8 +310,28 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond) service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, false, []string{}, false) go service.ListFiles(
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, false, []string{}, false) 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) time.Sleep(2 * time.Second)
} }

View file

@ -19,6 +19,7 @@ type CloneOptions struct {
ReferenceName string ReferenceName string
Username string Username string
Password string Password string
AuthType gittypes.GitCredentialAuthType
// TLSSkipVerify skips SSL verification when cloning the Git repository // TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"` TLSSkipVerify bool `example:"false"`
} }
@ -42,7 +43,15 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
cleanUp = true 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 cleanUp = false
if err := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false); err != nil { if err := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false); err != nil {
log.Warn().Err(err).Msg("failed restoring backup folder") log.Warn().Err(err).Msg("failed restoring backup folder")

View file

@ -7,12 +7,14 @@ import (
"strings" "strings"
gittypes "github.com/portainer/portainer/api/git/types" 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"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "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/filemode"
"github.com/go-git/go-git/v5/plumbing/object" "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" githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory" "github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -33,7 +35,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
URL: opt.repositoryUrl, URL: opt.repositoryUrl,
Depth: opt.depth, Depth: opt.depth,
InsecureSkipTLS: opt.tlsSkipVerify, InsecureSkipTLS: opt.tlsSkipVerify,
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.authType, opt.username, opt.password),
Tags: git.NoTags, Tags: git.NoTags,
} }
@ -51,7 +53,10 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
} }
if !c.preserveGitDirectory { 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 return nil
@ -64,7 +69,7 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
}) })
listOptions := &git.ListOptions{ listOptions := &git.ListOptions{
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.authType, opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify, 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) 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 password != "" {
if username == "" { if username == "" {
username = "token" username = "token"
@ -108,6 +129,15 @@ func getAuth(username, password string) *githttp.BasicAuth {
return nil 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) { func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, error) {
rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: "origin", Name: "origin",
@ -115,7 +145,7 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
}) })
listOptions := &git.ListOptions{ listOptions := &git.ListOptions{
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.authType, opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify, InsecureSkipTLS: opt.tlsSkipVerify,
} }
@ -143,7 +173,7 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
Depth: 1, Depth: 1,
SingleBranch: true, SingleBranch: true,
ReferenceName: plumbing.ReferenceName(opt.referenceName), ReferenceName: plumbing.ReferenceName(opt.referenceName),
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.authType, opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify, InsecureSkipTLS: opt.tlsSkipVerify,
Tags: git.NoTags, Tags: git.NoTags,
} }

View file

@ -2,6 +2,8 @@ package git
import ( import (
"context" "context"
"net/http"
"net/http/httptest"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
@ -24,7 +26,15 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
dst := t.TempDir() dst := t.TempDir()
repositoryUrl := privateGitRepoURL 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.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md")) 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) service := newService(context.TODO(), 0, 0)
repositoryUrl := privateGitRepoURL 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.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty") 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) service := newService(context.TODO(), 0, 0)
repositoryUrl := privateGitRepoURL 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.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) 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) service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
go service.ListRefs(repositoryUrl, username, accessToken, false, false) go service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListRefs(repositoryUrl, username, accessToken, false, false) service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
@ -202,7 +219,17 @@ func TestService_ListFiles_GitHub(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if tt.expect.shouldFail {
assert.Error(t, err) assert.Error(t, err)
if tt.expect.err != nil { if tt.expect.err != nil {
@ -226,8 +253,28 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME") username := getRequiredValue(t, "GITHUB_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond) service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) go service.ListFiles(
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false) 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) time.Sleep(2 * time.Second)
} }
@ -240,8 +287,18 @@ func TestService_purgeCache_Github(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME") username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService(context.TODO()) service := NewService(context.TODO())
service.ListRefs(repositoryUrl, username, accessToken, false, false) service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, 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.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.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 // 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result
service := newService(context.TODO(), 2, 40*timeout) service := newService(context.TODO(), 2, 40*timeout)
service.ListRefs(repositoryUrl, username, accessToken, false, false) service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, 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.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.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) service := newService(context.TODO(), 2, 0)
repositoryUrl := privateGitRepoURL 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.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) assert.GreaterOrEqual(t, len(refs), 1)
assert.Equal(t, 1, service.repoRefCache.Len()) 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.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len()) 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) service := newService(context.TODO(), 2, 0)
repositoryUrl := privateGitRepoURL 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.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) assert.GreaterOrEqual(t, len(refs), 1)
assert.Equal(t, 1, service.repoRefCache.Len()) 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.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1) assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 1, service.repoFileCache.Len()) 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.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1) assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 2, service.repoFileCache.Len()) 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.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len()) 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.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
// The relevant file caches should be removed too // 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") accessToken := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME") username := getRequiredValue(t, "GITHUB_USERNAME")
repositoryUrl := privateGitRepoURL 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.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1) assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 1, service.repoFileCache.Len()) 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.Error(t, err)
assert.Equal(t, 0, service.repoFileCache.Len()) 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)
}
}

View file

@ -38,7 +38,7 @@ func Test_ClonePublicRepository_Shallow(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
t.Logf("Cloning into %s", dir) 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.NoError(t, err)
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth") 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() dir := t.TempDir()
t.Logf("Cloning into %s", dir) 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.NoError(t, err)
assert.NoDirExists(t, filepath.Join(dir, ".git")) assert.NoDirExists(t, filepath.Join(dir, ".git"))
} }
@ -84,7 +84,7 @@ func Test_latestCommitID(t *testing.T) {
repositoryURL := setup(t) repositoryURL := setup(t)
referenceName := "refs/heads/main" 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.NoError(t, err)
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id) assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
@ -95,7 +95,7 @@ func Test_ListRefs(t *testing.T) {
repositoryURL := setup(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.NoError(t, err)
assert.Equal(t, []string{"refs/heads/main"}, fs) assert.Equal(t, []string{"refs/heads/main"}, fs)
@ -107,7 +107,17 @@ func Test_ListFiles(t *testing.T) {
repositoryURL := setup(t) repositoryURL := setup(t)
referenceName := "refs/heads/main" 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.NoError(t, err)
assert.Equal(t, []string{"docker-compose.yml"}, fs) 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", name: "list tree with real repository and head ref but no credential",
args: fetchOption{ args: fetchOption{
baseOption: baseOption{ baseOption: baseOption{
repositoryUrl: privateGitRepoURL + "fake", repositoryUrl: privateGitRepoURL,
username: "", username: "",
password: "", password: "",
}, },

View file

@ -8,6 +8,7 @@ import (
"time" "time"
lru "github.com/hashicorp/golang-lru" lru "github.com/hashicorp/golang-lru"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
@ -22,6 +23,7 @@ type baseOption struct {
repositoryUrl string repositoryUrl string
username string username string
password string password string
authType gittypes.GitCredentialAuthType
tlsSkipVerify bool tlsSkipVerify bool
} }
@ -123,13 +125,22 @@ func (service *Service) timerHasStopped() bool {
// CloneRepository clones a git repository using the specified URL in the specified // CloneRepository clones a git repository using the specified URL in the specified
// destination folder. // 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{ options := cloneOption{
fetchOption: fetchOption{ fetchOption: fetchOption{
baseOption: baseOption{ baseOption: baseOption{
repositoryUrl: repositoryURL, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify, tlsSkipVerify: tlsSkipVerify,
}, },
referenceName: referenceName, 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 // 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{ options := fetchOption{
baseOption: baseOption{ baseOption: baseOption{
repositoryUrl: repositoryURL, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify, tlsSkipVerify: tlsSkipVerify,
}, },
referenceName: referenceName, 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 // 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)) refCacheKey := generateCacheKey(repositoryURL, username, password, strconv.FormatBool(tlsSkipVerify))
if service.cacheEnabled && hardRefresh { if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result // 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, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify, tlsSkipVerify: tlsSkipVerify,
} }
@ -215,18 +242,62 @@ var singleflightGroup = &singleflight.Group{}
// ListFiles will list all the files of the target repository with specific extensions. // 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 // 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) { func (service *Service) ListFiles(
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly)) 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) { 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 return filterFiles(fs.([]string), includedExts), err
} }
func (service *Service) listFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, tlsSkipVerify bool) ([]string, error) { func (service *Service) listFiles(
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly)) 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 { if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result // 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, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify, tlsSkipVerify: tlsSkipVerify,
}, },
referenceName: referenceName, referenceName: referenceName,

View file

@ -1,12 +1,21 @@
package gittypes package gittypes
import "errors" import (
"errors"
)
var ( var (
ErrIncorrectRepositoryURL = errors.New("git repository could not be found, please ensure that the URL is correct") 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") 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 // RepoConfig represents a configuration for a repo
type RepoConfig struct { type RepoConfig struct {
// The repo url // The repo url
@ -24,10 +33,11 @@ type RepoConfig struct {
} }
type GitAuthentication struct { type GitAuthentication struct {
Username string Username string
Password string Password string
AuthorizationType GitCredentialAuthType
// Git credentials identifier when the value is not 0 // 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 // This is introduced since 2.15.0
GitCredentialID int `example:"0"` GitCredentialID int `example:"0"`
} }

View file

@ -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) 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 { if err != nil {
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId) 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{ cloneParams.auth = &gitAuth{
username: username, username: username,
password: password, password: password,
authType: gitConfig.Authentication.AuthorizationType,
} }
} }
@ -89,14 +97,31 @@ type cloneRepositoryParameters struct {
} }
type gitAuth struct { type gitAuth struct {
authType gittypes.GitCredentialAuthType
username string username string
password string password string
} }
func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error { func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error {
if cloneParams.auth != nil { 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,
)
} }

View file

@ -33,13 +33,28 @@ type TestGitService struct {
targetFilePath string 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) time.Sleep(100 * time.Millisecond)
return createTestFile(g.targetFilePath) 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 return "", nil
} }
@ -56,11 +71,26 @@ type InvalidTestGitService struct {
targetFilePath string 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") 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 return "", nil
} }

View file

@ -37,14 +37,16 @@ type customTemplateUpdatePayload struct {
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"` RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
// Reference name of a Git repository hosting the Stack file // Reference name of a Git repository hosting the Stack file
RepositoryReferenceName string `example:"refs/heads/master"` 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"` RepositoryAuthentication bool `example:"true"`
// Username used in basic authentication. Required when RepositoryAuthentication is 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"` RepositoryUsername string `example:"myGitUsername"`
// Password used in basic authentication. Required when RepositoryAuthentication is true // Password used in basic authentication or token used in token authentication.
// and RepositoryGitCredentialID is 0 // Required when RepositoryAuthentication is true and RepositoryGitCredentialID is 0
RepositoryPassword string `example:"myGitPassword"` 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 // GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication
// is true and RepositoryUsername/RepositoryPassword are not provided // is true and RepositoryUsername/RepositoryPassword are not provided
RepositoryGitCredentialID int `example:"0"` RepositoryGitCredentialID int `example:"0"`
@ -182,12 +184,15 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
repositoryUsername := "" repositoryUsername := ""
repositoryPassword := "" repositoryPassword := ""
repositoryAuthType := gittypes.GitCredentialAuthType_Basic
if payload.RepositoryAuthentication { if payload.RepositoryAuthentication {
repositoryUsername = payload.RepositoryUsername repositoryUsername = payload.RepositoryUsername
repositoryPassword = payload.RepositoryPassword repositoryPassword = payload.RepositoryPassword
repositoryAuthType = payload.RepositoryAuthorizationType
gitConfig.Authentication = &gittypes.GitAuthentication{ gitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername, Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword, Password: payload.RepositoryPassword,
AuthorizationType: payload.RepositoryAuthorizationType,
} }
} }
@ -197,6 +202,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
ReferenceName: gitConfig.ReferenceName, ReferenceName: gitConfig.ReferenceName,
Username: repositoryUsername, Username: repositoryUsername,
Password: repositoryPassword, Password: repositoryPassword,
AuthType: repositoryAuthType,
TLSSkipVerify: gitConfig.TLSSkipVerify, TLSSkipVerify: gitConfig.TLSSkipVerify,
}) })
if err != nil { if err != nil {
@ -205,7 +211,14 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
defer cleanBackup() 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 { 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)) return httperror.InternalServerError("Unable get latest commit id", fmt.Errorf("failed to fetch latest commit id of the template %v: %w", customTemplate.ID, err))
} }

View file

@ -33,6 +33,8 @@ type edgeStackFromGitRepositoryPayload struct {
RepositoryUsername string `example:"myGitUsername"` RepositoryUsername string `example:"myGitUsername"`
// Password used in basic authentication. Required when RepositoryAuthentication is true. // Password used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryPassword string `example:"myGitPassword"` 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 // Path to the Stack file inside the Git repository
FilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` FilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
// List of identifiers of EdgeGroups // List of identifiers of EdgeGroups
@ -125,8 +127,9 @@ func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dat
if payload.RepositoryAuthentication { if payload.RepositoryAuthentication {
repoConfig.Authentication = &gittypes.GitAuthentication{ repoConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername, Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword, Password: payload.RepositoryPassword,
AuthorizationType: payload.RepositoryAuthorizationType,
} }
} }
@ -145,12 +148,22 @@ func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStore
projectPath = handler.FileService.GetEdgeStackProjectPath(stackFolder) projectPath = handler.FileService.GetEdgeStackProjectPath(stackFolder)
repositoryUsername := "" repositoryUsername := ""
repositoryPassword := "" repositoryPassword := ""
repositoryAuthType := gittypes.GitCredentialAuthType_Basic
if repositoryConfig.Authentication != nil && repositoryConfig.Authentication.Password != "" { if repositoryConfig.Authentication != nil && repositoryConfig.Authentication.Password != "" {
repositoryUsername = repositoryConfig.Authentication.Username repositoryUsername = repositoryConfig.Authentication.Username
repositoryPassword = repositoryConfig.Authentication.Password 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 return "", "", "", err
} }

View file

@ -17,10 +17,11 @@ type fileResponse struct {
} }
type repositoryFilePreviewPayload struct { type repositoryFilePreviewPayload struct {
Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"` Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"`
Reference string `json:"reference" example:"refs/heads/master"` Reference string `json:"reference" example:"refs/heads/master"`
Username string `json:"username" example:"myGitUsername"` Username string `json:"username" example:"myGitUsername"`
Password string `json:"password" example:"myGitPassword"` Password string `json:"password" example:"myGitPassword"`
AuthorizationType gittypes.GitCredentialAuthType `json:"authorizationType"`
// Path to file whose content will be read // Path to file whose content will be read
TargetFile string `json:"targetFile" example:"docker-compose.yml"` TargetFile string `json:"targetFile" example:"docker-compose.yml"`
// TLSSkipVerify skips SSL verification when cloning the Git repository // 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) 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 err != nil {
if errors.Is(err, gittypes.ErrAuthenticationFailure) { if errors.Is(err, gittypes.ErrAuthenticationFailure) {
return httperror.BadRequest("Invalid git credential", err) return httperror.BadRequest("Invalid git credential", err)

View file

@ -19,14 +19,15 @@ import (
) )
type stackGitUpdatePayload struct { type stackGitUpdatePayload struct {
AutoUpdate *portainer.AutoUpdateSettings AutoUpdate *portainer.AutoUpdateSettings
Env []portainer.Pair Env []portainer.Pair
Prune bool Prune bool
RepositoryReferenceName string RepositoryReferenceName string
RepositoryAuthentication bool RepositoryAuthentication bool
RepositoryUsername string RepositoryUsername string
RepositoryPassword string RepositoryPassword string
TLSSkipVerify bool RepositoryAuthorizationType gittypes.GitCredentialAuthType
TLSSkipVerify bool
} }
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error { 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{ stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername, Username: payload.RepositoryUsername,
Password: password, 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) return httperror.InternalServerError("Unable to fetch git repository", err)
} }
} else { } else {

View file

@ -6,6 +6,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/git" "github.com/portainer/portainer/api/git"
gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
k "github.com/portainer/portainer/api/kubernetes" k "github.com/portainer/portainer/api/kubernetes"
@ -19,12 +20,13 @@ import (
) )
type stackGitRedployPayload struct { type stackGitRedployPayload struct {
RepositoryReferenceName string RepositoryReferenceName string
RepositoryAuthentication bool RepositoryAuthentication bool
RepositoryUsername string RepositoryUsername string
RepositoryPassword string RepositoryPassword string
Env []portainer.Pair RepositoryAuthorizationType gittypes.GitCredentialAuthType
Prune bool Env []portainer.Pair
Prune bool
// Force a pulling to current image with the original tag though the image is already the latest // Force a pulling to current image with the original tag though the image is already the latest
PullImage bool `example:"false"` PullImage bool `example:"false"`
@ -135,13 +137,16 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
repositoryUsername := "" repositoryUsername := ""
repositoryPassword := "" repositoryPassword := ""
repositoryAuthType := gittypes.GitCredentialAuthType_Basic
if payload.RepositoryAuthentication { if payload.RepositoryAuthentication {
repositoryPassword = payload.RepositoryPassword repositoryPassword = payload.RepositoryPassword
repositoryAuthType = payload.RepositoryAuthorizationType
// When the existing stack is using the custom username/password and the password is not updated, // 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 // the stack should keep using the saved username/password
if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil { if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
repositoryPassword = stack.GitConfig.Authentication.Password repositoryPassword = stack.GitConfig.Authentication.Password
repositoryAuthType = stack.GitConfig.Authentication.AuthorizationType
} }
repositoryUsername = payload.RepositoryUsername repositoryUsername = payload.RepositoryUsername
} }
@ -152,6 +157,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
ReferenceName: stack.GitConfig.ReferenceName, ReferenceName: stack.GitConfig.ReferenceName,
Username: repositoryUsername, Username: repositoryUsername,
Password: repositoryPassword, Password: repositoryPassword,
AuthType: repositoryAuthType,
TLSSkipVerify: stack.GitConfig.TLSSkipVerify, TLSSkipVerify: stack.GitConfig.TLSSkipVerify,
} }
@ -166,7 +172,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
return err 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 { 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)) return httperror.InternalServerError("Unable get latest commit id", errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID))
} }

View file

@ -27,12 +27,13 @@ type kubernetesFileStackUpdatePayload struct {
} }
type kubernetesGitStackUpdatePayload struct { type kubernetesGitStackUpdatePayload struct {
RepositoryReferenceName string RepositoryReferenceName string
RepositoryAuthentication bool RepositoryAuthentication bool
RepositoryUsername string RepositoryUsername string
RepositoryPassword string RepositoryPassword string
AutoUpdate *portainer.AutoUpdateSettings RepositoryAuthorizationType gittypes.GitCredentialAuthType
TLSSkipVerify bool AutoUpdate *portainer.AutoUpdateSettings
TLSSkipVerify bool
} }
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error { 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{ stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername, Username: payload.RepositoryUsername,
Password: password, 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) return httperror.InternalServerError("Unable to fetch git repository", err)
} }
} }

View file

@ -5,6 +5,7 @@ import (
"slices" "slices"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "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) 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) return httperror.InternalServerError("Unable to clone git repository", err)
} }

View file

@ -15,6 +15,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "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/proxy/factory/utils"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/authorization"
@ -418,7 +419,14 @@ func (transport *Transport) updateDefaultGitBranch(request *http.Request) error
} }
repositoryURL := remote[:len(remote)-4] 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 { if err != nil {
return err return err
} }

View file

@ -1,6 +1,9 @@
package testhelpers 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 { type gitService struct {
cloneErr error 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 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 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 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 return nil, nil
} }

View file

@ -1538,10 +1538,42 @@ type (
// GitService represents a service for managing Git // GitService represents a service for managing Git
GitService interface { GitService interface {
CloneRepository(destination string, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error CloneRepository(
LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) destination string,
ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) repositoryURL,
ListFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, includeExts []string, tlsSkipVerify bool) ([]string, error) 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 // OpenAMTService represents a service for managing OpenAMT

View file

@ -19,13 +19,23 @@ var (
func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitService, getProjectPath func() string) (string, error) { func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitService, getProjectPath func() string) (string, error) {
username := "" username := ""
password := "" password := ""
authType := gittypes.GitCredentialAuthType_Basic
if config.Authentication != nil { if config.Authentication != nil {
username = config.Authentication.Username username = config.Authentication.Username
password = config.Authentication.Password password = config.Authentication.Password
authType = config.Authentication.AuthorizationType
} }
projectPath := getProjectPath() 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 err != nil {
if errors.Is(err, gittypes.ErrAuthenticationFailure) { if errors.Is(err, gittypes.ErrAuthenticationFailure) {
newErr := git.ErrInvalidGitCredential newErr := git.ErrInvalidGitCredential
@ -36,7 +46,14 @@ func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitS
return "", newErr 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 { if err != nil {
newErr := fmt.Errorf("unable to fetch git repository id: %w", err) newErr := fmt.Errorf("unable to fetch git repository id: %w", err)
return "", newErr return "", newErr