feat(gitops): allow to skip tls verification [EE-5023] (#8679)

pull/8736/head
Chaim Lev-Ari 2023-04-03 09:19:09 +03:00 committed by GitHub
parent ab1a8c1d6a
commit 99331a81d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 267 additions and 188 deletions

View File

@ -15,8 +15,6 @@ import (
"github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/crypto"
gittypes "github.com/portainer/portainer/api/git/types" gittypes "github.com/portainer/portainer/api/git/types"
"github.com/go-git/go-git/v5/plumbing/transport/client"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -51,21 +49,22 @@ type azureItem struct {
} }
type azureClient struct { type azureClient struct {
client *http.Client
baseUrl string baseUrl string
} }
func NewAzureClient() *azureClient { func NewAzureClient() *azureClient {
httpsCli := newHttpClientForAzure()
return &azureClient{ return &azureClient{
client: httpsCli,
baseUrl: "https://dev.azure.com", baseUrl: "https://dev.azure.com",
} }
} }
func newHttpClientForAzure() *http.Client { func newHttpClientForAzure(insecureSkipVerify bool) *http.Client {
tlsConfig := crypto.CreateTLSConfiguration() tlsConfig := crypto.CreateTLSConfiguration()
if insecureSkipVerify {
tlsConfig.InsecureSkipVerify = true
}
httpsCli := &http.Client{ httpsCli := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
@ -74,7 +73,6 @@ func newHttpClientForAzure() *http.Client {
Timeout: 300 * time.Second, Timeout: 300 * time.Second,
} }
client.InstallProtocol("https", githttp.NewClient(httpsCli))
return httpsCli return httpsCli
} }
@ -106,6 +104,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
if err != nil { if err != nil {
return "", errors.WithMessage(err, "failed to create temp file") return "", errors.WithMessage(err, "failed to create temp file")
} }
defer zipFile.Close() defer zipFile.Close()
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil) req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
@ -119,10 +118,14 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
return "", errors.WithMessage(err, "failed to create a new HTTP request") return "", errors.WithMessage(err, "failed to create a new HTTP request")
} }
res, err := a.client.Do(req) client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
res, err := client.Do(req)
if err != nil { if err != nil {
return "", errors.WithMessage(err, "failed to make an HTTP request") return "", errors.WithMessage(err, "failed to make an HTTP request")
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
@ -166,7 +169,10 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
return nil, errors.WithMessage(err, "failed to create a new HTTP request") return nil, errors.WithMessage(err, "failed to create a new HTTP request")
} }
resp, err := a.client.Do(req) client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request") return nil, errors.WithMessage(err, "failed to make an HTTP request")
} }
@ -399,7 +405,10 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
return nil, errors.WithMessage(err, "failed to create a new HTTP request") return nil, errors.WithMessage(err, "failed to create a new HTTP request")
} }
resp, err := a.client.Do(req) client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request") return nil, errors.WithMessage(err, "failed to make an HTTP request")
} }
@ -456,7 +465,10 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
return nil, errors.WithMessage(err, "failed to create a new HTTP request") return nil, errors.WithMessage(err, "failed to create a new HTTP request")
} }
resp, err := a.client.Do(req) client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request") return nil, errors.WithMessage(err, "failed to make an HTTP request")
} }

View File

@ -59,7 +59,7 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
dst := t.TempDir() dst := t.TempDir()
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password) repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "") err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "", false)
assert.NoError(t, err) assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md")) assert.FileExists(t, filepath.Join(dst, "README.md"))
}) })
@ -74,7 +74,7 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
dst := t.TempDir() dst := t.TempDir()
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat) err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md")) assert.FileExists(t, filepath.Join(dst, "README.md"))
} }
@ -85,7 +85,7 @@ func TestService_LatestCommitID_Azure(t *testing.T) {
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT") pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService(context.TODO()) service := NewService(context.TODO())
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat) id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty") assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
} }
@ -97,7 +97,7 @@ func TestService_ListRefs_Azure(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := NewService(context.TODO()) service := NewService(context.TODO())
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false) refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) assert.GreaterOrEqual(t, len(refs), 1)
} }
@ -109,8 +109,8 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond) service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
go service.ListRefs(privateAzureRepoURL, username, accessToken, false) go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, false) service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
@ -248,7 +248,7 @@ func TestService_ListFiles_Azure(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions) paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions, false)
if tt.expect.shouldFail { if tt.expect.shouldFail {
assert.Error(t, err) assert.Error(t, err)
if tt.expect.err != nil { if tt.expect.err != nil {
@ -271,8 +271,8 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond) service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}) go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}) service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }

View File

@ -292,7 +292,6 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
defer server.Close() defer server.Close()
a := &azureClient{ a := &azureClient{
client: server.Client(),
baseUrl: server.URL, baseUrl: server.URL,
} }
@ -329,7 +328,6 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
defer server.Close() defer server.Close()
a := &azureClient{ a := &azureClient{
client: server.Client(),
baseUrl: server.URL, baseUrl: server.URL,
} }
@ -442,6 +440,7 @@ func Test_listRefs_azure(t *testing.T) {
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT") accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME") username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
tests := []struct { tests := []struct {
name string name string
args baseOption args baseOption

View File

@ -20,6 +20,8 @@ type CloneOptions struct {
ReferenceName string ReferenceName string
Username string Username string
Password string Password string
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }
func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) { func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) {
@ -43,7 +45,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
cleanUp = true cleanUp = true
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password) err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify)
if err != nil { if err != nil {
cleanUp = false cleanUp = false
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath) restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)

View File

@ -30,6 +30,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
gitOptions := git.CloneOptions{ gitOptions := git.CloneOptions{
URL: opt.repositoryUrl, URL: opt.repositoryUrl,
Depth: opt.depth, Depth: opt.depth,
InsecureSkipTLS: opt.tlsSkipVerify,
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.username, opt.password),
} }
@ -61,6 +62,7 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
listOptions := &git.ListOptions{ listOptions := &git.ListOptions{
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
} }
refs, err := remote.List(listOptions) refs, err := remote.List(listOptions)
@ -111,6 +113,7 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
listOptions := &git.ListOptions{ listOptions := &git.ListOptions{
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
} }
refs, err := rem.List(listOptions) refs, err := rem.List(listOptions)
@ -138,6 +141,7 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
SingleBranch: true, SingleBranch: true,
ReferenceName: plumbing.ReferenceName(opt.referenceName), ReferenceName: plumbing.ReferenceName(opt.referenceName),
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
} }
repo, err := git.Clone(memory.NewStorage(), nil, cloneOption) repo, err := git.Clone(memory.NewStorage(), nil, cloneOption)

