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"
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"
)
@ -51,21 +49,22 @@ type azureItem struct {
}
type azureClient struct {
client *http.Client
baseUrl string
}
func NewAzureClient() *azureClient {
httpsCli := newHttpClientForAzure()
return &azureClient{
client: httpsCli,
baseUrl: "https://dev.azure.com",
}
}
func newHttpClientForAzure() *http.Client {
func newHttpClientForAzure(insecureSkipVerify bool) *http.Client {
tlsConfig := crypto.CreateTLSConfiguration()
if insecureSkipVerify {
tlsConfig.InsecureSkipVerify = true
}
httpsCli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
@ -74,7 +73,6 @@ func newHttpClientForAzure() *http.Client {
Timeout: 300 * time.Second,
}
client.InstallProtocol("https", githttp.NewClient(httpsCli))
return httpsCli
}
@ -106,6 +104,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
if err != nil {
return "", errors.WithMessage(err, "failed to create temp file")
}
defer zipFile.Close()
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")
}
res, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
res, err := client.Do(req)
if err != nil {
return "", errors.WithMessage(err, "failed to make an HTTP request")
}
defer res.Body.Close()
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")
}
resp, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
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")
}
resp, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
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")
}
resp, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
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) {
dst := t.TempDir()
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.FileExists(t, filepath.Join(dst, "README.md"))
})
@ -74,7 +74,7 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
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.FileExists(t, filepath.Join(dst, "README.md"))
}
@ -85,7 +85,7 @@ func TestService_LatestCommitID_Azure(t *testing.T) {
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService(context.TODO())
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat)
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat, false)
assert.NoError(t, err)
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")
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.GreaterOrEqual(t, len(refs), 1)
}
@ -109,8 +109,8 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
go service.ListRefs(privateAzureRepoURL, username, accessToken, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, false)
go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
time.Sleep(2 * time.Second)
}
@ -248,7 +248,7 @@ func TestService_ListFiles_Azure(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, 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 {
assert.Error(t, err)
if tt.expect.err != nil {
@ -271,8 +271,8 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
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{}, false)
time.Sleep(2 * time.Second)
}

View File

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

View File

@ -20,6 +20,8 @@ type CloneOptions struct {
ReferenceName string
Username 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) {
@ -43,7 +45,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
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 {
cleanUp = false
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)

View File

@ -28,9 +28,10 @@ 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,
Auth: getAuth(opt.username, opt.password),
URL: opt.repositoryUrl,
Depth: opt.depth,
InsecureSkipTLS: opt.tlsSkipVerify,
Auth: getAuth(opt.username, opt.password),
}
if opt.referenceName != "" {
@ -60,7 +61,8 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
})
listOptions := &git.ListOptions{
Auth: getAuth(opt.username, opt.password),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
}
refs, err := remote.List(listOptions)
@ -110,7 +112,8 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
})
listOptions := &git.ListOptions{
Auth: getAuth(opt.username, opt.password),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
}
refs, err := rem.List(listOptions)
@ -132,12 +135,13 @@ 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.username, opt.password),
URL: opt.repositoryUrl,
NoCheckout: true,
Depth: 1,
SingleBranch: true,
ReferenceName: plumbing.ReferenceName(opt.referenceName),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
}
repo, err := git.Clone(memory.NewStorage(), nil, cloneOption)

View File

