mirror of
https://github.com/portainer/portainer.git
synced 2026-06-23 04:40:13 +00:00
feat(gitops): tidy up git auth [BE-12666] (#2026)
This commit is contained in:
+10
-11
@@ -4,13 +4,13 @@
|
||||
|
||||
Portainer maintains both Short-Term Support (STS) and Long-Term Support (LTS) versions in accordance with our official [Portainer Lifecycle Policy](https://docs.portainer.io/start/lifecycle).
|
||||
|
||||
| Version Type | Support Status |
|
||||
| --- | --- |
|
||||
| LTS (Long-Term Support) | Supported for critical security fixes |
|
||||
| Version Type | Support Status |
|
||||
| ------------------------ | ------------------------------------------- |
|
||||
| LTS (Long-Term Support) | Supported for critical security fixes |
|
||||
| STS (Short-Term Support) | Supported until the next STS or LTS release |
|
||||
| Legacy / EOL | Not supported |
|
||||
| Legacy / EOL | Not supported |
|
||||
|
||||
For a detailed breakdown of current versions and their specific End of Life (EOL) dates,
|
||||
For a detailed breakdown of current versions and their specific End of Life (EOL) dates,
|
||||
please refer to the [Portainer Lifecycle Policy](https://docs.portainer.io/start/lifecycle).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
@@ -23,17 +23,17 @@ The Portainer team takes the security of our products seriously. If you believe
|
||||
|
||||
1. **Report**: You can report in one of two ways:
|
||||
|
||||
- **GitHub**: Use the **Report a vulnerability** button on the **Security** tab of this repository.
|
||||
- **GitHub**: Use the **Report a vulnerability** button on the **Security** tab of this repository.
|
||||
|
||||
- **Email**: Send your findings to security@portainer.io.
|
||||
- **Email**: Send your findings to security@portainer.io.
|
||||
|
||||
2. **Details**: To help us verify the issue, please include:
|
||||
|
||||
- A description of the vulnerability and its potential impact.
|
||||
- A description of the vulnerability and its potential impact.
|
||||
|
||||
- Step-by-step instructions to reproduce the issue (e.g. proof-of-concept code, scripts, or screenshots).
|
||||
- Step-by-step instructions to reproduce the issue (e.g. proof-of-concept code, scripts, or screenshots).
|
||||
|
||||
- The version of the software and the environment in which it was found.
|
||||
- The version of the software and the environment in which it was found.
|
||||
|
||||
3. **Acknowledge**: We will acknowledge receipt of your report and provide an initial assessment.
|
||||
|
||||
@@ -51,7 +51,6 @@ If you follow the responsible disclosure process, we will:
|
||||
|
||||
- Give credit for the discovery (if desired) once the fix is public.
|
||||
|
||||
|
||||
We will make every effort to promptly address any security weaknesses. Security advisories and fixes will be published through GitHub Security Advisories and other channels as needed.
|
||||
|
||||
Thank you for helping keep Portainer and our community secure.
|
||||
|
||||
+90
-34
@@ -16,7 +16,9 @@ import (
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
@@ -26,7 +28,7 @@ const (
|
||||
visualStudioHostSuffix = ".visualstudio.com"
|
||||
)
|
||||
|
||||
func isAzureUrl(s string) bool {
|
||||
func IsAzureUrl(s string) bool {
|
||||
return strings.Contains(s, azureDevOpsHost) ||
|
||||
strings.Contains(s, visualStudioHostSuffix)
|
||||
}
|
||||
@@ -73,7 +75,11 @@ func newHttpClientForAzure(insecureSkipVerify bool) *http.Client {
|
||||
return httpsCli
|
||||
}
|
||||
|
||||
func (a *azureClient) download(ctx context.Context, destination string, opt cloneOption) error {
|
||||
func (a *azureClient) Download(ctx context.Context, destination string, opt *git.CloneOptions) error {
|
||||
if opt == nil {
|
||||
return errors.New("options cannot be nil")
|
||||
}
|
||||
|
||||
zipFilepath, err := a.downloadZipFromAzureDevOps(ctx, opt)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to download a zip file from Azure DevOps")
|
||||
@@ -91,13 +97,13 @@ func (a *azureClient) download(ctx context.Context, destination string, opt clon
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneOption) (string, error) {
|
||||
config, err := parseUrl(opt.repositoryUrl)
|
||||
func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt *git.CloneOptions) (string, error) {
|
||||
config, err := parseUrl(opt.URL)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
|
||||
downloadUrl, err := a.buildDownloadUrl(config, opt.referenceName)
|
||||
downloadUrl, err := a.buildDownloadUrl(config, string(opt.ReferenceName))
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to build download url")
|
||||
}
|
||||
@@ -109,9 +115,18 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
|
||||
defer logs.CloseAndLogErr(zipFile)
|
||||
|
||||
var basicAuth *githttp.BasicAuth
|
||||
if opt.Auth != nil {
|
||||
var ok bool
|
||||
basicAuth, ok = opt.Auth.(*githttp.BasicAuth)
|
||||
if !ok {
|
||||
return "", errors.New("only basic auth is supported for azure")
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
|
||||
if opt.username != "" || opt.password != "" {
|
||||
req.SetBasicAuth(opt.username, opt.password)
|
||||
if basicAuth != nil {
|
||||
req.SetBasicAuth(basicAuth.Username, basicAuth.Password)
|
||||
} else if config.username != "" || config.password != "" {
|
||||
req.SetBasicAuth(config.username, config.password)
|
||||
}
|
||||
@@ -120,7 +135,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
return "", errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
client := newHttpClientForAzure(opt.InsecureSkipTLS)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
res, err := client.Do(req)
|
||||
@@ -145,8 +160,12 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
return zipFile.Name(), nil
|
||||
}
|
||||
|
||||
func (a *azureClient) latestCommitID(ctx context.Context, opt fetchOption) (string, error) {
|
||||
rootItem, err := a.getRootItem(ctx, opt)
|
||||
func (a *azureClient) LatestCommitID(ctx context.Context, repositoryUrl, referenceName string, opt *git.ListOptions) (string, error) {
|
||||
if opt == nil {
|
||||
return "", errors.New("options cannot be nil")
|
||||
}
|
||||
|
||||
rootItem, err := a.getRootItem(ctx, repositoryUrl, referenceName, opt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -154,20 +173,29 @@ func (a *azureClient) latestCommitID(ctx context.Context, opt fetchOption) (stri
|
||||
return rootItem.CommitId, nil
|
||||
}
|
||||
|
||||
func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureItem, error) {
|
||||
config, err := parseUrl(opt.repositoryUrl)
|
||||
func (a *azureClient) getRootItem(ctx context.Context, repositoryUrl, referenceName string, opt *git.ListOptions) (*azureItem, error) {
|
||||
config, err := parseUrl(repositoryUrl)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
|
||||
rootItemUrl, err := a.buildRootItemUrl(config, opt.referenceName)
|
||||
rootItemUrl, err := a.buildRootItemUrl(config, referenceName)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to build azure root item url")
|
||||
}
|
||||
|
||||
var basicAuth *githttp.BasicAuth
|
||||
if opt.Auth != nil {
|
||||
var ok bool
|
||||
basicAuth, ok = opt.Auth.(*githttp.BasicAuth)
|
||||
if !ok {
|
||||
return nil, errors.New("only basic auth is supported for azure")
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", rootItemUrl, nil)
|
||||
if opt.username != "" || opt.password != "" {
|
||||
req.SetBasicAuth(opt.username, opt.password)
|
||||
if basicAuth != nil {
|
||||
req.SetBasicAuth(basicAuth.Username, basicAuth.Password)
|
||||
} else if config.username != "" || config.password != "" {
|
||||
req.SetBasicAuth(config.username, config.password)
|
||||
}
|
||||
@@ -176,7 +204,7 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
client := newHttpClientForAzure(opt.InsecureSkipTLS)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
@@ -239,8 +267,10 @@ func parseSshUrl(rawUrl string) (*azureOptions, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
const expectedAzureDevOpsHttpUrl = "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository"
|
||||
const expectedVisualStudioHttpUrl = "https://organisation.visualstudio.com/project/_git/repository"
|
||||
const (
|
||||
expectedAzureDevOpsHttpUrl = "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository"
|
||||
expectedVisualStudioHttpUrl = "https://organisation.visualstudio.com/project/_git/repository"
|
||||
)
|
||||
|
||||
func parseHttpUrl(rawUrl string) (*azureOptions, error) {
|
||||
u, err := url.Parse(rawUrl)
|
||||
@@ -283,7 +313,6 @@ func (a *azureClient) buildDownloadUrl(config *azureOptions, referenceName strin
|
||||
url.PathEscape(config.project),
|
||||
url.PathEscape(config.repository))
|
||||
u, err := url.Parse(rawUrl)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse download url path %s", rawUrl)
|
||||
}
|
||||
@@ -310,7 +339,6 @@ func (a *azureClient) buildRootItemUrl(config *azureOptions, referenceName strin
|
||||
url.PathEscape(config.project),
|
||||
url.PathEscape(config.repository))
|
||||
u, err := url.Parse(rawUrl)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse root item url path %s", rawUrl)
|
||||
}
|
||||
@@ -335,7 +363,6 @@ func (a *azureClient) buildRefsUrl(config *azureOptions) (string, error) {
|
||||
url.PathEscape(config.project),
|
||||
url.PathEscape(config.repository))
|
||||
u, err := url.Parse(rawUrl)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse list refs url path %s", rawUrl)
|
||||
}
|
||||
@@ -357,7 +384,6 @@ func (a *azureClient) buildTreeUrl(config *azureOptions, rootObjectHash string)
|
||||
url.PathEscape(rootObjectHash),
|
||||
)
|
||||
u, err := url.Parse(rawUrl)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse list tree url path %s", rawUrl)
|
||||
}
|
||||
@@ -400,8 +426,12 @@ func getVersionType(name string) string {
|
||||
return "commit"
|
||||
}
|
||||
|
||||
func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, error) {
|
||||
config, err := parseUrl(opt.repositoryUrl)
|
||||
func (a *azureClient) ListRefs(ctx context.Context, repositoryUrl string, opt *git.ListOptions) ([]string, error) {
|
||||
if opt == nil {
|
||||
return nil, errors.New("options cannot be nil")
|
||||
}
|
||||
|
||||
config, err := parseUrl(repositoryUrl)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
@@ -411,9 +441,18 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
|
||||
return nil, errors.WithMessage(err, "failed to build list refs url")
|
||||
}
|
||||
|
||||
var basicAuth *githttp.BasicAuth
|
||||
if opt.Auth != nil {
|
||||
var ok bool
|
||||
basicAuth, ok = opt.Auth.(*githttp.BasicAuth)
|
||||
if !ok {
|
||||
return nil, errors.New("only basic auth is supported for azure")
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", listRefsUrl, nil)
|
||||
if opt.username != "" || opt.password != "" {
|
||||
req.SetBasicAuth(opt.username, opt.password)
|
||||
if basicAuth != nil {
|
||||
req.SetBasicAuth(basicAuth.Username, basicAuth.Password)
|
||||
} else if config.username != "" || config.password != "" {
|
||||
req.SetBasicAuth(config.username, config.password)
|
||||
}
|
||||
@@ -422,7 +461,7 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
client := newHttpClientForAzure(opt.InsecureSkipTLS)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
@@ -459,13 +498,21 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
|
||||
}
|
||||
|
||||
// listFiles list all filenames under the specific repository
|
||||
func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string, error) {
|
||||
rootItem, err := a.getRootItem(ctx, opt)
|
||||
func (a *azureClient) ListFiles(ctx context.Context, dirOnly bool, opt *git.CloneOptions) ([]string, error) {
|
||||
if opt == nil {
|
||||
return nil, errors.New("options cannot be nil")
|
||||
}
|
||||
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: opt.Auth,
|
||||
InsecureSkipTLS: opt.InsecureSkipTLS,
|
||||
}
|
||||
rootItem, err := a.getRootItem(ctx, opt.URL, string(opt.ReferenceName), listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := parseUrl(opt.repositoryUrl)
|
||||
config, err := parseUrl(opt.URL)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
@@ -475,9 +522,18 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
|
||||
return nil, errors.WithMessage(err, "failed to build list tree url")
|
||||
}
|
||||
|
||||
var basicAuth *githttp.BasicAuth
|
||||
if opt.Auth != nil {
|
||||
var ok bool
|
||||
basicAuth, ok = opt.Auth.(*githttp.BasicAuth)
|
||||
if !ok {
|
||||
return nil, errors.New("only basic auth is supported for azure")
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", listTreeUrl, nil)
|
||||
if opt.username != "" || opt.password != "" {
|
||||
req.SetBasicAuth(opt.username, opt.password)
|
||||
if basicAuth != nil {
|
||||
req.SetBasicAuth(basicAuth.Username, basicAuth.Password)
|
||||
} else if config.username != "" || config.password != "" {
|
||||
req.SetBasicAuth(config.username, config.password)
|
||||
}
|
||||
@@ -486,7 +542,7 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
client := newHttpClientForAzure(opt.InsecureSkipTLS)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
@@ -518,7 +574,7 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
|
||||
for _, treeEntry := range tree.TreeEntries {
|
||||
mode, _ := filemode.New(treeEntry.Mode)
|
||||
isDir := filemode.Dir == mode
|
||||
if opt.dirOnly == isDir {
|
||||
if dirOnly == isDir {
|
||||
allPaths = append(allPaths, treeEntry.RelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
||||
tt.args.referenceName,
|
||||
"",
|
||||
"",
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -88,7 +87,6 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
"",
|
||||
pat,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -106,7 +104,6 @@ func TestService_LatestCommitID_Azure(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
"",
|
||||
pat,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -124,7 +121,6 @@ func TestService_ListRefs_Azure(t *testing.T) {
|
||||
privateAzureRepoURL,
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
@@ -140,10 +136,10 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
|
||||
go func() {
|
||||
_, _ = service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, _ = service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
}()
|
||||
|
||||
_, err := service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -152,6 +148,14 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
|
||||
func TestService_ListFiles_Azure(t *testing.T) {
|
||||
ensureIntegrationTest(t)
|
||||
|
||||
type args struct {
|
||||
repositoryUrl string
|
||||
referenceName string
|
||||
username string
|
||||
password string
|
||||
extensions []string
|
||||
}
|
||||
|
||||
type expectResult struct {
|
||||
shouldFail bool
|
||||
err error
|
||||
@@ -163,22 +167,19 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args fetchOption
|
||||
extensions []string
|
||||
expect expectResult
|
||||
name string
|
||||
args args
|
||||
expect expectResult
|
||||
}{
|
||||
{
|
||||
name: "list tree with real repository and head ref but incorrect credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
err: gittypes.ErrAuthenticationFailure,
|
||||
@@ -186,15 +187,13 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref but no credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: "",
|
||||
password: "",
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
err: gittypes.ErrAuthenticationFailure,
|
||||
@@ -202,15 +201,13 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
matchedCount: 19,
|
||||
@@ -218,15 +215,13 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref and existing file extension",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{"yml"},
|
||||
},
|
||||
extensions: []string{"yml"},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
matchedCount: 2,
|
||||
@@ -234,15 +229,13 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref and non-existing file extension",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{"hcl"},
|
||||
},
|
||||
extensions: []string{"hcl"},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
matchedCount: 2,
|
||||
@@ -250,30 +243,26 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository but non-existing ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list tree with fake repository ",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL + "fake",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL + "fake",
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
err: gittypes.ErrIncorrectRepositoryURL,
|
||||
@@ -288,10 +277,9 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
tt.args.referenceName,
|
||||
tt.args.username,
|
||||
tt.args.password,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
tt.extensions,
|
||||
tt.args.extensions,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -323,7 +311,6 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -336,7 +323,6 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
|
||||
+93
-75
@@ -7,6 +7,9 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/pkg/fips"
|
||||
|
||||
@@ -234,7 +237,7 @@ func Test_isAzureUrl(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, isAzureUrl(tt.args.s))
|
||||
assert.Equal(t, tt.want, IsAzureUrl(tt.args.s))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -243,7 +246,9 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
fips.InitFIPS(false)
|
||||
|
||||
type args struct {
|
||||
options baseOption
|
||||
repositoryUrl string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
type basicAuth struct {
|
||||
username, password string
|
||||
@@ -256,9 +261,7 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
{
|
||||
name: "username, password embedded",
|
||||
args: args{
|
||||
options: baseOption{
|
||||
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
|
||||
},
|
||||
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
|
||||
},
|
||||
want: &basicAuth{
|
||||
username: "username",
|
||||
@@ -268,11 +271,9 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
{
|
||||
name: "username, password embedded, clone options take precedence",
|
||||
args: args{
|
||||
options: baseOption{
|
||||
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
|
||||
username: "u",
|
||||
password: "p",
|
||||
},
|
||||
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
|
||||
username: "u",
|
||||
password: "p",
|
||||
},
|
||||
want: &basicAuth{
|
||||
username: "u",
|
||||
@@ -282,9 +283,7 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
{
|
||||
name: "no credentials",
|
||||
args: args{
|
||||
options: baseOption{
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository",
|
||||
},
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -303,10 +302,14 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
baseUrl: server.URL,
|
||||
}
|
||||
|
||||
option := cloneOption{
|
||||
fetchOption: fetchOption{
|
||||
baseOption: tt.args.options,
|
||||
},
|
||||
option := &git.CloneOptions{
|
||||
URL: tt.args.repositoryUrl,
|
||||
}
|
||||
if tt.args.username != "" || tt.args.password != "" {
|
||||
option.Auth = &githttp.BasicAuth{
|
||||
Username: tt.args.username,
|
||||
Password: tt.args.password,
|
||||
}
|
||||
}
|
||||
_, err := a.downloadZipFromAzureDevOps(context.Background(), option)
|
||||
require.Error(t, err)
|
||||
@@ -340,18 +343,21 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
|
||||
a := &azureClient{baseUrl: server.URL}
|
||||
|
||||
type args struct {
|
||||
repositoryUrl string
|
||||
referenceName string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args fetchOption
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should be able to parse response",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository",
|
||||
referenceName: "",
|
||||
},
|
||||
want: "27104ad7549d9e66685e115a497533f18024be9c",
|
||||
@@ -361,7 +367,7 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := a.latestCommitID(context.Background(), tt.args)
|
||||
id, err := a.LatestCommitID(context.Background(), tt.args.repositoryUrl, tt.args.referenceName, &git.ListOptions{})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("azureDownloader.latestCommitID() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -375,22 +381,23 @@ type testRepoManager struct {
|
||||
called bool
|
||||
}
|
||||
|
||||
func (t *testRepoManager) download(_ context.Context, _ string, _ cloneOption) error {
|
||||
func (t *testRepoManager) Download(_ context.Context, _ string, _ *git.CloneOptions) error {
|
||||
t.called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testRepoManager) latestCommitID(_ context.Context, _ fetchOption) (string, error) {
|
||||
func (t *testRepoManager) LatestCommitID(_ context.Context, _, _ string, _ *git.ListOptions) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (t *testRepoManager) listRefs(_ context.Context, _ baseOption) ([]string, error) {
|
||||
func (t *testRepoManager) ListRefs(_ context.Context, _ string, _ *git.ListOptions) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *testRepoManager) listFiles(_ context.Context, _ fetchOption) ([]string, error) {
|
||||
func (t *testRepoManager) ListFiles(_ context.Context, _ bool, _ *git.CloneOptions) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func Test_cloneRepository_azure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -420,15 +427,7 @@ func Test_cloneRepository_azure(t *testing.T) {
|
||||
git := &testRepoManager{}
|
||||
|
||||
s := &Service{azure: azure, git: git}
|
||||
err := s.cloneRepository("", cloneOption{
|
||||
fetchOption: fetchOption{
|
||||
baseOption: baseOption{
|
||||
|
||||
repositoryUrl: tt.url,
|
||||
},
|
||||
},
|
||||
depth: 1,
|
||||
})
|
||||
err := s.CloneRepository("", tt.url, "", "", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// if azure API is called, git isn't and vice versa
|
||||
@@ -443,6 +442,12 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
|
||||
client := NewAzureClient()
|
||||
|
||||
type args struct {
|
||||
repositoryUrl string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
type expectResult struct {
|
||||
err error
|
||||
refsCount int
|
||||
@@ -453,12 +458,12 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args baseOption
|
||||
args args
|
||||
expect expectResult
|
||||
}{
|
||||
{
|
||||
name: "list refs of a real repository",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
@@ -470,7 +475,7 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list refs of a real repository with incorrect credential",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
@@ -481,7 +486,7 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list refs of a real repository without providing credential",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -492,7 +497,7 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list refs of a fake repository",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL + "fake",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
@@ -505,7 +510,14 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
refs, err := client.listRefs(context.TODO(), tt.args)
|
||||
option := &git.ListOptions{}
|
||||
if tt.args.username != "" || tt.args.password != "" {
|
||||
option.Auth = &githttp.BasicAuth{
|
||||
Username: tt.args.username,
|
||||
Password: tt.args.password,
|
||||
}
|
||||
}
|
||||
refs, err := client.ListRefs(context.TODO(), tt.args.repositoryUrl, option)
|
||||
if tt.expect.err == nil {
|
||||
require.NoError(t, err)
|
||||
if tt.expect.refsCount > 0 {
|
||||
@@ -517,7 +529,6 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_listFiles_azure(t *testing.T) {
|
||||
@@ -525,6 +536,13 @@ func Test_listFiles_azure(t *testing.T) {
|
||||
|
||||
client := NewAzureClient()
|
||||
|
||||
type args struct {
|
||||
repositoryUrl string
|
||||
referenceName string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
type expectResult struct {
|
||||
shouldFail bool
|
||||
err error
|
||||
@@ -535,18 +553,16 @@ func Test_listFiles_azure(t *testing.T) {
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
tests := []struct {
|
||||
name string
|
||||
args fetchOption
|
||||
args args
|
||||
expect expectResult
|
||||
}{
|
||||
{
|
||||
name: "list tree with real repository and head ref but incorrect credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -555,13 +571,11 @@ func Test_listFiles_azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref but no credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -570,13 +584,11 @@ func Test_listFiles_azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
@@ -585,13 +597,11 @@ func Test_listFiles_azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository but non-existing ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL,
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -599,13 +609,11 @@ func Test_listFiles_azure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with fake repository ",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateAzureRepoURL + "fake",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateAzureRepoURL + "fake",
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -616,7 +624,17 @@ func Test_listFiles_azure(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, err := client.listFiles(context.TODO(), tt.args)
|
||||
option := &git.CloneOptions{
|
||||
URL: tt.args.repositoryUrl,
|
||||
ReferenceName: plumbing.ReferenceName(tt.args.referenceName),
|
||||
}
|
||||
if tt.args.username != "" || tt.args.password != "" {
|
||||
option.Auth = &githttp.BasicAuth{
|
||||
Username: tt.args.username,
|
||||
Password: tt.args.password,
|
||||
}
|
||||
}
|
||||
paths, err := client.ListFiles(context.TODO(), false, option)
|
||||
if tt.expect.shouldFail {
|
||||
require.Error(t, err)
|
||||
if tt.expect.err != nil {
|
||||
|
||||
@@ -19,7 +19,6 @@ 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"`
|
||||
}
|
||||
@@ -49,7 +48,6 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
|
||||
options.ReferenceName,
|
||||
options.Username,
|
||||
options.Password,
|
||||
options.AuthType,
|
||||
options.TLSSkipVerify,
|
||||
); err != nil {
|
||||
cleanUp = false
|
||||
|
||||
+12
-89
@@ -11,11 +11,8 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -30,21 +27,8 @@ func NewGitClient(preserveGitDir bool) *gitClient {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) error {
|
||||
gitOptions := git.CloneOptions{
|
||||
URL: opt.repositoryUrl,
|
||||
Depth: opt.depth,
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
Auth: getAuth(opt.authType, opt.username, opt.password),
|
||||
Tags: git.NoTags,
|
||||
}
|
||||
|
||||
if opt.referenceName != "" {
|
||||
gitOptions.ReferenceName = plumbing.ReferenceName(opt.referenceName)
|
||||
}
|
||||
|
||||
_, err := git.PlainCloneContext(ctx, dst, false, &gitOptions)
|
||||
|
||||
func (c *gitClient) Download(ctx context.Context, dst string, opt *git.CloneOptions) error {
|
||||
_, err := git.PlainCloneContext(ctx, dst, false, opt)
|
||||
if err != nil {
|
||||
if err.Error() == "authentication required" {
|
||||
return gittypes.ErrAuthenticationFailure
|
||||
@@ -62,18 +46,13 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string, error) {
|
||||
func (c *gitClient) LatestCommitID(ctx context.Context, repositoryUrl, referenceName string, opt *git.ListOptions) (string, error) {
|
||||
remote := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{opt.repositoryUrl},
|
||||
URLs: []string{repositoryUrl},
|
||||
})
|
||||
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: getAuth(opt.authType, opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
}
|
||||
|
||||
refs, err := remote.List(listOptions)
|
||||
refs, err := remote.List(opt)
|
||||
if err != nil {
|
||||
if err.Error() == "authentication required" {
|
||||
return "", gittypes.ErrAuthenticationFailure
|
||||
@@ -81,7 +60,6 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
|
||||
return "", errors.Wrap(err, "failed to list repository refs")
|
||||
}
|
||||
|
||||
referenceName := opt.referenceName
|
||||
if referenceName == "" {
|
||||
for _, ref := range refs {
|
||||
if strings.EqualFold(ref.Name().String(), "HEAD") {
|
||||
@@ -96,60 +74,16 @@ 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", referenceName)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
return &githttp.BasicAuth{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
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, repositoryUrl string, opt *git.ListOptions) ([]string, error) {
|
||||
rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{opt.repositoryUrl},
|
||||
URLs: []string{repositoryUrl},
|
||||
})
|
||||
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: getAuth(opt.authType, opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
}
|
||||
|
||||
refs, err := rem.List(listOptions)
|
||||
refs, err := rem.List(opt)
|
||||
if err != nil {
|
||||
return nil, checkGitError(err)
|
||||
}
|
||||
@@ -166,19 +100,8 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
|
||||
}
|
||||
|
||||
// listFiles list all filenames under the specific repository
|
||||
func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, error) {
|
||||
cloneOption := &git.CloneOptions{
|
||||
URL: opt.repositoryUrl,
|
||||
NoCheckout: true,
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName(opt.referenceName),
|
||||
Auth: getAuth(opt.authType, opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
Tags: git.NoTags,
|
||||
}
|
||||
|
||||
repo, err := git.Clone(memory.NewStorage(), nil, cloneOption)
|
||||
func (c *gitClient) ListFiles(ctx context.Context, dirOnly bool, opt *git.CloneOptions) ([]string, error) {
|
||||
repo, err := git.Clone(memory.NewStorage(), nil, opt)
|
||||
if err != nil {
|
||||
return nil, checkGitError(err)
|
||||
}
|
||||
@@ -210,7 +133,7 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
|
||||
}
|
||||
|
||||
isDir := entry.Mode == filemode.Dir
|
||||
if opt.dirOnly == isDir {
|
||||
if dirOnly == isDir {
|
||||
allPaths = append(allPaths, name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -54,7 +53,6 @@ func TestService_LatestCommitID_GitHub(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -69,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, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
}
|
||||
@@ -83,10 +81,10 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
go func() {
|
||||
_, _ = service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, _ = service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
}()
|
||||
|
||||
_, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -95,6 +93,14 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
|
||||
func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
ensureIntegrationTest(t)
|
||||
|
||||
type args struct {
|
||||
repositoryUrl string
|
||||
referenceName string
|
||||
username string
|
||||
password string
|
||||
extensions []string
|
||||
}
|
||||
|
||||
type expectResult struct {
|
||||
shouldFail bool
|
||||
err error
|
||||
@@ -105,22 +111,19 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args fetchOption
|
||||
extensions []string
|
||||
expect expectResult
|
||||
name string
|
||||
args args
|
||||
expect expectResult
|
||||
}{
|
||||
{
|
||||
name: "list tree with real repository and head ref but incorrect credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
err: gittypes.ErrAuthenticationFailure,
|
||||
@@ -128,15 +131,13 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref but no credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
referenceName: "refs/heads/main",
|
||||
username: "",
|
||||
password: "",
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
err: gittypes.ErrAuthenticationFailure,
|
||||
@@ -144,15 +145,13 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
matchedCount: 15,
|
||||
@@ -160,15 +159,13 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref and existing file extension",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{"yml"},
|
||||
},
|
||||
extensions: []string{"yml"},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
matchedCount: 2,
|
||||
@@ -176,15 +173,13 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref and non-existing file extension",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{"hcl"},
|
||||
},
|
||||
extensions: []string{"hcl"},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
matchedCount: 2,
|
||||
@@ -192,30 +187,26 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository but non-existing ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list tree with fake repository ",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
extensions: []string{},
|
||||
},
|
||||
extensions: []string{},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
err: gittypes.ErrIncorrectRepositoryURL,
|
||||
@@ -230,10 +221,9 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
tt.args.referenceName,
|
||||
tt.args.username,
|
||||
tt.args.password,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
tt.extensions,
|
||||
tt.args.extensions,
|
||||
false,
|
||||
)
|
||||
if tt.expect.shouldFail {
|
||||
@@ -265,7 +255,6 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -278,7 +267,6 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -297,7 +285,7 @@ func TestService_purgeCache_Github(t *testing.T) {
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := NewService(context.TODO())
|
||||
|
||||
_, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.ListFiles(
|
||||
@@ -305,7 +293,6 @@ func TestService_purgeCache_Github(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -331,14 +318,13 @@ 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)
|
||||
|
||||
_, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
require.NoError(t, err)
|
||||
_, err = service.ListFiles(
|
||||
repositoryUrl,
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -375,12 +361,12 @@ func TestService_HardRefresh_ListRefs_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 2, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
}
|
||||
@@ -393,7 +379,7 @@ 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, gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
@@ -403,7 +389,6 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -418,7 +403,6 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) {
|
||||
"refs/heads/test",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -428,11 +412,11 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) {
|
||||
assert.GreaterOrEqual(t, len(files), 1)
|
||||
assert.Equal(t, 2, service.repoFileCache.Len())
|
||||
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, true, false)
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", true, false)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
// The relevant file caches should be removed too
|
||||
@@ -451,7 +435,6 @@ func TestService_HardRefresh_ListFiles_GitHub(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{},
|
||||
@@ -466,7 +449,6 @@ func TestService_HardRefresh_ListFiles_GitHub(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
"fake-token",
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
true,
|
||||
[]string{},
|
||||
@@ -495,7 +477,6 @@ func TestService_CloneRepository_TokenAuth(t *testing.T) {
|
||||
"refs/heads/main",
|
||||
username,
|
||||
accessToken,
|
||||
gittypes.GitCredentialAuthType_Token,
|
||||
false,
|
||||
)
|
||||
|
||||
|
||||
+65
-67
@@ -10,7 +10,9 @@ import (
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -20,7 +22,7 @@ func setup(t *testing.T) string {
|
||||
dir := t.TempDir()
|
||||
bareRepoDir := filepath.Join(dir, "test-clone.git")
|
||||
|
||||
file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0755)
|
||||
file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0o755)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to open an archive"))
|
||||
}
|
||||
@@ -39,7 +41,7 @@ func Test_ClonePublicRepository_Shallow(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, getCommitHistoryLength(t, dir), "cloned repo has incorrect depth")
|
||||
}
|
||||
@@ -51,41 +53,18 @@ func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
|
||||
require.NoError(t, err)
|
||||
assert.NoDirExists(t, filepath.Join(dir, ".git"))
|
||||
}
|
||||
|
||||
func Test_cloneRepository(t *testing.T) {
|
||||
service := Service{git: NewGitClient(true)} // no need for http client since the test access the repo via file system.
|
||||
|
||||
repositoryURL := setup(t)
|
||||
referenceName := "refs/heads/main"
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Logf("Cloning into %s", dir)
|
||||
|
||||
err := service.cloneRepository(dir, cloneOption{
|
||||
fetchOption: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: repositoryURL,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
},
|
||||
depth: 10,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, getCommitHistoryLength(t, dir), "cloned repo has incorrect depth")
|
||||
}
|
||||
|
||||
func Test_latestCommitID(t *testing.T) {
|
||||
service := Service{git: NewGitClient(true)} // no need for http client since the test access the repo via file system.
|
||||
|
||||
repositoryURL := setup(t)
|
||||
referenceName := "refs/heads/main"
|
||||
|
||||
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false)
|
||||
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", false)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
|
||||
@@ -96,7 +75,7 @@ func Test_ListRefs(t *testing.T) {
|
||||
|
||||
repositoryURL := setup(t)
|
||||
|
||||
fs, err := service.ListRefs(repositoryURL, "", "", gittypes.GitCredentialAuthType_Basic, false, false)
|
||||
fs, err := service.ListRefs(repositoryURL, "", "", false, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"refs/heads/main"}, fs)
|
||||
@@ -113,7 +92,6 @@ func Test_ListFiles(t *testing.T) {
|
||||
referenceName,
|
||||
"",
|
||||
"",
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
false,
|
||||
[]string{".yml"},
|
||||
@@ -154,6 +132,12 @@ func Test_listRefsPrivateRepository(t *testing.T) {
|
||||
|
||||
client := NewGitClient(false)
|
||||
|
||||
type args struct {
|
||||
repositoryUrl string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
type expectResult struct {
|
||||
err error
|
||||
refsCount int
|
||||
@@ -161,12 +145,12 @@ func Test_listRefsPrivateRepository(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args baseOption
|
||||
args args
|
||||
expect expectResult
|
||||
}{
|
||||
{
|
||||
name: "list refs of a real private repository",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
@@ -178,7 +162,7 @@ func Test_listRefsPrivateRepository(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list refs of a real private repository with incorrect credential",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
@@ -189,7 +173,7 @@ func Test_listRefsPrivateRepository(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list refs of a fake repository without providing credential",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -200,7 +184,7 @@ func Test_listRefsPrivateRepository(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list refs of a fake repository",
|
||||
args: baseOption{
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
@@ -213,7 +197,14 @@ func Test_listRefsPrivateRepository(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
refs, err := client.listRefs(context.TODO(), tt.args)
|
||||
option := &git.ListOptions{}
|
||||
if tt.args.username != "" || tt.args.password != "" {
|
||||
option.Auth = &githttp.BasicAuth{
|
||||
Username: tt.args.username,
|
||||
Password: tt.args.password,
|
||||
}
|
||||
}
|
||||
refs, err := client.ListRefs(context.TODO(), tt.args.repositoryUrl, option)
|
||||
if tt.expect.err == nil {
|
||||
require.NoError(t, err)
|
||||
if tt.expect.refsCount > 0 {
|
||||
@@ -232,6 +223,13 @@ func Test_listFilesPrivateRepository(t *testing.T) {
|
||||
|
||||
client := NewGitClient(false)
|
||||
|
||||
type args struct {
|
||||
repositoryUrl string
|
||||
referenceName string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
type expectResult struct {
|
||||
shouldFail bool
|
||||
err error
|
||||
@@ -243,18 +241,16 @@ func Test_listFilesPrivateRepository(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args fetchOption
|
||||
args args
|
||||
expect expectResult
|
||||
}{
|
||||
{
|
||||
name: "list tree with real repository and head ref but incorrect credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: "test-username",
|
||||
password: "test-token",
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -263,13 +259,11 @@ func Test_listFilesPrivateRepository(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref but no credential",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -278,13 +272,11 @@ func Test_listFilesPrivateRepository(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository and head ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/heads/main",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
expect: expectResult{
|
||||
err: nil,
|
||||
@@ -293,13 +285,11 @@ func Test_listFilesPrivateRepository(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with real repository but non-existing ref",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL,
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -307,13 +297,11 @@ func Test_listFilesPrivateRepository(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list tree with fake repository ",
|
||||
args: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
args: args{
|
||||
repositoryUrl: privateGitRepoURL + "fake",
|
||||
referenceName: "refs/fake/feature",
|
||||
username: username,
|
||||
password: accessToken,
|
||||
},
|
||||
expect: expectResult{
|
||||
shouldFail: true,
|
||||
@@ -324,7 +312,17 @@ func Test_listFilesPrivateRepository(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, err := client.listFiles(context.TODO(), tt.args)
|
||||
option := &git.CloneOptions{
|
||||
URL: tt.args.repositoryUrl,
|
||||
ReferenceName: plumbing.ReferenceName(tt.args.referenceName),
|
||||
}
|
||||
if tt.args.username != "" || tt.args.password != "" {
|
||||
option.Auth = &githttp.BasicAuth{
|
||||
Username: tt.args.username,
|
||||
Password: tt.args.password,
|
||||
}
|
||||
}
|
||||
paths, err := client.ListFiles(context.TODO(), false, option)
|
||||
if tt.expect.shouldFail {
|
||||
require.Error(t, err)
|
||||
if tt.expect.err != nil {
|
||||
|
||||
+55
-85
@@ -7,8 +7,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
@@ -18,40 +20,18 @@ const (
|
||||
repositoryCacheTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
// baseOption provides a minimum group of information to operate a git repository, like git-remote
|
||||
type baseOption struct {
|
||||
repositoryUrl string
|
||||
username string
|
||||
password string
|
||||
authType gittypes.GitCredentialAuthType
|
||||
tlsSkipVerify bool
|
||||
}
|
||||
|
||||
// fetchOption allows to specify the reference name of the target repository
|
||||
type fetchOption struct {
|
||||
baseOption
|
||||
referenceName string
|
||||
dirOnly bool
|
||||
}
|
||||
|
||||
// cloneOption allows to add a history truncated to the specified number of commits
|
||||
type cloneOption struct {
|
||||
fetchOption
|
||||
depth int
|
||||
}
|
||||
|
||||
type repoManager interface {
|
||||
download(ctx context.Context, dst string, opt cloneOption) error
|
||||
latestCommitID(ctx context.Context, opt fetchOption) (string, error)
|
||||
listRefs(ctx context.Context, opt baseOption) ([]string, error)
|
||||
listFiles(ctx context.Context, opt fetchOption) ([]string, error)
|
||||
type RepoManager interface {
|
||||
Download(ctx context.Context, dst string, opt *git.CloneOptions) error
|
||||
LatestCommitID(ctx context.Context, repositoryUrl, referenceName string, opt *git.ListOptions) (string, error)
|
||||
ListRefs(ctx context.Context, repositoryUrl string, opt *git.ListOptions) ([]string, error)
|
||||
ListFiles(ctx context.Context, dirOnly bool, opt *git.CloneOptions) ([]string, error)
|
||||
}
|
||||
|
||||
// Service represents a service for managing Git.
|
||||
type Service struct {
|
||||
shutdownCtx context.Context
|
||||
azure repoManager
|
||||
git repoManager
|
||||
azure RepoManager
|
||||
git RepoManager
|
||||
timerStopped bool
|
||||
mut sync.Mutex
|
||||
|
||||
@@ -131,61 +111,47 @@ func (service *Service) CloneRepository(
|
||||
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,
|
||||
},
|
||||
depth: 1,
|
||||
gitOptions := &git.CloneOptions{
|
||||
URL: repositoryURL,
|
||||
Depth: 1,
|
||||
InsecureSkipTLS: tlsSkipVerify,
|
||||
Auth: GetBasicAuth(username, password),
|
||||
Tags: git.NoTags,
|
||||
}
|
||||
|
||||
return service.cloneRepository(destination, options)
|
||||
if referenceName != "" {
|
||||
gitOptions.ReferenceName = plumbing.ReferenceName(referenceName)
|
||||
}
|
||||
|
||||
return service.repoManager(repositoryURL).Download(context.TODO(), destination, gitOptions)
|
||||
}
|
||||
|
||||
func (service *Service) repoManager(options baseOption) repoManager {
|
||||
func (service *Service) repoManager(repositoryURL string) RepoManager {
|
||||
repoManager := service.git
|
||||
|
||||
if isAzureUrl(options.repositoryUrl) {
|
||||
if IsAzureUrl(repositoryURL) {
|
||||
repoManager = service.azure
|
||||
}
|
||||
|
||||
return repoManager
|
||||
}
|
||||
|
||||
func (service *Service) cloneRepository(destination string, options cloneOption) error {
|
||||
return service.repoManager(options.baseOption).download(context.TODO(), destination, options)
|
||||
}
|
||||
|
||||
// LatestCommitID returns SHA1 of the latest commit of the specified reference
|
||||
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,
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: GetBasicAuth(username, password),
|
||||
InsecureSkipTLS: tlsSkipVerify,
|
||||
}
|
||||
|
||||
return service.repoManager(options.baseOption).latestCommitID(context.TODO(), options)
|
||||
return service.repoManager(repositoryURL).LatestCommitID(context.TODO(), repositoryURL, referenceName, listOptions)
|
||||
}
|
||||
|
||||
// ListRefs will list target repository's references without cloning the repository
|
||||
@@ -193,7 +159,6 @@ func (service *Service) ListRefs(
|
||||
repositoryURL,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
hardRefresh bool,
|
||||
tlsSkipVerify bool,
|
||||
) ([]string, error) {
|
||||
@@ -218,15 +183,12 @@ func (service *Service) ListRefs(
|
||||
}
|
||||
}
|
||||
|
||||
options := baseOption{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
authType: authType,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
options := &git.ListOptions{
|
||||
Auth: GetBasicAuth(username, password),
|
||||
InsecureSkipTLS: tlsSkipVerify,
|
||||
}
|
||||
|
||||
refs, err := service.repoManager(options).listRefs(context.TODO(), options)
|
||||
refs, err := service.repoManager(repositoryURL).ListRefs(context.TODO(), repositoryURL, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -247,7 +209,6 @@ func (service *Service) ListFiles(
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
dirOnly,
|
||||
hardRefresh bool,
|
||||
includedExts []string,
|
||||
@@ -259,7 +220,6 @@ func (service *Service) ListFiles(
|
||||
username,
|
||||
password,
|
||||
strconv.FormatBool(tlsSkipVerify),
|
||||
strconv.Itoa(int(authType)),
|
||||
strconv.FormatBool(dirOnly),
|
||||
)
|
||||
|
||||
@@ -269,7 +229,6 @@ func (service *Service) ListFiles(
|
||||
referenceName,
|
||||
username,
|
||||
password,
|
||||
authType,
|
||||
dirOnly,
|
||||
hardRefresh,
|
||||
tlsSkipVerify,
|
||||
@@ -284,7 +243,6 @@ func (service *Service) listFiles(
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
dirOnly,
|
||||
hardRefresh bool,
|
||||
tlsSkipVerify bool,
|
||||
@@ -295,7 +253,6 @@ func (service *Service) listFiles(
|
||||
username,
|
||||
password,
|
||||
strconv.FormatBool(tlsSkipVerify),
|
||||
strconv.Itoa(int(authType)),
|
||||
strconv.FormatBool(dirOnly),
|
||||
)
|
||||
|
||||
@@ -313,19 +270,18 @@ func (service *Service) listFiles(
|
||||
}
|
||||
}
|
||||
|
||||
options := fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
authType: authType,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
dirOnly: dirOnly,
|
||||
cloneOption := &git.CloneOptions{
|
||||
URL: repositoryURL,
|
||||
NoCheckout: true,
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName(referenceName),
|
||||
Auth: GetBasicAuth(username, password),
|
||||
InsecureSkipTLS: tlsSkipVerify,
|
||||
Tags: git.NoTags,
|
||||
}
|
||||
|
||||
files, err := service.repoManager(options.baseOption).listFiles(context.TODO(), options)
|
||||
files, err := service.repoManager(repositoryURL).ListFiles(context.TODO(), dirOnly, cloneOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -380,3 +336,17 @@ func filterFiles(paths []string, includedExts []string) []string {
|
||||
|
||||
return includedFiles
|
||||
}
|
||||
|
||||
func GetBasicAuth(username, password string) *githttp.BasicAuth {
|
||||
if password != "" {
|
||||
if username == "" {
|
||||
username = "token"
|
||||
}
|
||||
|
||||
return &githttp.BasicAuth{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+3
-11
@@ -9,13 +9,6 @@ var (
|
||||
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
|
||||
@@ -33,11 +26,10 @@ type RepoConfig struct {
|
||||
}
|
||||
|
||||
type GitAuthentication struct {
|
||||
Username string
|
||||
Password string
|
||||
AuthorizationType GitCredentialAuthType
|
||||
Username string
|
||||
Password string
|
||||
// Git credentials identifier when the value is not 0
|
||||
// When the value is 0, Username, Password, and Authtype are set without using saved credential
|
||||
// When the value is 0, Username and Password are set without using saved credential
|
||||
// This is introduced since 2.15.0
|
||||
GitCredentialID int `example:"0"`
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g
|
||||
gitConfig.ReferenceName,
|
||||
username,
|
||||
password,
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
gitConfig.TLSSkipVerify,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -69,7 +68,6 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g
|
||||
cloneParams.auth = &gitAuth{
|
||||
username: username,
|
||||
password: password,
|
||||
authType: gitConfig.Authentication.AuthorizationType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +95,6 @@ type cloneRepositoryParameters struct {
|
||||
}
|
||||
|
||||
type gitAuth struct {
|
||||
authType gittypes.GitCredentialAuthType
|
||||
username string
|
||||
password string
|
||||
}
|
||||
@@ -110,7 +107,6 @@ func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepos
|
||||
cloneParams.ref,
|
||||
cloneParams.auth.username,
|
||||
cloneParams.auth.password,
|
||||
cloneParams.auth.authType,
|
||||
cloneParams.tlsSkipVerify,
|
||||
)
|
||||
}
|
||||
@@ -121,7 +117,6 @@ func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepos
|
||||
cloneParams.ref,
|
||||
"",
|
||||
"",
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
cloneParams.tlsSkipVerify,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
)
|
||||
|
||||
func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
|
||||
if len(repoConfig.URL) == 0 || !validate.IsURL(repoConfig.URL) {
|
||||
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
|
||||
return ValidateRepoAuthentication(repoConfig.Authentication)
|
||||
}
|
||||
|
||||
func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error {
|
||||
if auth != nil && len(auth.Password) == 0 && auth.GitCredentialID == 0 {
|
||||
return httperrors.NewInvalidPayloadError("Invalid repository credentials. Password or GitCredentialID must be specified when authentication is enabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -46,7 +46,6 @@ func (g *TestGitService) CloneRepository(
|
||||
referenceName string,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
tlsSkipVerify bool,
|
||||
) error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
@@ -59,7 +58,6 @@ func (g *TestGitService) LatestCommitID(
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
tlsSkipVerify bool,
|
||||
) (string, error) {
|
||||
return "", nil
|
||||
@@ -84,7 +82,6 @@ func (g *InvalidTestGitService) CloneRepository(
|
||||
refName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
tlsSkipVerify bool,
|
||||
) error {
|
||||
return errors.New("simulate network error")
|
||||
@@ -95,7 +92,6 @@ func (g *InvalidTestGitService) LatestCommitID(
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
tlsSkipVerify bool,
|
||||
) (string, error) {
|
||||
return "", nil
|
||||
|
||||
@@ -45,8 +45,6 @@ type customTemplateUpdatePayload struct {
|
||||
// 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"`
|
||||
@@ -184,15 +182,12 @@ 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,
|
||||
AuthorizationType: payload.RepositoryAuthorizationType,
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +197,6 @@ 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 {
|
||||
@@ -216,7 +210,6 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
gitConfig.ReferenceName,
|
||||
repositoryUsername,
|
||||
repositoryPassword,
|
||||
repositoryAuthType,
|
||||
gitConfig.TLSSkipVerify,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,8 +34,6 @@ 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
|
||||
@@ -128,9 +126,8 @@ func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dat
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
repoConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
AuthorizationType: payload.RepositoryAuthorizationType,
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +149,9 @@ 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(
|
||||
@@ -165,7 +160,6 @@ func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStore
|
||||
repositoryConfig.ReferenceName,
|
||||
repositoryUsername,
|
||||
repositoryPassword,
|
||||
repositoryAuthType,
|
||||
repositoryConfig.TLSSkipVerify,
|
||||
); err != nil {
|
||||
return "", "", "", err
|
||||
|
||||
@@ -18,11 +18,10 @@ 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"`
|
||||
AuthorizationType gittypes.GitCredentialAuthType `json:"authorizationType"`
|
||||
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"`
|
||||
// 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
|
||||
@@ -76,7 +75,6 @@ func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *ht
|
||||
payload.Reference,
|
||||
payload.Username,
|
||||
payload.Password,
|
||||
payload.AuthorizationType,
|
||||
payload.TLSSkipVerify,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,15 +19,14 @@ import (
|
||||
)
|
||||
|
||||
type stackGitUpdatePayload struct {
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
Env []portainer.Pair
|
||||
Prune bool
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
RepositoryAuthorizationType gittypes.GitCredentialAuthType
|
||||
TLSSkipVerify bool
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
Env []portainer.Pair
|
||||
Prune bool
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -169,9 +168,8 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
AuthorizationType: payload.RepositoryAuthorizationType,
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if _, err := handler.GitService.LatestCommitID(
|
||||
@@ -179,7 +177,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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"
|
||||
@@ -20,13 +19,12 @@ import (
|
||||
)
|
||||
|
||||
type stackGitRedployPayload struct {
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
RepositoryAuthorizationType gittypes.GitCredentialAuthType
|
||||
Env []portainer.Pair
|
||||
Prune bool
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
Env []portainer.Pair
|
||||
Prune bool
|
||||
// RepullImageAndRedeploy indicates whether to force repulling images and redeploying the stack
|
||||
RepullImageAndRedeploy bool
|
||||
|
||||
@@ -143,16 +141,13 @@ 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
|
||||
}
|
||||
@@ -163,7 +158,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
ReferenceName: stack.GitConfig.ReferenceName,
|
||||
Username: repositoryUsername,
|
||||
Password: repositoryPassword,
|
||||
AuthType: repositoryAuthType,
|
||||
TLSSkipVerify: stack.GitConfig.TLSSkipVerify,
|
||||
}
|
||||
|
||||
@@ -178,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, repositoryAuthType, stack.GitConfig.TLSSkipVerify)
|
||||
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, 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))
|
||||
}
|
||||
|
||||
@@ -27,13 +27,12 @@ type kubernetesFileStackUpdatePayload struct {
|
||||
}
|
||||
|
||||
type kubernetesGitStackUpdatePayload struct {
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
RepositoryAuthorizationType gittypes.GitCredentialAuthType
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
TLSSkipVerify bool
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -77,9 +76,8 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
}
|
||||
|
||||
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
AuthorizationType: payload.RepositoryAuthorizationType,
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if _, err := handler.GitService.LatestCommitID(
|
||||
@@ -87,7 +85,6 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
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)
|
||||
|
||||
@@ -5,7 +5,6 @@ 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"
|
||||
@@ -78,7 +77,6 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
); err != nil {
|
||||
return httperror.InternalServerError("Unable to clone git repository", err)
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/docker/client"
|
||||
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"
|
||||
@@ -449,7 +448,6 @@ func (transport *Transport) updateDefaultGitBranch(request *http.Request) error
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
gittypes.GitCredentialAuthType_Basic,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package testhelpers
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
)
|
||||
|
||||
type gitService struct {
|
||||
@@ -24,7 +23,6 @@ func (g *gitService) CloneRepository(
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
tlsSkipVerify bool,
|
||||
) error {
|
||||
return g.cloneErr
|
||||
@@ -35,7 +33,6 @@ func (g *gitService) LatestCommitID(
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
tlsSkipVerify bool,
|
||||
) (string, error) {
|
||||
return g.id, nil
|
||||
@@ -45,7 +42,6 @@ func (g *gitService) ListRefs(
|
||||
repositoryURL,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
hardRefresh bool,
|
||||
tlsSkipVerify bool,
|
||||
) ([]string, error) {
|
||||
@@ -57,7 +53,6 @@ func (g *gitService) ListFiles(
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
dirOnly,
|
||||
hardRefresh bool,
|
||||
includedExts []string,
|
||||
|
||||
@@ -1647,7 +1647,6 @@ type (
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
tlsSkipVerify bool,
|
||||
) error
|
||||
LatestCommitID(
|
||||
@@ -1655,14 +1654,12 @@ type (
|
||||
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)
|
||||
@@ -1671,7 +1668,6 @@ type (
|
||||
referenceName,
|
||||
username,
|
||||
password string,
|
||||
authType gittypes.GitCredentialAuthType,
|
||||
dirOnly,
|
||||
hardRefresh bool,
|
||||
includeExts []string,
|
||||
|
||||
@@ -19,11 +19,9 @@ 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()
|
||||
@@ -33,7 +31,6 @@ func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitS
|
||||
config.ReferenceName,
|
||||
username,
|
||||
password,
|
||||
authType,
|
||||
config.TLSSkipVerify,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -51,7 +48,6 @@ func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitS
|
||||
config.ReferenceName,
|
||||
username,
|
||||
password,
|
||||
authType,
|
||||
config.TLSSkipVerify,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
)
|
||||
|
||||
type InstallOptions struct {
|
||||
@@ -30,7 +29,6 @@ type InstallOptions struct {
|
||||
CreateNamespace bool
|
||||
|
||||
// GitOps related options
|
||||
GitConfig *gittypes.RepoConfig
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
|
||||
// StackID is the ID of the Portainer stack associated with this release
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/pkg/libhelm/options"
|
||||
"github.com/portainer/portainer/pkg/libhelm/release"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -195,7 +194,7 @@ func ensureHelmDirectoriesExist(settings *cli.EnvSettings) error {
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
log.Error().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Str("directory", dir).
|
||||
@@ -211,7 +210,7 @@ func ensureHelmDirectoriesExist(settings *cli.EnvSettings) error {
|
||||
if _, err := os.Stat(settings.RegistryConfig); os.IsNotExist(err) {
|
||||
// Create the directory if it doesn't exist
|
||||
dir := filepath.Dir(settings.RegistryConfig)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
log.Error().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Str("directory", dir).
|
||||
@@ -237,7 +236,7 @@ func ensureHelmDirectoriesExist(settings *cli.EnvSettings) error {
|
||||
if _, err := os.Stat(settings.RepositoryConfig); os.IsNotExist(err) {
|
||||
// Create an empty repository config file with default yaml structure
|
||||
f := repo.NewFile()
|
||||
if err := f.WriteFile(settings.RepositoryConfig, 0644); err != nil {
|
||||
if err := f.WriteFile(settings.RepositoryConfig, 0o644); err != nil {
|
||||
log.Error().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Str("file", settings.RepositoryConfig).
|
||||
@@ -258,7 +257,7 @@ func ensureHelmDirectoriesExist(settings *cli.EnvSettings) error {
|
||||
// appendChartReferenceAnnotations encodes chart reference values for safe storage in Helm labels.
|
||||
// It creates a new map with encoded values for specific chart reference labels.
|
||||
// Preserves existing labels and handles edge cases gracefully.
|
||||
func appendChartReferenceAnnotations(chartPath, repoURL string, registryID int, stackID int, gitConfig *gittypes.RepoConfig, autoUpdateSettings *portainer.AutoUpdateSettings, existingAnnotations map[string]string) map[string]string {
|
||||
func appendChartReferenceAnnotations(chartPath, repoURL string, registryID int, stackID int, autoUpdateSettings *portainer.AutoUpdateSettings, existingAnnotations map[string]string) map[string]string {
|
||||
// Copy existing annotations
|
||||
annotations := make(map[string]string)
|
||||
maps.Copy(annotations, existingAnnotations)
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestAppendChartReferenceAnnotations(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := appendChartReferenceAnnotations(
|
||||
tt.chartPath, tt.repoURL, tt.registryID, tt.stackID,
|
||||
nil, nil, tt.existing,
|
||||
nil, tt.existing,
|
||||
)
|
||||
|
||||
assert.Equal(t, tt.want, result)
|
||||
@@ -85,18 +85,18 @@ func TestAppendChartReferenceAnnotations(t *testing.T) {
|
||||
func TestAppendChartReferenceAnnotations_RepoURLLogic(t *testing.T) {
|
||||
t.Run("repoURL only added when registryID is zero", func(t *testing.T) {
|
||||
// With registry ID - no repoURL
|
||||
result := appendChartReferenceAnnotations("chart", "url", 5, 0, nil, nil, nil)
|
||||
result := appendChartReferenceAnnotations("chart", "url", 5, 0, nil, nil)
|
||||
_, hasRepoURL := result[RepoURLAnnotation]
|
||||
assert.False(t, hasRepoURL)
|
||||
|
||||
// Without registry ID - includes repoURL
|
||||
result = appendChartReferenceAnnotations("chart", "url", 0, 0, nil, nil, nil)
|
||||
result = appendChartReferenceAnnotations("chart", "url", 0, 0, nil, nil)
|
||||
assert.Equal(t, "url", result[RepoURLAnnotation])
|
||||
})
|
||||
|
||||
t.Run("does not mutate existing map", func(t *testing.T) {
|
||||
existing := map[string]string{"key": "value"}
|
||||
appendChartReferenceAnnotations("chart", "", 0, 0, nil, nil, existing)
|
||||
appendChartReferenceAnnotations("chart", "", 0, 0, nil, existing)
|
||||
assert.Equal(t, map[string]string{"key": "value"}, existing)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (hspm *HelmSDKPackageManager) install(installOpts options.InstallOptions) (
|
||||
if installOpts.Registry != nil {
|
||||
registryID = int(installOpts.Registry.ID)
|
||||
}
|
||||
chart.Metadata.Annotations = appendChartReferenceAnnotations(installOpts.Chart, installOpts.Repo, registryID, installOpts.StackID, installOpts.GitConfig, installOpts.AutoUpdate, chart.Metadata.Annotations)
|
||||
chart.Metadata.Annotations = appendChartReferenceAnnotations(installOpts.Chart, installOpts.Repo, registryID, installOpts.StackID, installOpts.AutoUpdate, chart.Metadata.Annotations)
|
||||
|
||||
// Run the installation
|
||||
log.Info().
|
||||
|
||||
@@ -124,7 +124,7 @@ func (hspm *HelmSDKPackageManager) Upgrade(upgradeOpts options.InstallOptions) (
|
||||
if upgradeOpts.Registry != nil {
|
||||
registryID = int(upgradeOpts.Registry.ID)
|
||||
}
|
||||
chart.Metadata.Annotations = appendChartReferenceAnnotations(upgradeOpts.Chart, upgradeOpts.Repo, registryID, upgradeOpts.StackID, upgradeOpts.GitConfig, upgradeOpts.AutoUpdate, chart.Metadata.Annotations)
|
||||
chart.Metadata.Annotations = appendChartReferenceAnnotations(upgradeOpts.Chart, upgradeOpts.Repo, registryID, upgradeOpts.StackID, upgradeOpts.AutoUpdate, chart.Metadata.Annotations)
|
||||
|
||||
log.Info().
|
||||
Str("context", "HelmClient").
|
||||
|
||||
Reference in New Issue
Block a user