View File

@ -24,7 +24,7 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
dst := t.TempDir() dst := t.TempDir()
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken) err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md")) assert.FileExists(t, filepath.Join(dst, "README.md"))
} }
@ -37,7 +37,7 @@ func TestService_LatestCommitID_GitHub(t *testing.T) {
service := newService(context.TODO(), 0, 0) service := newService(context.TODO(), 0, 0)
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken) id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty") assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
} }
@ -50,7 +50,7 @@ func TestService_ListRefs_GitHub(t *testing.T) {
service := newService(context.TODO(), 0, 0) service := newService(context.TODO(), 0, 0)
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false) refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) assert.GreaterOrEqual(t, len(refs), 1)
} }
@ -63,8 +63,8 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond) service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
go service.ListRefs(repositoryUrl, username, accessToken, false) go service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListRefs(repositoryUrl, username, accessToken, false) service.ListRefs(repositoryUrl, username, accessToken, false, false)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
@ -202,7 +202,7 @@ func TestService_ListFiles_GitHub(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions) paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions, false)
if tt.expect.shouldFail { if tt.expect.shouldFail {
assert.Error(t, err) assert.Error(t, err)
if tt.expect.err != nil { if tt.expect.err != nil {
@ -226,8 +226,8 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME") username := getRequiredValue(t, "GITHUB_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond) service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}) go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}) service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
@ -240,8 +240,8 @@ func TestService_purgeCache_Github(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME") username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService(context.TODO()) service := NewService(context.TODO())
service.ListRefs(repositoryUrl, username, accessToken, false) service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}) service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.Len()) assert.Equal(t, 1, service.repoFileCache.Len())
@ -261,8 +261,8 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) {
// 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result // 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result
service := newService(context.TODO(), 2, 40*timeout) service := newService(context.TODO(), 2, 40*timeout)
service.ListRefs(repositoryUrl, username, accessToken, false) service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}) service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.Len()) assert.Equal(t, 1, service.repoFileCache.Len())
@ -293,12 +293,12 @@ func TestService_HardRefresh_ListRefs_GitHub(t *testing.T) {
service := newService(context.TODO(), 2, 0) service := newService(context.TODO(), 2, 0)
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false) refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) assert.GreaterOrEqual(t, len(refs), 1)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", false) _, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
} }
@ -311,26 +311,26 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) {
service := newService(context.TODO(), 2, 0) service := newService(context.TODO(), 2, 0)
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false) refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1) assert.GreaterOrEqual(t, len(refs), 1)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}) files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1) assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 1, service.repoFileCache.Len()) assert.Equal(t, 1, service.repoFileCache.Len())
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{}) files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{}, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1) assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 2, service.repoFileCache.Len()) assert.Equal(t, 2, service.repoFileCache.Len())
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", false) _, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", true) _, err = service.ListRefs(repositoryUrl, username, "fake-token", true, false)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len()) assert.Equal(t, 1, service.repoRefCache.Len())
// The relevant file caches should be removed too // The relevant file caches should be removed too
@ -344,12 +344,12 @@ func TestService_HardRefresh_ListFiles_GitHub(t *testing.T) {
accessToken := getRequiredValue(t, "GITHUB_PAT") accessToken := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME") username := getRequiredValue(t, "GITHUB_USERNAME")
repositoryUrl := privateGitRepoURL repositoryUrl := privateGitRepoURL
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}) files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1) assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 1, service.repoFileCache.Len()) assert.Equal(t, 1, service.repoFileCache.Len())
files, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", true, []string{}) _, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", true, []string{}, false)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, 0, service.repoFileCache.Len()) assert.Equal(t, 0, service.repoFileCache.Len())
} }

View File

@ -38,7 +38,7 @@ func Test_ClonePublicRepository_Shallow(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
t.Logf("Cloning into %s", dir) t.Logf("Cloning into %s", dir)
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "") err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth") assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
} }
@ -50,7 +50,7 @@ func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
t.Logf("Cloning into %s", dir) t.Logf("Cloning into %s", dir)
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "") err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
assert.NoError(t, err) assert.NoError(t, err)
assert.NoDirExists(t, filepath.Join(dir, ".git")) assert.NoDirExists(t, filepath.Join(dir, ".git"))
} }
@ -84,7 +84,7 @@ func Test_latestCommitID(t *testing.T) {
repositoryURL := setup(t) repositoryURL := setup(t)
referenceName := "refs/heads/main" referenceName := "refs/heads/main"
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "") id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", false)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id) assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)

View File