@ -24,7 +24,7 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
dst := t.TempDir()
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.FileExists(t, filepath.Join(dst, "README.md"))
}
@ -37,7 +37,7 @@ func TestService_LatestCommitID_GitHub(t *testing.T) {
service := newService(context.TODO(), 0, 0)
repositoryUrl := privateGitRepoURL
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken)
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken, false)
assert.NoError(t, err)
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)
repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err)
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)
repositoryUrl := privateGitRepoURL
go service.ListRefs(repositoryUrl, username, accessToken, false)
service.ListRefs(repositoryUrl, username, accessToken, false)
go service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListRefs(repositoryUrl, username, accessToken, false, false)
time.Sleep(2 * time.Second)
}
@ -202,7 +202,7 @@ func TestService_ListFiles_GitHub(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, 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 {
assert.Error(t, err)
if tt.expect.err != nil {
@ -226,8 +226,8 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
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{}, false)
time.Sleep(2 * time.Second)
}
@ -240,8 +240,8 @@ func TestService_purgeCache_Github(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService(context.TODO())
service.ListRefs(repositoryUrl, username, accessToken, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
assert.Equal(t, 1, service.repoRefCache.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
service := newService(context.TODO(), 2, 40*timeout)
service.ListRefs(repositoryUrl, username, accessToken, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
assert.Equal(t, 1, service.repoRefCache.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)
repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1)
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.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)
repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1)
assert.Equal(t, 1, service.repoRefCache.Len())
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 1, service.repoFileCache.Len())
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{})
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{}, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1)
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.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.Equal(t, 1, service.repoRefCache.Len())
// 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")
username := getRequiredValue(t, "GITHUB_USERNAME")
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.GreaterOrEqual(t, len(files), 1)
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.Equal(t, 0, service.repoFileCache.Len())
}

View File

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

View File

@ -2,6 +2,7 @@ package git
import (
"context"
"strconv"
"strings"
"sync"
"time"
@ -20,6 +21,7 @@ type baseOption struct {
repositoryUrl string
username string
password string
tlsSkipVerify bool
}
// 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
// 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{
fetchOption: fetchOption{
baseOption: baseOption{
repositoryUrl: repositoryURL,
username: username,
password: password,
tlsSkipVerify: tlsSkipVerify,
},
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
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{
baseOption: baseOption{
repositoryUrl: repositoryURL,
username: username,
password: password,
tlsSkipVerify: tlsSkipVerify,
},
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
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
refCacheKey := generateCacheKey(repositoryURL, password)
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
refCacheKey := generateCacheKey(repositoryURL, username, password, strconv.FormatBool(tlsSkipVerify))
if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result
service.repoRefCache.Remove(refCacheKey)
@ -193,6 +197,7 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
repositoryUrl: repositoryURL,
username: username,
password: password,
tlsSkipVerify: tlsSkipVerify,
}
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.
// 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) {
repoKey := generateCacheKey(repositoryURL, referenceName)
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify))
if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result
@ -246,6 +251,7 @@ func (service *Service) ListFiles(repositoryURL, referenceName, username, passwo
repositoryUrl: repositoryURL,
username: username,
password: password,
tlsSkipVerify: tlsSkipVerify,
},
referenceName: referenceName,
}

View File

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

View File