@ -2,6 +2,7 @@ package git
import ( import (
"context" "context"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -20,6 +21,7 @@ type baseOption struct {
repositoryUrl string repositoryUrl string
username string username string
password string password string
tlsSkipVerify bool
} }
// fetchOption allows to specify the reference name of the target repository // fetchOption allows to specify the reference name of the target repository
@ -119,13 +121,14 @@ func (service *Service) timerHasStopped() bool {
// CloneRepository clones a git repository using the specified URL in the specified // CloneRepository clones a git repository using the specified URL in the specified
// destination folder. // destination folder.
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string) error { func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error {
options := cloneOption{ options := cloneOption{
fetchOption: fetchOption{ fetchOption: fetchOption{
baseOption: baseOption{ baseOption: baseOption{
repositoryUrl: repositoryURL, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
tlsSkipVerify: tlsSkipVerify,
}, },
referenceName: referenceName, referenceName: referenceName,
}, },
@ -144,12 +147,13 @@ func (service *Service) cloneRepository(destination string, options cloneOption)
} }
// LatestCommitID returns SHA1 of the latest commit of the specified reference // LatestCommitID returns SHA1 of the latest commit of the specified reference
func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) { func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
options := fetchOption{ options := fetchOption{
baseOption: baseOption{ baseOption: baseOption{
repositoryUrl: repositoryURL, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
tlsSkipVerify: tlsSkipVerify,
}, },
referenceName: referenceName, referenceName: referenceName,
} }
@ -162,8 +166,8 @@ func (service *Service) LatestCommitID(repositoryURL, referenceName, username, p
} }
// ListRefs will list target repository's references without cloning the repository // ListRefs will list target repository's references without cloning the repository
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) { func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
refCacheKey := generateCacheKey(repositoryURL, password) refCacheKey := generateCacheKey(repositoryURL, username, password, strconv.FormatBool(tlsSkipVerify))
if service.cacheEnabled && hardRefresh { if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result // Should remove the cache explicitly, so that the following normal list can show the correct result
service.repoRefCache.Remove(refCacheKey) service.repoRefCache.Remove(refCacheKey)
@ -193,6 +197,7 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
repositoryUrl: repositoryURL, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
tlsSkipVerify: tlsSkipVerify,
} }
var ( var (
@ -219,8 +224,8 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
// ListFiles will list all the files of the target repository with specific extensions. // ListFiles will list all the files of the target repository with specific extensions.
// If extension is not provided, it will list all the files under the target repository // If extension is not provided, it will list all the files under the target repository
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) { func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName) repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify))
if service.cacheEnabled && hardRefresh { if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result // Should remove the cache explicitly, so that the following normal list can show the correct result
@ -246,6 +251,7 @@ func (service *Service) ListFiles(repositoryURL, referenceName, username, passwo
repositoryUrl: repositoryURL, repositoryUrl: repositoryURL,
username: username, username: username,
password: password, password: password,
tlsSkipVerify: tlsSkipVerify,
}, },
referenceName: referenceName, referenceName: referenceName,
} }

View File

@ -3,8 +3,8 @@ package gittypes
import "errors" import "errors"
var ( var (
ErrIncorrectRepositoryURL = errors.New("Git repository could not be found, please ensure that the URL is correct.") ErrIncorrectRepositoryURL = errors.New("git repository could not be found, please ensure that the URL is correct")
ErrAuthenticationFailure = errors.New("Authentication failed, please ensure that the git credentials are correct.") ErrAuthenticationFailure = errors.New("authentication failed, please ensure that the git credentials are correct")
) )
// RepoConfig represents a configuration for a repo // RepoConfig represents a configuration for a repo
@ -19,6 +19,8 @@ type RepoConfig struct {
Authentication *GitAuthentication Authentication *GitAuthentication
// Repository hash // Repository hash
ConfigHash string `example:"bc4c183d756879ea4d173315338110b31004b8e0"` ConfigHash string `example:"bc4c183d756879ea4d173315338110b31004b8e0"`
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }
type GitAuthentication struct { type GitAuthentication struct {

View File

@ -6,14 +6,13 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/git" "github.com/portainer/portainer/api/git"
gittypes "github.com/portainer/portainer/api/git/types" gittypes "github.com/portainer/portainer/api/git/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// UpdateGitObject updates a git object based on its config // UpdateGitObject updates a git object based on its config
func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.DataStore, objId string, gitConfig *gittypes.RepoConfig, autoUpdateConfig *portainer.AutoUpdateSettings, projectPath string) (bool, string, error) { func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *gittypes.RepoConfig, forceUpdate bool, projectPath string) (bool, string, error) {
if gitConfig == nil { if gitConfig == nil {
return false, "", nil return false, "", nil
} }
@ -29,13 +28,13 @@ func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.Dat
return false, "", errors.WithMessagef(err, "failed to get credentials for %v", objId) return false, "", errors.WithMessagef(err, "failed to get credentials for %v", objId)
} }
newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password) newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password, gitConfig.TLSSkipVerify)
if err != nil { if err != nil {
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId) return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
} }
hashChanged := !strings.EqualFold(newHash, string(gitConfig.ConfigHash)) hashChanged := !strings.EqualFold(newHash, gitConfig.ConfigHash)
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
if !hashChanged && !forceUpdate { if !hashChanged && !forceUpdate {
log.Debug(). log.Debug().
Str("hash", newHash). Str("hash", newHash).
@ -51,6 +50,7 @@ func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.Dat
url: gitConfig.URL, url: gitConfig.URL,
ref: gitConfig.ReferenceName, ref: gitConfig.ReferenceName,
toDir: projectPath, toDir: projectPath,
tlsSkipVerify: gitConfig.TLSSkipVerify,
} }
if gitConfig.Authentication != nil { if gitConfig.Authentication != nil {
cloneParams.auth = &gitAuth{ cloneParams.auth = &gitAuth{
@ -78,6 +78,8 @@ type cloneRepositoryParameters struct {
ref string ref string
toDir string toDir string
auth *gitAuth auth *gitAuth
// tlsSkipVerify skips SSL verification when cloning the Git repository
tlsSkipVerify bool `example:"false"`
} }
type gitAuth struct { type gitAuth struct {
@ -87,8 +89,8 @@ type gitAuth struct {
func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error { func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error {
if cloneParams.auth != nil { if cloneParams.auth != nil {
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password) return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password, cloneParams.tlsSkipVerify)
} }
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "") return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "", cloneParams.tlsSkipVerify)
} }

View File

@ -213,6 +213,8 @@ type customTemplateFromGitRepositoryPayload struct {
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
// Definitions of variables in the stack file // Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition Variables []portainer.CustomTemplateVariableDefinition
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error { func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -279,7 +281,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
repositoryPassword = "" repositoryPassword = ""
} }
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword) err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword, payload.TLSSkipVerify)
if err != nil { if err != nil {
if err == gittypes.ErrAuthenticationFailure { if err == gittypes.ErrAuthenticationFailure {
return nil, fmt.Errorf("invalid git credential") return nil, fmt.Errorf("invalid git credential")

View File

@ -201,6 +201,8 @@ type swarmStackFromGitRepositoryPayload struct {
Registries []portainer.RegistryID Registries []portainer.RegistryID
// Uses the manifest's namespaces instead of the default one // Uses the manifest's namespaces instead of the default one
UseManifestNamespaces bool UseManifestNamespaces bool
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -247,6 +249,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request, dryru
URL: payload.RepositoryURL, URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName, ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.FilePathInRepository, ConfigFilePath: payload.FilePathInRepository,
TLSSkipVerify: payload.TLSSkipVerify,
} }
if payload.RepositoryAuthentication { if payload.RepositoryAuthentication {
@ -345,7 +348,7 @@ func (handler *Handler) storeManifestFromGitRepository(stackFolder string, relat
repositoryPassword = repositoryConfig.Authentication.Password repositoryPassword = repositoryConfig.Authentication.Password
} }
err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword) err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }

View File

@ -18,32 +18,12 @@ import (
"github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/edge/edgestacks" "github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt" "github.com/portainer/portainer/api/jwt"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type gitService struct {
cloneErr error
id string
}
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
return g.cloneErr
}
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
return g.id, nil
}
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
return nil, nil
}
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
return nil, nil
}
// Helpers // Helpers
func setupHandler(t *testing.T) (*Handler, string, func()) { func setupHandler(t *testing.T) (*Handler, string, func()) {
t.Helper() t.Helper()
@ -98,7 +78,7 @@ func setupHandler(t *testing.T) (*Handler, string, func()) {
t.Fatal(err) t.Fatal(err)
} }
handler.GitService = &gitService{errors.New("Clone error"), "git-service-id"} handler.GitService = testhelpers.NewGitService(errors.New("Clone error"), "git-service-id")
return handler, rawAPIKey, storeTeardown return handler, rawAPIKey, storeTeardown
} }

View File

@ -162,9 +162,11 @@ type composeStackFromGitRepositoryPayload struct {
Env []portainer.Pair Env []portainer.Pair
// Whether the stack is from a app template // Whether the stack is from a app template
FromAppTemplate bool `example:"false"` FromAppTemplate bool `example:"false"`
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload { func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool, repoSkipSSLVerify bool) stackbuilders.StackPayload {
return stackbuilders.StackPayload{ return stackbuilders.StackPayload{
Name: name, Name: name,
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{ RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
@ -173,6 +175,7 @@ func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoU
Authentication: repoAuthentication, Authentication: repoAuthentication,
Username: repoUsername, Username: repoUsername,
Password: repoPassword, Password: repoPassword,
TLSSkipVerify: repoSkipSSLVerify,
}, },
ComposeFile: composeFile, ComposeFile: composeFile,
AdditionalFiles: additionalFiles, AdditionalFiles: additionalFiles,
@ -258,7 +261,9 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
payload.AdditionalFiles, payload.AdditionalFiles,
payload.AutoUpdate, payload.AutoUpdate,
payload.Env, payload.Env,
payload.FromAppTemplate) payload.FromAppTemplate,
payload.TLSSkipVerify,
)
composeStackBuilder := stackbuilders.CreateComposeStackGitBuilder(securityContext, composeStackBuilder := stackbuilders.CreateComposeStackGitBuilder(securityContext,
handler.DataStore, handler.DataStore,

View File

@ -46,9 +46,11 @@ type kubernetesGitDeploymentPayload struct {
ManifestFile string ManifestFile string
AdditionalFiles []string AdditionalFiles []string
AutoUpdate *portainer.AutoUpdateSettings AutoUpdate *portainer.AutoUpdateSettings
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings) stackbuilders.StackPayload { func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, repoSkipSSLVerify bool) stackbuilders.StackPayload {
return stackbuilders.StackPayload{ return stackbuilders.StackPayload{
StackName: name, StackName: name,
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{ RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
@ -57,6 +59,7 @@ func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsern
Authentication: repoAuthentication, Authentication: repoAuthentication,
Username: repoUsername, Username: repoUsername,
Password: repoPassword, Password: repoPassword,
TLSSkipVerify: repoSkipSSLVerify,
}, },
Namespace: namespace, Namespace: namespace,
ComposeFormat: composeFormat, ComposeFormat: composeFormat,
@ -203,7 +206,9 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
payload.Namespace, payload.Namespace,
payload.ManifestFile, payload.ManifestFile,
payload.AdditionalFiles, payload.AdditionalFiles,
payload.AutoUpdate) payload.AutoUpdate,
payload.TLSSkipVerify,
)
k8sStackBuilder := stackbuilders.CreateKubernetesStackGitBuilder(handler.DataStore, k8sStackBuilder := stackbuilders.CreateKubernetesStackGitBuilder(handler.DataStore,
handler.FileService, handler.FileService,

View File

@ -117,6 +117,8 @@ type swarmStackFromGitRepositoryPayload struct {
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"` AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
// Optional auto update configuration // Optional auto update configuration
AutoUpdate *portainer.AutoUpdateSettings AutoUpdate *portainer.AutoUpdateSettings
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -138,7 +140,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
return nil return nil
} }
func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload { func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool, repoSkipSSLVerify bool) stackbuilders.StackPayload {
return stackbuilders.StackPayload{ return stackbuilders.StackPayload{
Name: name, Name: name,
SwarmID: swarmID, SwarmID: swarmID,
@ -201,7 +203,9 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
payload.AdditionalFiles, payload.AdditionalFiles,
payload.AutoUpdate, payload.AutoUpdate,
payload.Env, payload.Env,
payload.FromAppTemplate) payload.FromAppTemplate,
payload.TLSSkipVerify,
)
swarmStackBuilder := stackbuilders.CreateSwarmStackGitBuilder(securityContext, swarmStackBuilder := stackbuilders.CreateSwarmStackGitBuilder(securityContext,
handler.DataStore, handler.DataStore,

View File

@ -158,7 +158,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
Username: payload.RepositoryUsername, Username: payload.RepositoryUsername,
Password: password, Password: password,
} }
_, err = handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password) _, err = handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to fetch git repository", err) return httperror.InternalServerError("Unable to fetch git repository", err)
} }

View File

@ -145,7 +145,16 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
repositoryUsername = payload.RepositoryUsername repositoryUsername = payload.RepositoryUsername
} }
clean, err := git.CloneWithBackup(handler.GitService, handler.FileService, git.CloneOptions{ProjectPath: stack.ProjectPath, URL: stack.GitConfig.URL, ReferenceName: stack.GitConfig.ReferenceName, Username: repositoryUsername, Password: repositoryPassword}) cloneOptions := git.CloneOptions{
ProjectPath: stack.ProjectPath,
URL: stack.GitConfig.URL,
ReferenceName: stack.GitConfig.ReferenceName,
Username: repositoryUsername,
Password: repositoryPassword,
TLSSkipVerify: stack.GitConfig.TLSSkipVerify,
}
clean, err := git.CloneWithBackup(handler.GitService, handler.FileService, cloneOptions)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to clone git repository directory", err) return httperror.InternalServerError("Unable to clone git repository directory", err)
} }
@ -157,7 +166,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
return httpErr return httpErr
} }
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword) newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, stack.GitConfig.TLSSkipVerify)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable get latest commit id", errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID)) return httperror.InternalServerError("Unable get latest commit id", errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID))
} }