@ -6,14 +6,13 @@ import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/git"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/rs/zerolog/log"
)
// 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 {
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)
}
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 {
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
}
hashChanged := !strings.EqualFold(newHash, string(gitConfig.ConfigHash))
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
hashChanged := !strings.EqualFold(newHash, gitConfig.ConfigHash)
if !hashChanged && !forceUpdate {
log.Debug().
Str("hash", newHash).
@ -48,9 +47,10 @@ func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.Dat
}
cloneParams := &cloneRepositoryParameters{
url: gitConfig.URL,
ref: gitConfig.ReferenceName,
toDir: projectPath,
url: gitConfig.URL,
ref: gitConfig.ReferenceName,
toDir: projectPath,
tlsSkipVerify: gitConfig.TLSSkipVerify,
}
if gitConfig.Authentication != nil {
cloneParams.auth = &gitAuth{
@ -78,6 +78,8 @@ type cloneRepositoryParameters struct {
ref string
toDir string
auth *gitAuth
// tlsSkipVerify skips SSL verification when cloning the Git repository
tlsSkipVerify bool `example:"false"`
}
type gitAuth struct {
@ -87,8 +89,8 @@ type gitAuth struct {
func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error {
if cloneParams.auth != nil {
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password)
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"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
}
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -279,7 +281,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
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 == gittypes.ErrAuthenticationFailure {
return nil, fmt.Errorf("invalid git credential")

View File

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

View File

@ -18,32 +18,12 @@ import (
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"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
func setupHandler(t *testing.T) (*Handler, string, func()) {
t.Helper()
@ -98,7 +78,7 @@ func setupHandler(t *testing.T) (*Handler, string, func()) {
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
}

View File

@ -162,9 +162,11 @@ type composeStackFromGitRepositoryPayload struct {
Env []portainer.Pair
// Whether the stack is from a app template
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{
Name: name,
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
@ -173,6 +175,7 @@ func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoU
Authentication: repoAuthentication,
Username: repoUsername,
Password: repoPassword,
TLSSkipVerify: repoSkipSSLVerify,
},
ComposeFile: composeFile,
AdditionalFiles: additionalFiles,
@ -258,7 +261,9 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
payload.AdditionalFiles,
payload.AutoUpdate,
payload.Env,
payload.FromAppTemplate)
payload.FromAppTemplate,
payload.TLSSkipVerify,
)
composeStackBuilder := stackbuilders.CreateComposeStackGitBuilder(securityContext,
handler.DataStore,

View File

@ -46,9 +46,11 @@ type kubernetesGitDeploymentPayload struct {
ManifestFile string
AdditionalFiles []string
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{
StackName: name,
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
@ -57,6 +59,7 @@ func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsern
Authentication: repoAuthentication,
Username: repoUsername,
Password: repoPassword,
TLSSkipVerify: repoSkipSSLVerify,
},
Namespace: namespace,
ComposeFormat: composeFormat,
@ -203,7 +206,9 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
payload.Namespace,
payload.ManifestFile,
payload.AdditionalFiles,
payload.AutoUpdate)
payload.AutoUpdate,
payload.TLSSkipVerify,
)
k8sStackBuilder := stackbuilders.CreateKubernetesStackGitBuilder(handler.DataStore,
handler.FileService,

View File

@ -117,6 +117,8 @@ type swarmStackFromGitRepositoryPayload struct {
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
// Optional auto update configuration
AutoUpdate *portainer.AutoUpdateSettings
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
}
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -138,7 +140,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
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{
Name: name,
SwarmID: swarmID,
@ -201,7 +203,9 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
payload.AdditionalFiles,
payload.AutoUpdate,
payload.Env,
payload.FromAppTemplate)
payload.FromAppTemplate,
payload.TLSSkipVerify,
)
swarmStackBuilder := stackbuilders.CreateSwarmStackGitBuilder(securityContext,
handler.DataStore,

View File

@ -158,7 +158,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
Username: payload.RepositoryUsername,
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 {
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
}
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 {
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
}
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 {
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,
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 {
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)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "")
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "", false)
if err != nil {
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")
if strings.HasSuffix(remote, ".git") {
repositoryURL := remote[:len(remote)-4]
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "")
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "", false)
if err != nil {
return err
}

View File

@ -1,29 +1,16 @@
package docker
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/testhelpers"
"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) {
type fields struct {
gitService portainer.GitService
@ -33,8 +20,10 @@ func TestTransport_updateDefaultGitBranch(t *testing.T) {
request *http.Request
}
commitId := "my-latest-commit-id"
defaultFields := fields{
gitService: &noopGitService{},
gitService: testhelpers.NewGitService(nil, commitId),
}
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),
},
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",

View File

@ -1,12 +1,32 @@
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.
func NewGitService() *gitService {
return &gitService{}
func NewGitService(cloneErr error, id string) portainer.GitService {
return &gitService{
cloneErr: cloneErr,
id: id,
}
}
func (service *gitService) CloneRepository(destination string, repositoryURL, referenceName string, username, password string) error {
return nil
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error {
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 interface {
CloneRepository(destination string, repositoryURL, referenceName, username, password string) error
LatestCommitID(repositoryURL, referenceName, username, password string) (string, error)
ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error)
ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includeExts []string) ([]string, error)
CloneRepository(destination string, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error
LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error)
ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error)
ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includeExts []string, tlsSkipVerify bool) ([]string, error)
}
// 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("stack", stack.Name).
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}
}
var gitCommitChangedOrForceUpdate bool
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 {
return err
}
@ -99,7 +99,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
err := deployer.DeployKubernetesStack(stack, endpoint, user)
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:
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)

View File

@ -6,33 +6,13 @@ import (
"testing"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types"
"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{}
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"})
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)
}
@ -97,7 +77,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
}})
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)
}
@ -125,7 +105,7 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
}})
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.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
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)
})
@ -170,7 +150,7 @@ func Test_redeployWhenChanged(t *testing.T) {
stack.Type = portainer.DockerSwarmStack
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)
})
@ -178,7 +158,7 @@ func Test_redeployWhenChanged(t *testing.T) {
stack.Type = portainer.KubernetesStack
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)
})
}