View File

@ -73,7 +73,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
Username: payload.RepositoryUsername, Username: payload.RepositoryUsername,
Password: password, Password: password,
} }
_, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password) _, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to fetch git repository", err) return httperror.InternalServerError("Unable to fetch git repository", err)
} }

View File

@ -98,7 +98,7 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
defer handler.cleanUp(projectPath) defer handler.cleanUp(projectPath)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "") err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "", false)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to clone git repository", err) return httperror.InternalServerError("Unable to clone git repository", err)
} }

View File

@ -395,7 +395,7 @@ func (transport *Transport) updateDefaultGitBranch(request *http.Request) error
remote := request.URL.Query().Get("remote") remote := request.URL.Query().Get("remote")
if strings.HasSuffix(remote, ".git") { if strings.HasSuffix(remote, ".git") {
repositoryURL := remote[:len(remote)-4] repositoryURL := remote[:len(remote)-4]
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "") latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "", false)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,29 +1,16 @@
package docker package docker
import ( import (
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type noopGitService struct{}
func (s *noopGitService) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
return nil
}
func (s *noopGitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
return "my-latest-commit-id", nil
}
func (g *noopGitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
return nil, nil
}
func (g *noopGitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
return nil, nil
}
func TestTransport_updateDefaultGitBranch(t *testing.T) { func TestTransport_updateDefaultGitBranch(t *testing.T) {
type fields struct { type fields struct {
gitService portainer.GitService gitService portainer.GitService
@ -33,8 +20,10 @@ func TestTransport_updateDefaultGitBranch(t *testing.T) {
request *http.Request request *http.Request
} }
commitId := "my-latest-commit-id"
defaultFields := fields{ defaultFields := fields{
gitService: &noopGitService{}, gitService: testhelpers.NewGitService(nil, commitId),
} }
tests := []struct { tests := []struct {
@ -51,7 +40,7 @@ func TestTransport_updateDefaultGitBranch(t *testing.T) {
request: httptest.NewRequest(http.MethodPost, "http://unixsocket/build?dockerfile=Dockerfile&remote=https://my-host.com/my-user/my-repo.git&t=my-image", nil), request: httptest.NewRequest(http.MethodPost, "http://unixsocket/build?dockerfile=Dockerfile&remote=https://my-host.com/my-user/my-repo.git&t=my-image", nil),
}, },
wantErr: false, wantErr: false,
expectedQuery: "dockerfile=Dockerfile&remote=https%3A%2F%2Fmy-host.com%2Fmy-user%2Fmy-repo.git%23my-latest-commit-id&t=my-image", expectedQuery: fmt.Sprintf("dockerfile=Dockerfile&remote=https%%3A%%2F%%2Fmy-host.com%%2Fmy-user%%2Fmy-repo.git%%23%s&t=my-image", commitId),
}, },
{ {
name: "not append commit ID", name: "not append commit ID",

View File

@ -1,12 +1,32 @@
package testhelpers package testhelpers
type gitService struct{} import portainer "github.com/portainer/portainer/api"
type gitService struct {
cloneErr error
id string
}
// NewGitService creates new mock for portainer.GitService. // NewGitService creates new mock for portainer.GitService.
func NewGitService() *gitService { func NewGitService(cloneErr error, id string) portainer.GitService {
return &gitService{} return &gitService{
cloneErr: cloneErr,
id: id,
}
} }
func (service *gitService) CloneRepository(destination string, repositoryURL, referenceName string, username, password string) error { func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error {
return nil return g.cloneErr
}
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
return g.id, nil
}
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
return nil, nil
}
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
return nil, nil
} }

View File

@ -1393,10 +1393,10 @@ type (
// GitService represents a service for managing Git // GitService represents a service for managing Git
GitService interface { GitService interface {
CloneRepository(destination string, repositoryURL, referenceName, username, password string) error CloneRepository(destination string, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error
LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error)
ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error)
ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includeExts []string) ([]string, error) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includeExts []string, tlsSkipVerify bool) ([]string, error)
} }
// OpenAMTService represents a service for managing OpenAMT // OpenAMTService represents a service for managing OpenAMT

View File

@ -53,14 +53,14 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
Str("author", author). Str("author", author).
Str("stack", stack.Name). Str("stack", stack.Name).
Int("endpoint_id", int(stack.EndpointID)). Int("endpoint_id", int(stack.EndpointID)).
Msg("cannot autoupdate a stack, stack author user is missing") Msg("cannot auto update a stack, stack author user is missing")
return &StackAuthorMissingErr{int(stack.ID), author} return &StackAuthorMissingErr{int(stack.ID), author}
} }
var gitCommitChangedOrForceUpdate bool var gitCommitChangedOrForceUpdate bool
if !stack.FromAppTemplate { if !stack.FromAppTemplate {
updated, newHash, err := update.UpdateGitObject(gitService, datastore, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, stack.AutoUpdate, stack.ProjectPath) updated, newHash, err := update.UpdateGitObject(gitService, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, false, stack.ProjectPath)
if err != nil { if err != nil {
return err return err
} }
@ -99,7 +99,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
err := deployer.DeployKubernetesStack(stack, endpoint, user) err := deployer.DeployKubernetesStack(stack, endpoint, user)
if err != nil { if err != nil {
return errors.WithMessagef(err, "failed to deploy a kubternetes app stack %v", stackID) return errors.WithMessagef(err, "failed to deploy a kubernetes app stack %v", stackID)
} }
default: default:
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type) return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)

View File

@ -6,33 +6,13 @@ import (
"testing" "testing"
"github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types" gittypes "github.com/portainer/portainer/api/git/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type gitService struct {
cloneErr error
id string
}
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
return g.cloneErr
}
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
return g.id, nil
}
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
return nil, nil
}
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
return nil, nil
}
type noopDeployer struct{} type noopDeployer struct{}
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error { func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
@ -67,7 +47,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNotAGitBasedStack(t *testing.T) {
err = store.Stack().Create(&portainer.Stack{ID: 1, CreatedBy: "admin"}) err = store.Stack().Create(&portainer.Stack{ID: 1, CreatedBy: "admin"})
assert.NoError(t, err, "failed to create a test stack") assert.NoError(t, err, "failed to create a test stack")
err = RedeployWhenChanged(1, nil, store, &gitService{nil, ""}) err = RedeployWhenChanged(1, nil, store, testhelpers.NewGitService(nil, ""))
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -97,7 +77,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
}}) }})
assert.NoError(t, err, "failed to create a test stack") assert.NoError(t, err, "failed to create a test stack")
err = RedeployWhenChanged(1, nil, store, &gitService{nil, "oldHash"}) err = RedeployWhenChanged(1, nil, store, testhelpers.NewGitService(nil, "oldHash"))
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -125,7 +105,7 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
}}) }})
assert.NoError(t, err, "failed to create a test stack") assert.NoError(t, err, "failed to create a test stack")
err = RedeployWhenChanged(1, nil, store, &gitService{cloneErr, "newHash"}) err = RedeployWhenChanged(1, nil, store, testhelpers.NewGitService(cloneErr, "newHash"))
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, cloneErr, "should failed to clone but didn't, check test setup") assert.ErrorIs(t, err, cloneErr, "should failed to clone but didn't, check test setup")
} }
@ -162,7 +142,7 @@ func Test_redeployWhenChanged(t *testing.T) {
stack.Type = portainer.DockerComposeStack stack.Type = portainer.DockerComposeStack
store.Stack().UpdateStack(stack.ID, &stack) store.Stack().UpdateStack(stack.ID, &stack)
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"}) err = RedeployWhenChanged(1, &noopDeployer{}, store, testhelpers.NewGitService(nil, "newHash"))
assert.NoError(t, err) assert.NoError(t, err)
}) })
@ -170,7 +150,7 @@ func Test_redeployWhenChanged(t *testing.T) {
stack.Type = portainer.DockerSwarmStack stack.Type = portainer.DockerSwarmStack
store.Stack().UpdateStack(stack.ID, &stack) store.Stack().UpdateStack(stack.ID, &stack)
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"}) err = RedeployWhenChanged(1, &noopDeployer{}, store, testhelpers.NewGitService(nil, "newHash"))
assert.NoError(t, err) assert.NoError(t, err)
}) })
@ -178,7 +158,7 @@ func Test_redeployWhenChanged(t *testing.T) {
stack.Type = portainer.KubernetesStack stack.Type = portainer.KubernetesStack
store.Stack().UpdateStack(stack.ID, &stack) store.Stack().UpdateStack(stack.ID, &stack)
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"}) err = RedeployWhenChanged(1, &noopDeployer{}, store, testhelpers.NewGitService(nil, "newHash"))
assert.NoError(t, err) assert.NoError(t, err)
}) })
} }

View File