View File

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

View File

@ -52,4 +52,6 @@ type RepositoryConfigPayload struct {
// Password used in basic authentication. Required when RepositoryAuthentication is true
// and RepositoryGitCredentialID is 0
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)
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 == gittypes.ErrAuthenticationFailure {
newErr := ErrInvalidGitCredential
@ -38,7 +38,7 @@ func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig
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 {
newErr := fmt.Errorf("unable to fetch git repository id: %w", err)
return "", newErr

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { TimeWindowDisplay } from '@/react/portainer/gitops/TimeWindowDisplay';
import { FormSection } from '@@/form-components/FormSection';
import { validateForm } from '@@/form-components/validate-form';
import { SwitchField } from '@@/form-components/SwitchField';
import { GitCredential } from '../account/git-credentials/types';
@ -104,6 +105,19 @@ export function GitForm({
)}
<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>
);
@ -127,7 +141,18 @@ export function buildGitValidationSchema(
): SchemaOf<GitFormModel> {
return object({
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'),
RepositoryReferenceName: refFieldValidation(),
ComposeFilePathInRepository: string().required(
@ -136,5 +161,6 @@ export function buildGitValidationSchema(
AdditionalFiles: array(string().required('Path is required')).default([]),
RepositoryURLValid: boolean().default(false),
AutoUpdate: autoUpdateValidation().nullable(),
TLSSkipVerify: boolean().default(false),
}).concat(gitAuthValidation(gitCredentials));
}

View File

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

View File

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

View File

@ -8,19 +8,23 @@ interface Creds {
password?: string;
gitCredentialId?: number;
}
interface CheckRepoOptions {
creds?: Creds;
force?: boolean;
tlsSkipVerify?: boolean;
}
export function useCheckRepo(
url: string,
creds: Creds,
force: boolean,
options: CheckRepoOptions,
{
enabled,
onSettled,
}: { enabled?: boolean; onSettled?(isValid?: boolean): void } = {}
) {
return useQuery(
['git_repo_valid', url, creds, force],
() => checkRepo(url, creds, force),
['git_repo_valid', url, options],
() => checkRepo(url, options),
{
enabled: !!url && enabled,
onSettled,
@ -31,13 +35,12 @@ export function useCheckRepo(
export async function checkRepo(
repository: string,
creds: Creds,
force: boolean
{ force, ...options }: CheckRepoOptions
): Promise<boolean> {
try {
await axios.post<string[]>(
'/gitops/repo/refs',
{ repository, ...creds },
{ repository, tlsSkipVerify: options.tlsSkipVerify, ...options.creds },
force ? { params: { force } } : {}
);
return true;
@ -45,11 +48,12 @@ export async function checkRepo(
throw parseAxiosError(error as Error, '', (axiosError: AxiosError) => {
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 (
!(creds.username && creds.password) &&
details ===
'Authentication failed, please ensure that the git credentials are correct.'
'authentication failed, please ensure that the git credentials are correct'
) {
details =
'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;
username?: string;
password?: string;
tlsSkipVerify?: boolean;
}
export function useGitRefs<T = string[]>(

View File

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