@ -67,6 +67,8 @@ func (b *GitMethodStackBuilder) SetGitRepository(payload *StackPayload) GitMetho
repoConfig.URL = payload.URL repoConfig.URL = payload.URL
repoConfig.ReferenceName = payload.ReferenceName repoConfig.ReferenceName = payload.ReferenceName
repoConfig.TLSSkipVerify = payload.TLSSkipVerify
repoConfig.ConfigFilePath = payload.ComposeFile repoConfig.ConfigFilePath = payload.ComposeFile
if payload.ComposeFile == "" { if payload.ComposeFile == "" {
repoConfig.ConfigFilePath = filesystem.ComposeFileDefaultName repoConfig.ConfigFilePath = filesystem.ComposeFileDefaultName

View File

@ -52,4 +52,6 @@ type RepositoryConfigPayload struct {
// Password used in basic authentication. Required when RepositoryAuthentication is true // Password used in basic authentication. Required when RepositoryAuthentication is true
// and RepositoryGitCredentialID is 0 // and RepositoryGitCredentialID is 0
Password string `example:"myGitPassword"` Password string `example:"myGitPassword"`
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
} }

View File

@ -27,7 +27,7 @@ func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig
stackFolder := fmt.Sprintf("%d", stackID) stackFolder := fmt.Sprintf("%d", stackID)
projectPath := fileService.GetStackProjectPath(stackFolder) projectPath := fileService.GetStackProjectPath(stackFolder)
err := gitService.CloneRepository(projectPath, config.URL, config.ReferenceName, username, password) err := gitService.CloneRepository(projectPath, config.URL, config.ReferenceName, username, password, config.TLSSkipVerify)
if err != nil { if err != nil {
if err == gittypes.ErrAuthenticationFailure { if err == gittypes.ErrAuthenticationFailure {
newErr := ErrInvalidGitCredential newErr := ErrInvalidGitCredential
@ -38,7 +38,7 @@ func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig
return "", newErr return "", newErr
} }
commitID, err := gitService.LatestCommitID(config.URL, config.ReferenceName, username, password) commitID, err := gitService.LatestCommitID(config.URL, config.ReferenceName, username, password, config.TLSSkipVerify)
if err != nil { if err != nil {
newErr := fmt.Errorf("unable to fetch git repository id: %w", err) newErr := fmt.Errorf("unable to fetch git repository id: %w", err)
return "", newErr return "", newErr

View File

@ -56,6 +56,7 @@ angular.module('portainer.edge').factory('EdgeStackService', function EdgeStackS
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication, RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
RepositoryUsername: repositoryOptions.RepositoryUsername, RepositoryUsername: repositoryOptions.RepositoryUsername,
RepositoryPassword: repositoryOptions.RepositoryPassword, RepositoryPassword: repositoryOptions.RepositoryPassword,
TLSSkipVerify: repositoryOptions.TLSSkipVerify,
} }
).$promise; ).$promise;
} catch (err) { } catch (err) {

View File

@ -23,6 +23,7 @@ export default class CreateEdgeStackViewController {
Groups: [], Groups: [],
DeploymentType: 0, DeploymentType: 0,
UseManifestNamespaces: false, UseManifestNamespaces: false,
TLSSkipVerify: false,
}; };
this.EditorType = EditorType; this.EditorType = EditorType;
@ -215,6 +216,7 @@ export default class CreateEdgeStackViewController {
RepositoryAuthentication: this.formValues.RepositoryAuthentication, RepositoryAuthentication: this.formValues.RepositoryAuthentication,
RepositoryUsername: this.formValues.RepositoryUsername, RepositoryUsername: this.formValues.RepositoryUsername,
RepositoryPassword: this.formValues.RepositoryPassword, RepositoryPassword: this.formValues.RepositoryPassword,
TLSSkipVerify: this.formValues.TLSSkipVerify,
}; };
return this.EdgeStackService.createStackFromGitRepository( return this.EdgeStackService.createStackFromGitRepository(
{ {

View File

@ -59,6 +59,7 @@ class KubernetesDeployController {
ComposeFilePathInRepository: '', ComposeFilePathInRepository: '',
Variables: {}, Variables: {},
AutoUpdate: parseAutoUpdateResponse(), AutoUpdate: parseAutoUpdateResponse(),
TLSSkipVerify: false,
}; };
this.ManifestDeployTypes = KubernetesDeployManifestTypes; this.ManifestDeployTypes = KubernetesDeployManifestTypes;
@ -248,6 +249,7 @@ class KubernetesDeployController {
}; };
if (method === KubernetesDeployRequestMethods.REPOSITORY) { if (method === KubernetesDeployRequestMethods.REPOSITORY) {
payload.TLSSkipVerify = this.formValues.TLSSkipVerify;
payload.RepositoryURL = this.formValues.RepositoryURL; payload.RepositoryURL = this.formValues.RepositoryURL;
payload.RepositoryReferenceName = this.formValues.RepositoryReferenceName; payload.RepositoryReferenceName = this.formValues.RepositoryReferenceName;
payload.RepositoryAuthentication = this.formValues.RepositoryAuthentication ? true : false; payload.RepositoryAuthentication = this.formValues.RepositoryAuthentication ? true : false;

View File

@ -355,6 +355,7 @@ angular.module('portainer.app').factory('StackService', [
RepositoryPassword: repositoryOptions.RepositoryPassword, RepositoryPassword: repositoryOptions.RepositoryPassword,
Env: env, Env: env,
FromAppTemplate: repositoryOptions.FromAppTemplate, FromAppTemplate: repositoryOptions.FromAppTemplate,
TLSSkipVerify: repositoryOptions.TLSSkipVerify,
}; };
if (repositoryOptions.AutoUpdate) { if (repositoryOptions.AutoUpdate) {
@ -382,6 +383,7 @@ angular.module('portainer.app').factory('StackService', [
RepositoryPassword: repositoryOptions.RepositoryPassword, RepositoryPassword: repositoryOptions.RepositoryPassword,
Env: env, Env: env,
FromAppTemplate: repositoryOptions.FromAppTemplate, FromAppTemplate: repositoryOptions.FromAppTemplate,
TLSSkipVerify: repositoryOptions.TLSSkipVerify,
}; };
if (repositoryOptions.AutoUpdate) { if (repositoryOptions.AutoUpdate) {

View File

@ -44,6 +44,7 @@ class CreateCustomTemplateViewController {
Type: 1, Type: 1,
AccessControlData: new AccessControlFormData(), AccessControlData: new AccessControlFormData(),
Variables: [], Variables: [],
TLSSkipVerify: false,
}; };
this.state = { this.state = {

View File

@ -57,6 +57,7 @@ angular
EnableWebhook: false, EnableWebhook: false,
Variables: {}, Variables: {},
AutoUpdate: parseAutoUpdateResponse(), AutoUpdate: parseAutoUpdateResponse(),
TLSSkipVerify: false,
}; };
$scope.state = { $scope.state = {
@ -175,6 +176,7 @@ angular
RepositoryUsername: $scope.formValues.RepositoryUsername, RepositoryUsername: $scope.formValues.RepositoryUsername,
RepositoryPassword: $scope.formValues.RepositoryPassword, RepositoryPassword: $scope.formValues.RepositoryPassword,
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate, $scope.state.webhookId), AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate, $scope.state.webhookId),
TLSSkipVerify: $scope.formValues.TLSSkipVerify,
}; };
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId); return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
@ -201,6 +203,7 @@ angular
RepositoryUsername: $scope.formValues.RepositoryUsername, RepositoryUsername: $scope.formValues.RepositoryUsername,
RepositoryPassword: $scope.formValues.RepositoryPassword, RepositoryPassword: $scope.formValues.RepositoryPassword,
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate, $scope.state.webhookId), AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate, $scope.state.webhookId),
TLSSkipVerify: $scope.formValues.TLSSkipVerify,
}; };
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId); return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);

View File

@ -35,6 +35,7 @@ export function PathSelector({
repository: model.RepositoryURL, repository: model.RepositoryURL,
keyword: searchTerm, keyword: searchTerm,
reference: model.RepositoryReferenceName, reference: model.RepositoryReferenceName,
tlsSkipVerify: model.TLSSkipVerify,
...creds, ...creds,
}; };
const enabled = Boolean( const enabled = Boolean(

View File

@ -70,6 +70,7 @@ export function Primary({
ComposeFilePathInRepository: '', ComposeFilePathInRepository: '',
NewCredentialName: '', NewCredentialName: '',
SaveCredential: false, SaveCredential: false,
TLSSkipVerify: false,
}; };
return ( return (

View File

@ -9,6 +9,7 @@ import { TimeWindowDisplay } from '@/react/portainer/gitops/TimeWindowDisplay';
import { FormSection } from '@@/form-components/FormSection'; import { FormSection } from '@@/form-components/FormSection';
import { validateForm } from '@@/form-components/validate-form'; import { validateForm } from '@@/form-components/validate-form';
import { SwitchField } from '@@/form-components/SwitchField';
import { GitCredential } from '../account/git-credentials/types'; import { GitCredential } from '../account/git-credentials/types';
@ -104,6 +105,19 @@ export function GitForm({
)} )}
<TimeWindowDisplay /> <TimeWindowDisplay />
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Skip TLS Verification"
checked={value.TLSSkipVerify}
onChange={(value) => handleChange({ TLSSkipVerify: value })}
name="TLSSkipVerify"
tooltip="Enabling this will allow skipping TLS validation for any self-signed certificate."
labelClass="col-sm-3 col-lg-2"
/>
</div>
</div>
</FormSection> </FormSection>
); );
@ -127,7 +141,18 @@ export function buildGitValidationSchema(
): SchemaOf<GitFormModel> { ): SchemaOf<GitFormModel> {
return object({ return object({
RepositoryURL: string() RepositoryURL: string()
.url('Invalid Url') .test('valid URL', 'The URL must be a valid URL', (value) => {
if (!value) {
return true;
}
try {
const url = new URL(value);
return !!url.hostname;
} catch {
return false;
}
})
.required('Repository URL is required'), .required('Repository URL is required'),
RepositoryReferenceName: refFieldValidation(), RepositoryReferenceName: refFieldValidation(),
ComposeFilePathInRepository: string().required( ComposeFilePathInRepository: string().required(
@ -136,5 +161,6 @@ export function buildGitValidationSchema(
AdditionalFiles: array(string().required('Path is required')).default([]), AdditionalFiles: array(string().required('Path is required')).default([]),
RepositoryURLValid: boolean().default(false), RepositoryURLValid: boolean().default(false),
AutoUpdate: autoUpdateValidation().nullable(), AutoUpdate: autoUpdateValidation().nullable(),
TLSSkipVerify: boolean().default(false),
}).concat(gitAuthValidation(gitCredentials)); }).concat(gitAuthValidation(gitCredentials));
} }

View File

@ -40,14 +40,18 @@ export function GitFormUrlField({
const creds = getAuthentication(model); const creds = getAuthentication(model);
const [force, setForce] = useState(false); const [force, setForce] = useState(false);
const repoStatusQuery = useCheckRepo(value, creds, force, { const repoStatusQuery = useCheckRepo(
value,
{ creds, force, tlsSkipVerify: model.TLSSkipVerify },
{
onSettled(isValid) { onSettled(isValid) {
onChangeRepositoryValid(!!isValid); onChangeRepositoryValid(!!isValid);
setForce(false); setForce(false);
}, },
// disabled check on CE since it's not supported // disabled check on CE since it's not supported
enabled: isBE, enabled: isBE,
}); }
);
const [debouncedValue, debouncedOnChange] = useDebounce(value, onChange); const [debouncedValue, debouncedOnChange] = useDebounce(value, onChange);
@ -115,7 +119,7 @@ export function useUrlValidation(force: boolean) {
const model = context.parent as GitFormModel; const model = context.parent as GitFormModel;
const creds = getAuthentication(model); const creds = getAuthentication(model);
return checkRepo(url, creds, force); return checkRepo(url, { creds, force });
} }
); );

View File

@ -24,6 +24,7 @@ export function RefSelector({
const payload = { const payload = {
repository: model.RepositoryURL, repository: model.RepositoryURL,
stackId, stackId,
tlsSkipVerify: model.TLSSkipVerify,
...creds, ...creds,
}; };

View File

@ -2,4 +2,5 @@ import { GitCredentialsModel } from '../types';
export interface RefFieldModel extends GitCredentialsModel { export interface RefFieldModel extends GitCredentialsModel {
RepositoryURL: string; RepositoryURL: string;
TLSSkipVerify?: boolean;
} }

View File

@ -8,19 +8,23 @@ interface Creds {
password?: string; password?: string;
gitCredentialId?: number; gitCredentialId?: number;
} }
interface CheckRepoOptions {
creds?: Creds;
force?: boolean;
tlsSkipVerify?: boolean;
}
export function useCheckRepo( export function useCheckRepo(
url: string, url: string,
creds: Creds, options: CheckRepoOptions,
force: boolean,
{ {
enabled, enabled,
onSettled, onSettled,
}: { enabled?: boolean; onSettled?(isValid?: boolean): void } = {} }: { enabled?: boolean; onSettled?(isValid?: boolean): void } = {}
) { ) {
return useQuery( return useQuery(
['git_repo_valid', url, creds, force], ['git_repo_valid', url, options],
() => checkRepo(url, creds, force), () => checkRepo(url, options),
{ {
enabled: !!url && enabled, enabled: !!url && enabled,
onSettled, onSettled,
@ -31,13 +35,12 @@ export function useCheckRepo(
export async function checkRepo( export async function checkRepo(
repository: string, repository: string,
creds: Creds, { force, ...options }: CheckRepoOptions
force: boolean
): Promise<boolean> { ): Promise<boolean> {
try { try {
await axios.post<string[]>( await axios.post<string[]>(
'/gitops/repo/refs', '/gitops/repo/refs',
{ repository, ...creds }, { repository, tlsSkipVerify: options.tlsSkipVerify, ...options.creds },
force ? { params: { force } } : {} force ? { params: { force } } : {}
); );
return true; return true;
@ -45,11 +48,12 @@ export async function checkRepo(
throw parseAxiosError(error as Error, '', (axiosError: AxiosError) => { throw parseAxiosError(error as Error, '', (axiosError: AxiosError) => {
let details = axiosError.response?.data.details; let details = axiosError.response?.data.details;
const { creds = {} } = options;
// If no credentials were provided alter error from git to indicate repository is not found or is private // If no credentials were provided alter error from git to indicate repository is not found or is private
if ( if (
!(creds.username && creds.password) && !(creds.username && creds.password) &&
details === details ===
'Authentication failed, please ensure that the git credentials are correct.' 'authentication failed, please ensure that the git credentials are correct'
) { ) {
details = details =
'Git repository could not be found or is private, please ensure that the URL is correct or credentials are provided.'; 'Git repository could not be found or is private, please ensure that the URL is correct or credentials are provided.';

View File

@ -6,6 +6,7 @@ interface RefsPayload {
repository: string; repository: string;
username?: string; username?: string;
password?: string; password?: string;
tlsSkipVerify?: boolean;
} }
export function useGitRefs<T = string[]>( export function useGitRefs<T = string[]>(

View File

@ -60,6 +60,7 @@ export interface GitFormModel extends GitAuthModel {
SaveCredential?: boolean; SaveCredential?: boolean;
NewCredentialName?: string; NewCredentialName?: string;
TLSSkipVerify: boolean;
/** /**
* Auto update * Auto update