Compare commits

...

39 Commits

Author SHA1 Message Date
Matt Hook
e3b727a636 bump version to 2.18.1 (#8802) 2023-04-18 14:50:18 +12:00
Chaim Lev-Ari
d56ea05218 fix(edge/updates): add padding for edge groups [EE-5349] (#8771) 2023-04-18 13:40:06 +12:00
Dakota Walsh
8e724e3fbe feat(libhelm): allow passing optional env and http client [EE-5252] (#8798)
Co-authored-by: Matt Hook <hookenz@gmail.com>
2023-04-14 15:51:11 +12:00
cmeng
33b141bcd3 fix(backup) add description text to backup EE-5283 (#8776) 2023-04-13 16:04:59 +12:00
Matt Hook
ded8ce48a8 feat(cert): ce teasers for ca cert [EE-5252] (#8769) 2023-04-13 15:32:58 +12:00
Oscar Zhou
e60635bf32 fix(swagger): correct endpoint api annotations [EE-5333] (#8762) 2023-04-13 15:31:18 +12:00
cmeng
6fb4951949 fix(stack): upgrade docker-compose EE-5334 (#8756) 2023-04-11 17:55:53 +12:00
Oscar Zhou
c429b29216 fix(k8s/gitops): missing git auth toggle in k8s app edit page [EE-5320] (#8740) 2023-04-10 20:14:04 +12:00
Ali
8ab490f224 fix(ns): add selection caching back [EE-5273] (#8739)
Co-authored-by: testa113 <testa113>
2023-04-06 14:28:05 +12:00
Matt Hook
78b83420bf search for correct source directory when doing a restore (#8677) 2023-04-06 10:39:16 +12:00
cmeng
b4dbc341cc fix(homepage) move heartbeat logic to backend EE-5317 (#8736) 2023-04-06 09:09:13 +12:00
Matt Hook
3118c639f6 fix(docs): add missing swagger docs for upload file [EE-4886] (#8707)
* add docs for uploading files via host management features

* fix other doc issues
2023-04-04 16:59:26 +12:00
cmeng
5d7ab85473 fix(security): potential vulnerability of path traversal attacks EE-5303 (#8727) 2023-04-04 09:00:11 +12:00
Chaim Lev-Ari
99331a81d4 feat(gitops): allow to skip tls verification [EE-5023] (#8679) 2023-04-03 09:19:09 +03:00
Prabhat Khera
ab1a8c1d6a fix(ui): namespace caching issue EE-5273 (#8710)
* fix namespace caching issue

* fix(apps): add loading state [EE-5273]

* rm endpoint provider

* fix(namespace): remove caching [EE-5273]

* variable typo

---------

Co-authored-by: testa113 <testa113>
2023-03-31 13:25:00 +13:00
Chaim Lev-Ari
e063cba81b fix(ui/code-editor): stretch code editor content full height [EE-5202] (#8672) 2023-03-30 12:26:35 +03:00
Ali
23e6a982b9 fix(ns): save filter to local storage [EE-5287] (#8724)
* fix(ns): save filter to local storage [EE-5287]

* allow system ns and save per user

---------

Co-authored-by: testa113 <testa113>
2023-03-30 11:21:08 +13:00
andres-portainer
0bf75ae113 fix(snapshots): change the snapshot object to maintain backwards compatibility EE-5240 (#8704) 2023-03-23 13:30:50 -03:00
Ali
72b41dde01 fix(apps) UI release fixes [EE-5197] (#8703)
* fix(apps) searchbar flex resizing and insights

* UI fixes

* update stacks datatable

---------

Co-authored-by: testa113 <testa113>
2023-03-23 08:20:34 +13:00
Ali
36b122ca21 fix(dashboard): use faster proxy request [EE-5160] (#8694)
Co-authored-by: testa113 <testa113>
2023-03-22 15:34:48 +13:00
Prabhat Khera
649799069b fix Gpus null issue (#8691) 2023-03-21 16:05:55 +13:00
Oscar Zhou
0ca56ddbb1 fix(stack/git): fix cursor movement issue in git text fields (#8656) 2023-03-20 10:00:35 +13:00
Chaim Lev-Ari
3a30c8ed1e fix(ui/box-selector): BE link and use icons standard size [EE-5133] (#8659) 2023-03-19 13:37:44 +01:00
Ali
151db6bfe7 fix(kubeconfig): fix download checkbox [EE-5199] (#8675)
Co-authored-by: testa113 <testa113>
2023-03-17 10:34:00 +13:00
Ali
106c719a34 fix(wizard): Capitalise Kubernetes [EE-5178] (#8663)
Co-authored-by: testa113 <testa113>
2023-03-16 18:50:58 +13:00
Dakota Walsh
1cfd031db1 fix(kubernetes): Prevent rerunning initial cluster detection [EE-5170] (#8667) 2023-03-16 15:39:43 +13:00
Prabhat Khera
fbc1a2d44d fix(ui): namespace cache refresh on reload EE-5155 (#8657) 2023-03-16 10:10:26 +13:00
Oscar Zhou
47478efd1e fix(stack/git): remove duplicate code used to backup compose dir (#8620) 2023-03-15 12:27:23 +13:00
Ali
50940b7fba fix(annotations) ingress tip to match ee [EE-5158] (#8654)
Co-authored-by: testa113 <testa113>
2023-03-14 10:41:41 +13:00
matias-portainer
7468d5637b fix(upgrade): remove yellow upgrade banner EE-5141 (#8641) 2023-03-13 09:01:39 -03:00
Ali
6edc210ae7 fix(kube): check for ns on enter [EE-5160] (#8648)
Co-authored-by: testa113 <testa113>
2023-03-13 13:57:07 +13:00
Prabhat Khera
f859876cb6 fix typo in delete image modal dialog (#8622) 2023-03-13 11:05:55 +13:00
Matt Hook
5e434a82ed reduce throttling in the kube client (#8631) 2023-03-13 09:47:23 +13:00
Ali
d9f6471a00 fix(annotation): update wording/styling [EE-5158] (#8643)
Co-authored-by: testa113 <testa113>
2023-03-10 16:52:15 +13:00
cmeng
a7d1a20dfb fix(edge-stack) always show edge group selector [EE-5157] (#8638) 2023-03-10 10:48:53 +13:00
Ali
17517d7521 fix(app): restrict ns fix create app [EE-5123] (#8633)
Co-authored-by: testa113 <testa113>
2023-03-10 10:24:20 +13:00
andres-portainer
c609f6912f fix(home): disable live connect for async [EE-5000] (#8628) 2023-03-09 15:50:36 -03:00
Ali
346fe9e3f1 refactor(GPU): colocate and update UI [EE-5127] (#8634)
Co-authored-by: testa113 <testa113>
2023-03-09 22:06:49 +13:00
matias-portainer
69f14e569b fix(stacks): pass WorkingDir to deployer command EE-5142 (#8624) 2023-03-08 19:34:50 -03:00
208 changed files with 1358 additions and 961 deletions

View File

@@ -3,8 +3,10 @@ package backup
import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"time"
"github.com/pkg/errors"
@@ -43,6 +45,12 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
return errors.Wrap(err, "Failed to stop db")
}
// At some point, backups were created containing a subdirectory, now we need to handle both
restorePath, err = getRestoreSourcePath(restorePath)
if err != nil {
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
}
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
@@ -59,6 +67,26 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
func getRestoreSourcePath(dir string) (string, error) {
// find portainer.db or portainer.edb file. Return the parent directory
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
backupDirPath := dir
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if portainerdbRegex.MatchString(d.Name()) {
backupDirPath = filepath.Dir(path)
return filepath.SkipDir
}
return nil
})
return backupDirPath, err
}
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)

View File

@@ -37,6 +37,7 @@
"EdgeKey": "",
"Extensions": [],
"GroupId": 1,
"Heartbeat": false,
"Id": 1,
"Name": "local",
"PublicURL": "",

View File

@@ -49,6 +49,7 @@
"EnableGPUManagement": false,
"Gpus": [],
"GroupId": 1,
"Heartbeat": false,
"Id": 1,
"IsEdgeDevice": false,
"Kubernetes": {
@@ -64,6 +65,7 @@
"UseServerMetrics": false
},
"Flags": {
"IsServerIngressClassDetected": false,
"IsServerMetricsDetected": false,
"IsServerStorageDetected": false
},
@@ -905,8 +907,7 @@
},
"Role": 1,
"ThemeSettings": {
"color": "",
"subtleUpgradeButton": false
"color": ""
},
"TokenIssueAt": 0,
"UserTheme": "",
@@ -936,8 +937,7 @@
},
"Role": 1,
"ThemeSettings": {
"color": "",
"subtleUpgradeButton": false
"color": ""
},
"TokenIssueAt": 0,
"UserTheme": "",
@@ -945,6 +945,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.18.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.18.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -53,7 +53,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack, false)
filePaths := stackutils.GetStackFilePaths(stack, true)
err = manager.deployer.Deploy(ctx, filePaths, libstack.DeployOptions{
Options: libstack.Options{
WorkingDir: stack.ProjectPath,
@@ -82,9 +82,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
}
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,
Host: url,
})
return errors.Wrap(err, "failed to remove a stack")
}
@@ -104,7 +106,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack, false)
filePaths := stackutils.GetStackFilePaths(stack, true)
err = manager.deployer.Pull(ctx, filePaths, libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,

View File

@@ -90,7 +90,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
// Deploy executes the docker stack deploy command.
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pullImage bool, endpoint *portainer.Endpoint) error {
filePaths := stackutils.GetStackFilePaths(stack, false)
filePaths := stackutils.GetStackFilePaths(stack, true)
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if err != nil {
return err

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

@@ -342,12 +342,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27 h1:PceCpp86SDYb3lZHT4KpuBCkmcJMW5x1qrdFNEfAdUo=
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a h1:4VGM1OH15fqm5rgki0eLF6vND/NxHfoPt3CA6/YdA0k=
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f h1:z/lmLhZMMSIwg70Ap1rPluXNe1vQXH9gfK9K/ols4JA=
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7 h1:/i985KPNw0KvVtLhTEPUa86aJMtun5ZPOyFCJzdY+dY=
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=

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

@@ -19,7 +19,7 @@ import (
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @param id path string true "EdgeJob Id"
// @param id path int true "EdgeJob Id"
// @success 204
// @failure 500
// @failure 400

View File

@@ -20,7 +20,7 @@ type edgeJobFileResponse struct {
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"
// @param id path int true "EdgeJob Id"
// @success 200 {object} edgeJobFileResponse
// @failure 500
// @failure 400

View File

@@ -21,7 +21,7 @@ type edgeJobInspectResponse struct {
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"
// @param id path int true "EdgeJob Id"
// @success 200 {object} portainer.EdgeJob
// @failure 500
// @failure 400

View File

@@ -19,8 +19,8 @@ import (
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"
// @param taskID path string true "Task Id"
// @param id path int true "EdgeJob Id"
// @param taskID path int true "Task Id"
// @success 204
// @failure 500
// @failure 400

View File

@@ -19,8 +19,8 @@ import (
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"
// @param taskID path string true "Task Id"
// @param id path int true "EdgeJob Id"
// @param taskID path int true "Task Id"
// @success 204
// @failure 500
// @failure 400

View File

@@ -20,8 +20,8 @@ type fileResponse struct {
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"
// @param taskID path string true "Task Id"
// @param id path int true "EdgeJob Id"
// @param taskID path int true "Task Id"
// @success 200 {object} fileResponse
// @failure 500
// @failure 400

View File

@@ -25,7 +25,7 @@ type taskContainer struct {
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"
// @param id path int true "EdgeJob Id"
// @success 200 {array} taskContainer
// @failure 500
// @failure 400

View File

@@ -41,7 +41,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @param id path int true "EdgeJob Id"
// @param body body edgeJobUpdatePayload true "EdgeGroup data"
// @success 200 {object} portainer.EdgeJob
// @failure 500

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

@@ -15,7 +15,7 @@ import (
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @param id path string true "EdgeStack Id"
// @param id path int true "EdgeStack Id"
// @success 204
// @failure 500
// @failure 400

View File

@@ -20,7 +20,7 @@ type stackFileResponse struct {
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeStack Id"
// @param id path int true "EdgeStack Id"
// @success 200 {object} stackFileResponse
// @failure 500
// @failure 400

View File

@@ -16,7 +16,7 @@ import (
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeStack Id"
// @param id path int true "EdgeStack Id"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 400

View File

@@ -15,7 +15,7 @@ import (
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
// @tags edge_stacks
// @produce json
// @param id path string true "EdgeStack Id"
// @param id path int true "EdgeStack Id"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 400

View File

@@ -40,7 +40,7 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error {
// @tags edge_stacks
// @accept json
// @produce json
// @param id path string true "EdgeStack Id"
// @param id path int true "EdgeStack Id"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 400

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

@@ -42,7 +42,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeStack Id"
// @param id path int true "EdgeStack Id"
// @param body body updateEdgeStackPayload true "EdgeStack data"
// @success 200 {object} portainer.EdgeStack
// @failure 500

View File

@@ -25,8 +25,8 @@ func (payload *logsPayload) Validate(r *http.Request) error {
// @tags edge, endpoints
// @accept json
// @produce json
// @param id path string true "environment(endpoint) Id"
// @param jobID path string true "Job Id"
// @param id path int true "environment(endpoint) Id"
// @param jobID path int true "Job Id"
// @success 200
// @failure 500
// @failure 400

View File

@@ -25,8 +25,8 @@ type configResponse struct {
// @tags edge, endpoints, edge_stacks
// @accept json
// @produce json
// @param id path string true "environment(endpoint) Id"
// @param stackId path string true "EdgeStack Id"
// @param id path int true "environment(endpoint) Id"
// @param stackId path int true "EdgeStack Id"
// @success 200 {object} configResponse
// @failure 500
// @failure 400

View File

@@ -0,0 +1,25 @@
package endpoints
/// This feature is implemented in the agent API and not directly here.
/// However, it's proxied. So we document it here.
// @summary Upload a file under a specific path on the file system of an environment (endpoint)
// @description Use this environment(endpoint) to upload TLS files.
// @description **Access policy**: administrator
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @accept multipart/form-data
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param volumeID query string false "Optional volume identifier to upload the file"
// @param Path formData string true "The destination path to upload the file to"
// @param file formData file true "The file to upload"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /endpoints/{id}/docker/v2/browse/put [post]
func _fileBrowseFileUploadV2() {
// dummy function to make swag pick up the above docs for the following REST call
// POST request on /browse/put?volumeID=:id
}

View File

@@ -177,20 +177,20 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
// @accept multipart/form-data
// @produce json
// @param Name formData string true "Name that will be used to identify this environment(endpoint) (example: my-environment)"
// @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment" Enum(1,2,3,4,5)
// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)". Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment)
// @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment)" Enum(1,2,3,4,5)
// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine). Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment)"
// @param PublicURL formData string false "URL or IP address where exposed containers will be reachable. Defaults to URL if not specified (example: docker.mydomain.tld:2375)"
// @param GroupID formData int false "Environment(Endpoint) group identifier. If not specified will default to 1 (unassigned)."
// @param TLS formData bool false "Require TLS to connect against this environment(endpoint)"
// @param TLSSkipVerify formData bool false "Skip server verification when using TLS"
// @param TLSSkipClientVerify formData bool false "Skip client verification when using TLS"
// @param TLS formData bool false "Require TLS to connect against this environment(endpoint). Must be true if EndpointCreationType is set to 2 (Agent environment)"
// @param TLSSkipVerify formData bool false "Skip server verification when using TLS. Must be true if EndpointCreationType is set to 2 (Agent environment)"
// @param TLSSkipClientVerify formData bool false "Skip client verification when using TLS. Must be true if EndpointCreationType is set to 2 (Agent environment)"
// @param TLSCACertFile formData file false "TLS CA certificate file"
// @param TLSCertFile formData file false "TLS client certificate file"
// @param TLSKeyFile formData file false "TLS client key file"
// @param AzureApplicationID formData string false "Azure application ID. Required if environment(endpoint) type is set to 3"
// @param AzureTenantID formData string false "Azure tenant ID. Required if environment(endpoint) type is set to 3"
// @param AzureAuthenticationKey formData string false "Azure authentication key. Required if environment(endpoint) type is set to 3"
// @param TagIDs formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
// @param TagIds formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
// @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)"
// @param EdgeTunnelServerAddress formData string true "URL or IP address that will be used to establish a reverse tunnel"
// @param Gpus formData array false "List of GPUs"

View File

@@ -42,7 +42,13 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
return httperror.Forbidden("Permission denied to access environment", err)
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
}
hideFields(endpoint)
endpointutils.UpdateEdgeEndpointHeartbeat(endpoint, settings)
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
if !excludeSnapshot(r) {

View File

@@ -9,6 +9,7 @@ import (
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
@@ -103,6 +104,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
paginatedEndpoints[idx].QueryDate = time.Now().Unix()
endpointutils.UpdateEdgeEndpointHeartbeat(&paginatedEndpoints[idx], settings)
if !query.excludeSnapshots {
err = handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx])
if err != nil {

View File

@@ -82,7 +82,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.18.0
// @version 2.18.1
// @description.markdown api-description.md
// @termsOfService
@@ -97,7 +97,7 @@ type Handler struct {
// @securitydefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @name x-api-key
// @securitydefinitions.apikey jwt
// @in header
@@ -107,6 +107,8 @@ type Handler struct {
// @tag.description Authenticate against Portainer HTTP API
// @tag.name custom_templates
// @tag.description Manage Custom Templates
// @tag.name edge
// @tag.description Manage Edge related environment(endpoint) settings
// @tag.name edge_groups
// @tag.description Manage Edge Groups
// @tag.name edge_jobs
@@ -115,8 +117,6 @@ type Handler struct {
// @tag.description Manage Edge Stacks
// @tag.name edge_templates
// @tag.description Manage Edge Templates
// @tag.name edge
// @tag.description Manage Edge related environment(endpoint) settings
// @tag.name endpoints
// @tag.description Manage Docker environments(endpoints)
// @tag.name endpoint_groups
@@ -133,8 +133,14 @@ type Handler struct {
// @tag.description Manage roles
// @tag.name settings
// @tag.description Manage Portainer settings
// @tag.name users
// @tag.description Manage users
// @tag.name ssl
// @tag.description Manage ssl settings
// @tag.name stacks
// @tag.description Manage stacks
// @tag.name status
// @tag.description Information about the Portainer instance
// @tag.name system
// @tag.description Manage Portainer system
// @tag.name tags
// @tag.description Manage tags
// @tag.name teams
@@ -143,20 +149,14 @@ type Handler struct {
// @tag.description Manage team memberships
// @tag.name templates
// @tag.description Manage App Templates
// @tag.name stacks
// @tag.description Manage stacks
// @tag.name ssl
// @tag.description Manage ssl settings
// @tag.name users
// @tag.description Manage users
// @tag.name upload
// @tag.description Upload files
// @tag.name webhooks
// @tag.description Manage webhooks
// @tag.name websocket
// @tag.description Create exec sessions using websockets
// @tag.name status
// @tag.description Information about the Portainer instance
// @tag.name system
// @tag.description Manage Portainer system
// ServeHTTP delegates a request to the appropriate subhandler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

View File

@@ -18,7 +18,7 @@ import (
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @param release path string true "The name of the release/application to uninstall"
// @param namespace query string true "An optional namespace"
// @param namespace query string false "An optional namespace"
// @success 204 "Success"
// @failure 400 "Invalid environment(endpoint) id or bad request"
// @failure 401 "Unauthorized"

View File

@@ -20,9 +20,9 @@ import (
// @accept json
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param namespace query string true "specify an optional namespace"
// @param filter query string true "specify an optional filter"
// @param selector query string true "specify an optional selector"
// @param namespace query string false "specify an optional namespace"
// @param filter query string false "specify an optional filter"
// @param selector query string false "specify an optional selector"
// @success 200 {array} release.ReleaseElement "Success"
// @failure 400 "Invalid environment(endpoint) identifier"
// @failure 401 "Unauthorized"

View File

@@ -25,7 +25,7 @@ type addHelmRepoUrlPayload struct {
}
func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
return libhelm.ValidateHelmRepositoryURL(p.URL)
return libhelm.ValidateHelmRepositoryURL(p.URL, nil)
}
// @id HelmUserRepositoryCreate

View File

@@ -143,7 +143,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL, nil)
if err != nil {
return httperror.BadRequest("Invalid Helm repository URL. Must correspond to a valid URL format", err)
}

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

@@ -151,6 +151,9 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
// When the existing stack is using the custom username/password and the password is not updated,
// the stack should keep using the saved username/password
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
password = stack.GitConfig.Authentication.Password
}
@@ -158,7 +161,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

@@ -1,7 +1,6 @@
package stacks
import (
"fmt"
"net/http"
"time"
@@ -10,7 +9,6 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/git"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
@@ -137,23 +135,29 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
}
}
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
if err != nil {
return httperror.InternalServerError("Unable to move git repository directory", err)
}
repositoryUsername := ""
repositoryPassword := ""
if payload.RepositoryAuthentication {
repositoryPassword = payload.RepositoryPassword
// When the existing stack is using the custom username/password and the password is not updated,
// the stack should keep using the saved username/password
if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
repositoryPassword = stack.GitConfig.Authentication.Password
}
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)
}
@@ -165,7 +169,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

@@ -17,7 +17,7 @@ import (
// @tags teams
// @security ApiKeyAuth
// @security jwt
// @param id path string true "Team Id"
// @param id path int true "Team Id"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"

View File

@@ -19,7 +19,7 @@ import (
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "Team Id"
// @param id path int true "Team Id"
// @success 200 {array} portainer.TeamMembership "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"

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

@@ -18,8 +18,6 @@ import (
type themePayload struct {
// Color represents the color theme of the UI
Color *string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
SubtleUpgradeButton *bool `json:"subtleUpgradeButton" example:"false"`
}
type userUpdatePayload struct {
@@ -33,11 +31,11 @@ type userUpdatePayload struct {
func (payload *userUpdatePayload) Validate(r *http.Request) error {
if govalidator.Contains(payload.Username, " ") {
return errors.New("Invalid username. Must not contain any whitespace")
return errors.New("invalid username. Must not contain any whitespace")
}
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
return errors.New("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
return errors.New("invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
}
return nil
}
@@ -120,10 +118,6 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
if payload.Theme.Color != nil {
user.ThemeSettings.Color = *payload.Theme.Color
}
if payload.Theme.SubtleUpgradeButton != nil {
user.ThemeSettings.SubtleUpgradeButton = *payload.Theme.SubtleUpgradeButton
}
}
if payload.Role != 0 {

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

@@ -76,6 +76,16 @@ func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bo
}
func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
if endpoint.Kubernetes.Flags.IsServerIngressClassDetected {
return
}
defer func() {
endpoint.Kubernetes.Flags.IsServerIngressClassDetected = true
endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
}()
cli, err := factory.GetKubeClient(endpoint)
if err != nil {
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
@@ -107,6 +117,16 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
}
func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
if endpoint.Kubernetes.Flags.IsServerMetricsDetected {
return
}
defer func() {
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
}()
cli, err := factory.GetKubeClient(endpoint)
if err != nil {
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
@@ -118,11 +138,6 @@ func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService datas
return
}
endpoint.Kubernetes.Configuration.UseServerMetrics = true
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
err = endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
if err != nil {
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
return
@@ -158,6 +173,16 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
}
func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
if endpoint.Kubernetes.Flags.IsServerStorageDetected {
return
}
defer func() {
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
}()
log.Info().Msg("attempting to detect storage classes in the cluster")
err := storageDetect(endpoint, endpointService, factory)
if err == nil {
@@ -172,3 +197,39 @@ func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService datas
log.Err(err).Msg("final error while detecting storage classes")
}()
}
func UpdateEdgeEndpointHeartbeat(endpoint *portainer.Endpoint, settings *portainer.Settings) {
if IsEdgeEndpoint(endpoint) {
checkInInterval := getEndpointCheckinInterval(endpoint, settings)
endpoint.Heartbeat = endpoint.QueryDate-endpoint.LastCheckInDate <= int64(checkInInterval*2+20)
}
}
func getEndpointCheckinInterval(endpoint *portainer.Endpoint, settings *portainer.Settings) int {
if endpoint.Edge.AsyncMode {
defaultInterval := 60
intervals := [][]int{
{endpoint.Edge.PingInterval, settings.Edge.PingInterval},
{endpoint.Edge.CommandInterval, settings.Edge.CommandInterval},
{endpoint.Edge.SnapshotInterval, settings.Edge.SnapshotInterval},
}
for i := 0; i < len(intervals); i++ {
effectiveInterval := intervals[i][0]
if effectiveInterval <= 0 {
effectiveInterval = intervals[i][1]
}
if effectiveInterval > 0 && effectiveInterval < defaultInterval {
defaultInterval = effectiveInterval
}
}
return defaultInterval
}
if endpoint.EdgeCheckinInterval > 0 {
return endpoint.EdgeCheckinInterval
}
return settings.EdgeAgentCheckinInterval
}

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

@@ -17,6 +17,11 @@ import (
"k8s.io/client-go/tools/clientcmd"
)
const (
DefaultKubeClientQPS = 30
DefaultKubeClientBurst = 100
)
type (
// ClientFactory is used to create Kubernetes clients
ClientFactory struct {
@@ -113,6 +118,9 @@ func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, k
return nil, err
}
cliConfig.QPS = DefaultKubeClientQPS
cliConfig.Burst = DefaultKubeClientBurst
cli, err := kubernetes.NewForConfig(cliConfig)
if err != nil {
return nil, err
@@ -198,7 +206,10 @@ func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernete
if err != nil {
return nil, err
}
config.Insecure = true
config.QPS = DefaultKubeClientQPS
config.Burst = DefaultKubeClientBurst
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return &agentHeaderRoundTripper{
@@ -217,6 +228,9 @@ func buildLocalClient() (*kubernetes.Clientset, error) {
return nil, err
}
config.QPS = DefaultKubeClientQPS
config.Burst = DefaultKubeClientBurst
return kubernetes.NewForConfig(config)
}

View File

@@ -225,7 +225,7 @@ type (
// It contains some information of Docker's ContainerJSON struct
DockerContainerSnapshot struct {
types.Container
Env []string `json:"Env"`
Env []string `json:"Env,omitempty"` // EE-5240
}
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
@@ -388,6 +388,9 @@ type (
LastCheckInDate int64
// QueryDate of each query with the endpoints list
QueryDate int64
// Heartbeat indicates the heartbeat status of an edge environment
Heartbeat bool `json:"Heartbeat" example:"true"`
// IsEdgeDevice marks if the environment was created as an EdgeDevice
IsEdgeDevice bool
// Whether the device has been trusted or not by the user
@@ -588,9 +591,12 @@ type (
Flags KubernetesFlags `json:"Flags"`
}
// KubernetesFlags are used to detect if we need to run initial cluster
// detection again.
KubernetesFlags struct {
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
IsServerIngressClassDetected bool `json:"IsServerIngressClassDetected"`
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
}
// KubernetesSnapshot represents a snapshot of a specific Kubernetes environment(endpoint) at a specific time
@@ -1286,8 +1292,6 @@ type (
UserThemeSettings struct {
// Color represents the color theme of the UI
Color string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
SubtleUpgradeButton bool `json:"subtleUpgradeButton"`
}
// Webhook represents a url webhook that can be used to update a service
@@ -1392,10 +1396,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
@@ -1510,7 +1514,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.18.0"
APIVersion = "2.18.1"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

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

@@ -15,6 +15,10 @@ func UserIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.Endpo
}
// GetStackFilePaths returns a list of file paths based on stack project path
// If absolute is false, the path sanitization step will be skipped, which makes the returning
// paths vulnerable to path traversal attacks. Thus, the followed function using the returning
// paths are responsible to sanitize the raw paths
// If absolute is true, the raw paths will be sanitized
func GetStackFilePaths(stack *portainer.Stack, absolute bool) []string {
if !absolute {
return append([]string{stack.EntryPoint}, stack.AdditionalFiles...)

View File

@@ -1,6 +1,7 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withControlledInput } from '@/react-tools/withControlledInput';
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
@@ -11,6 +12,9 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
import { GpusInsights } from '@/react/docker/host/SetupView/GpusInsights';
import { InsightsBox } from '@/react/components/InsightsBox';
export const componentsModule = angular
.module('portainer.docker.react.components', [])
@@ -45,4 +49,20 @@ export const componentsModule = angular
'usedAllGpus',
'enableGpuManagement',
])
).name;
)
.component(
'gpusList',
r2a(withControlledInput(GpusList), ['value', 'onChange'])
)
.component(
'insightsBox',
r2a(InsightsBox, [
'header',
'content',
'setHtmlContent',
'insightCloseId',
'type',
'className',
])
)
.component('gpusInsights', r2a(GpusInsights, [])).name;

View File

@@ -179,6 +179,8 @@ export default class DockerFeaturesConfigurationController {
disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers,
};
// this.endpoint.Gpus could be null as it is Gpus: []Pair in the API
this.endpoint.Gpus = this.endpoint.Gpus || [];
this.state.enableGPUManagement = this.isDockerStandaloneEnv && (this.endpoint.EnableGPUManagement || this.endpoint.Gpus.length > 0);
this.initialGPUs = this.endpoint.Gpus;
this.initialEnableGPUManagement = this.endpoint.EnableGPUManagement;

View File

@@ -151,21 +151,7 @@
<div class="col-sm-12 form-section-title"> Other </div>
<div class="form-group">
<div class="col-sm-12 pb-3">
<insights-box
header="'GPU settings update'"
set-html-content="true"
insight-close-id="'gpu-settings-update-closed'"
content="'
<p>
From 2.18 on, the set-up of available GPUs for a Docker Standalone environment has been shifted from Add environment and Environment details to Host -> Setup, so as to align with other settings.
</p>
<p>
A toggle has been introduced for enabling/disabling management of GPU settings in the Portainer UI - to alleviate the performance impact of showing those settings.
</p>
<p>
The UI has been updated to clarify that GPU settings support is only for Docker Standalone (and not Docker Swarm, which was never supported in the UI).
</p>'"
></insights-box>
<gpus-insights></gpus-insights>
</div>
<div class="col-sm-12">
<por-switch-field

View File

@@ -73,6 +73,7 @@
<box-selector options="options" slim="true" value="state.BuildType" on-change="(onChangeBuildType)"></box-selector>
<!-- web-editor -->
<!-- TODO use web-editor-form component -->
<div ng-show="state.BuildType === 'editor'">
<div class="col-sm-12 form-section-title"> Web editor </div>
<div class="form-group">

View File

@@ -167,6 +167,6 @@ function confirmImageForceRemoval() {
title: 'Are you sure?',
modalType: ModalType.Destructive,
message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.',
confirmButton: buildConfirmButton('Remote the image', 'danger'),
confirmButton: buildConfirmButton('Remove the image', 'danger'),
});
}

View File

@@ -135,13 +135,14 @@
<!-- !execution-method -->
<!-- web-editor -->
<!-- TODO use web-editor-form component -->
<div ng-show="$ctrl.formValues.method === 'editor'">
<div class="col-sm-12 form-section-title"> Web editor </div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
identifier="execute-edge-job-editor"
placeholder="# Define or paste the content of your script file here"
placeholder="Define or paste the content of your script file here"
on-change="($ctrl.editorUpdate)"
value="$ctrl.model.FileContent"
></code-editor>

View File

@@ -34,7 +34,7 @@
value="$ctrl.model.StackFileContent"
yml="true"
identifier="compose-editor"
placeholder="# Define or paste the content of your docker compose file here"
placeholder="Define or paste the content of your docker compose file here"
on-change="($ctrl.onChangeComposeConfig)"
read-only="$ctrl.hasKubeEndpoint()"
>
@@ -63,7 +63,7 @@
value="$ctrl.model.StackFileContent"
yml="true"
identifier="kube-manifest-editor"
placeholder="# Define or paste the content of your manifest here"
placeholder="Define or paste the content of your manifest here"
on-change="($ctrl.onChangeKubeManifest)"
>
<editor-description>

View File

@@ -7,12 +7,13 @@ import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInInt
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
import { withUIRouter } from '@/react-tools/withUIRouter';
export const componentsModule = angular
.module('portainer.edge.react.components', [])
.component(
'edgeGroupsSelector',
r2a(withReactQuery(EdgeGroupsSelector), [
r2a(withUIRouter(withReactQuery(EdgeGroupsSelector)), [
'onChange',
'value',
'error',

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;
@@ -86,7 +87,6 @@ export default class CreateEdgeStackViewController {
async $onInit() {
try {
this.edgeGroups = await this.EdgeGroupService.groups();
this.noGroups = this.edgeGroups.length === 0;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
}
@@ -216,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

@@ -39,7 +39,7 @@
</div>
<!-- !name-input -->
<edge-groups-selector ng-if="!$ctrl.noGroups" value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
<edge-groups-selector value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
<edge-stack-deployment-type-selector
value="$ctrl.formValues.DeploymentType"

View File

@@ -8,7 +8,7 @@
on-change="($ctrl.onChangeFileContent)"
ng-required="true"
yml="true"
placeholder="# Define or paste the content of your docker compose file here"
placeholder="Define or paste the content of your docker compose file here"
>
<editor-description>
You can get more information about Compose file format in the
@@ -55,7 +55,7 @@
value="$ctrl.formValues.StackFileContent"
on-change="($ctrl.onChangeFileContent)"
yml="true"
placeholder="# Define or paste the content of your docker compose file here"
placeholder="Define or paste the content of your docker compose file here"
ng-required="true"
>
</web-editor-form>

View File

@@ -18,7 +18,7 @@
value="$ctrl.formValues.StackFileContent"
on-change="($ctrl.onChangeFileContent)"
yml="true"
placeholder="# Define or paste the content of your manifest here"
placeholder="Define or paste the content of your manifest here"
ng-required="true"
>
<editor-description>

View File

@@ -1,4 +1,5 @@
import { EnvironmentStatus } from '@/react/portainer/environments/types';
import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/service';
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
@@ -46,6 +47,14 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment && endpoint.Status === EnvironmentStatus.Down) {
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
}
// use selfsubject access review to check if we can connect to the kubernetes environment
// because it's gets a fast response, and is accessible to all users
try {
await getSelfSubjectAccessReview(endpoint.Id, 'default');
} catch (e) {
throw new Error('Environment is unreachable.');
}
} catch (e) {
let params = {};

View File

@@ -1,132 +1,144 @@
<div class="datatable">
<!-- toolbar header actions and settings -->
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col gap-1">
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col !gap-0">
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="'box'"></pr-icon>
</div>
Applications
</div>
<div class="form-group namespaces !mb-0 !mr-0 min-w-[280px]">
<div class="input-group">
<span class="input-group-addon">
<pr-icon icon="'filter'"></pr-icon>
Namespace
</span>
<select
class="form-control"
ng-model="$ctrl.state.namespace"
ng-change="$ctrl.onChangeNamespace()"
data-cy="component-namespaceSelect"
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
>
</select>
</div>
</div>
<div class="searchBar vertical-center !mr-0 min-w-[280px]">
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for an application..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="k8sApp-searchApplicationsInput"
/>
</div>
<div class="actionBar !mr-0 !gap-3">
<button
ng-if="$ctrl.isPrimary"
type="button"
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sApp-removeAppButton"
>
<pr-icon icon="'trash-2'"></pr-icon>
Remove
</button>
<button
ng-if="$ctrl.isPrimary"
type="button"
class="btn btn-sm btn-secondary vertical-center !ml-0 h-fit"
ui-sref="kubernetes.applications.new"
data-cy="k8sApp-addApplicationButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
</button>
<button
ng-if="$ctrl.isPrimary"
type="button"
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
ui-sref="kubernetes.deploy"
data-cy="k8sApp-deployFromManifestButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
</button>
</div>
<div class="settings" data-cy="k8sApp-tableSettings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle aria-label="Settings">
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="applications_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input
id="setting_auto_refresh"
type="checkbox"
ng-model="$ctrl.settings.repeater.autoRefresh"
ng-change="$ctrl.onSettingsRepeaterChange()"
data-cy="k8sApp-autoRefreshCheckbox"
/>
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select
id="settings_refresh_rate"
ng-model="$ctrl.settings.repeater.refreshRate"
ng-change="$ctrl.onSettingsRepeaterChange()"
class="small-select"
data-cy="k8sApp-refreshRateDropdown"
>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
<div class="settings" data-cy="k8sApp-tableSettings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle aria-label="Settings">
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="applications_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input
id="setting_auto_refresh"
type="checkbox"
ng-model="$ctrl.settings.repeater.autoRefresh"
ng-change="$ctrl.onSettingsRepeaterChange()"
data-cy="k8sApp-autoRefreshCheckbox"
/>
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select
id="settings_refresh_rate"
ng-model="$ctrl.settings.repeater.refreshRate"
ng-change="$ctrl.onSettingsRepeaterChange()"
class="small-select"
data-cy="k8sApp-refreshRateDropdown"
>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
</div>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
<div>
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
</div>
</div>
</div>
</span>
</div>
<div class="actionBar !mr-0 !gap-3">
<button
ng-if="$ctrl.isPrimary"
type="button"
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sApp-removeAppButton"
>
<pr-icon icon="'trash-2'"></pr-icon>
Remove
</button>
<button
ng-if="$ctrl.isPrimary"
hide-deployment-option="form"
type="button"
class="btn btn-sm btn-secondary vertical-center !ml-0 h-fit"
ui-sref="kubernetes.applications.new"
data-cy="k8sApp-addApplicationButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
</button>
<button
ng-if="$ctrl.isPrimary"
type="button"
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
ui-sref="kubernetes.deploy"
data-cy="k8sApp-deployFromManifestButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
</button>
</div>
<div class="searchBar">
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="k8sApp-searchApplicationsInput"
/>
</div>
<div class="form-group namespaces !mb-0 !mr-0 !h-[30px] min-w-[140px]">
<div class="input-group">
<span class="input-group-addon">
<pr-icon icon="'filter'" size="'sm'"></pr-icon>
Namespace
</span>
<select
class="form-control !h-[30px] !py-1"
ng-model="$ctrl.state.namespace"
ng-change="$ctrl.onChangeNamespace()"
data-cy="component-namespaceSelect"
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
>
</select>
</div>
</span>
</div>
</div>
</div>
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<span class="small text-muted vertical-center mt-1">
<pr-icon icon="'info'" mode="'primary'" class="vertical-center"></pr-icon>
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
System resources are hidden, this can be changed in the table settings.
</span>
</div>
<div class="w-full">
<div class="w-fit">
<insights-box class-name="'mt-2'" type="'slim'" header="'From 2.18 on, you can filter this view by namespace.'" insight-close-id="'k8s-namespace-filtering'"></insights-box>
</div>
</div>
</div>
<!-- data table content -->
<div ng-class="{ 'table-responsive': $ctrl.isPrimary, 'inner-datatable': !$ctrl.isPrimary }">
@@ -232,6 +244,7 @@
</thead>
<tbody>
<tr
ng-show="!$ctrl.isAppsLoading"
ng-click="$ctrl.expandItem(item, !$ctrl.isItemExpanded(item))"
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.applyFilters | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
ng-class="{ active: item.Checked, interactive: $ctrl.isExpandable(item), 'secondary-body': !$ctrl.isPrimary }"
@@ -323,16 +336,16 @@
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<tr ng-if="$ctrl.isAppsLoading">
<td colspan="8" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<tr ng-if="$ctrl.state.filteredDataSet.length === 0 && !$ctrl.isAppsLoading">
<td colspan="8" class="text-muted text-center">No application available.</td>
</tr>
</tbody>
</table>
</div>
<div ng-if="$ctrl.isPrimary" class="footer pl-5" ng-if="$ctrl.dataset">
<div class="footer pl-5" ng-if="$ctrl.isPrimary && $ctrl.dataset">
<div class="infoBar !ml-0" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">

View File

@@ -18,6 +18,7 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatabl
namespaces: '<',
namespace: '<',
onChangeNamespaceDropdown: '<',
isAppsLoading: '<',
isSystemResources: '<',
setSystemResources: '<',
},

View File

@@ -144,15 +144,17 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
};
this.updateNamespace = function () {
if (this.namespaces) {
const namespaces = [{ Name: 'All namespaces', Value: '', IsSystem: false }];
this.namespaces.find((ns) => {
if (!this.settings.showSystem && ns.IsSystem) {
return false;
}
namespaces.push({ Name: ns.Name, Value: ns.Name, IsSystem: ns.IsSystem });
});
this.state.namespaces = namespaces;
if (this.namespaces && this.settingsLoaded) {
const allNamespacesOption = { Name: 'All namespaces', Value: '', IsSystem: false };
const visibleNamespaceOptions = this.namespaces
.filter((ns) => {
if (!this.settings.showSystem && ns.IsSystem) {
return false;
}
return true;
})
.map((ns) => ({ Name: ns.Name, Value: ns.Name, IsSystem: ns.IsSystem }));
this.state.namespaces = [allNamespacesOption, ...visibleNamespaceOptions];
if (this.state.namespace && !this.state.namespaces.find((ns) => ns.Name === this.state.namespace)) {
if (this.state.namespaces.length > 1) {
@@ -216,7 +218,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
this.setSystemResources && this.setSystemResources(this.settings.showSystem);
}
this.settingsLoaded = true;
// Set the default selected namespace
if (!this.state.namespace) {
this.state.namespace = this.namespace;

View File

@@ -1,115 +1,122 @@
<div class="datatable">
<!-- table title and action menu -->
<div class="toolBar !flex-col gap-1">
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
<!-- title -->
<div class="toolBar !flex-col !gap-0">
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="'list'"></pr-icon>
</div>
Stacks
</div>
<!-- actions -->
<div class="form-group namespaces !mb-0 !mr-0 min-w-[280px]">
<div class="input-group">
<span class="input-group-addon">
<pr-icon icon="'filter'"></pr-icon>
Namespace
</span>
<select
class="form-control"
ng-model="$ctrl.state.namespace"
ng-change="$ctrl.onChangeNamespace()"
data-cy="component-namespaceSelect"
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
<div class="actionBar !mr-0 !gap-3">
<button
type="button"
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sApp-removeStackButton"
>
</select>
</div>
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'" class-name="'!h-3'"></pr-icon>
<input
type="text"
class="searchInput min-w-min self-start"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for a stack..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="actionBar !mr-0 !gap-3">
<button
type="button"
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sApp-removeStackButton"
>
<pr-icon icon="'trash-2'"></pr-icon>
Remove
</button>
<div class="settings" data-cy="k8sApp-StackTableSettings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle>
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="applications_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input
id="setting_auto_refresh"
type="checkbox"
ng-model="$ctrl.settings.repeater.autoRefresh"
ng-change="$ctrl.onSettingsRepeaterChange()"
data-cy="k8sApp-autoRefreshCheckbox-stack"
/>
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select
id="settings_refresh_rate"
ng-model="$ctrl.settings.repeater.refreshRate"
ng-change="$ctrl.onSettingsRepeaterChange()"
class="small-select"
data-cy="k8sApp-refreshRateDropdown-stack"
>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
<pr-icon icon="'trash-2'"></pr-icon>
Remove
</button>
<div class="settings" data-cy="k8sApp-StackTableSettings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle>
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="applications_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input
id="setting_auto_refresh"
type="checkbox"
ng-model="$ctrl.settings.repeater.autoRefresh"
ng-change="$ctrl.onSettingsRepeaterChange()"
data-cy="k8sApp-autoRefreshCheckbox-stack"
/>
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select
id="settings_refresh_rate"
ng-model="$ctrl.settings.repeater.refreshRate"
ng-change="$ctrl.onSettingsRepeaterChange()"
class="small-select"
data-cy="k8sApp-refreshRateDropdown-stack"
>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
</div>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton-stack">Close</a>
<div>
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton-stack">Close</a>
</div>
</div>
</div>
</div>
</span>
</span>
</div>
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'" class-name="'!h-3'"></pr-icon>
<input
type="text"
class="searchInput min-w-min self-start"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="form-group namespaces !mb-0 !mr-0 !h-[30px] w-fit min-w-[140px]">
<div class="input-group">
<span class="input-group-addon">
<pr-icon icon="'filter'" size="'sm'"></pr-icon>
Namespace
</span>
<select
class="form-control !h-[30px] !py-1"
ng-model="$ctrl.state.namespace"
ng-change="$ctrl.onChangeNamespace()"
data-cy="component-namespaceSelect"
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
>
</select>
</div>
</div>
</div>
</div>
<!-- info text -->
<div class="flex w-full flex-row">
<span class="small text-muted vertical-center mt-1" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<span class="small text-muted vertical-center mt-1">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
System resources are hidden, this can be changed in the table settings.
</span>
</div>
<div class="w-full">
<div class="w-fit">
<insights-box class-name="'mt-2'" type="'slim'" header="'From 2.18 on, you can filter this view by namespace.'" insight-close-id="'k8s-namespace-filtering'"></insights-box>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table">
@@ -161,6 +168,7 @@
</thead>
<tbody>
<tr
ng-show="!$ctrl.isAppsLoading"
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter: $ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
ng-class="{ active: item.Checked, 'datatable-highlighted': item.Highlighted }"
ng-click="$ctrl.expandItem(item, !item.Expanded)"
@@ -213,10 +221,10 @@
>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<tr ng-if="$ctrl.isAppsLoading">
<td colspan="5" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<tr ng-if="$ctrl.state.filteredDataSet.length === 0 && !$ctrl.isAppsLoading">
<td colspan="5" class="text-muted text-center">No stack available.</td>
</tr>
</tbody>

View File

@@ -13,6 +13,7 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDa
namespaces: '<',
namespace: '<',
onChangeNamespaceDropdown: '<',
isAppsLoading: '<',
isSystemResources: '<',
setSystemResources: '<',
},

View File

@@ -11,7 +11,10 @@
<div class="actionBar">
<form class="form-horizontal" name="addUserHelmRepoForm">
<div class="form-group">
<span class="col-sm-12 text-muted small"> Add a Helm repository. All Helm charts in the repository will be added to the list. </span>
<span class="col-sm-12 text-muted small">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Add a Helm repository. All Helm charts in the repository will be added to the list.
</span>
</div>
<div class="form-group mb-2">

View File

@@ -141,7 +141,9 @@ export default class HelmTemplatesController {
try {
const resourcePools = await this.KubernetesResourcePoolService.get();
const nonSystemNamespaces = resourcePools.filter((resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
const nonSystemNamespaces = resourcePools.filter(
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
);
this.state.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
this.state.resourcePool = this.state.resourcePools[0];
} catch (err) {

View File

@@ -124,7 +124,7 @@
value="$ctrl.state.values"
on-change="($ctrl.editorUpdate)"
yml="true"
placeholder="# Define or paste the content of your values yaml file here"
placeholder="Define or paste the content of your values yaml file here"
>
<editor-description class="vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>

View File

@@ -159,7 +159,7 @@
value="$ctrl.formValues.DataYaml"
on-change="($ctrl.editorUpdate)"
yml="true"
placeholder="# Define or paste key-value pairs, one pair per line"
placeholder="Define or paste key-value pairs, one pair per line"
>
</web-editor-form>
</div>

View File

@@ -3,7 +3,7 @@
identifier="application-details-yaml"
value="$ctrl.data"
yml="true"
placeholder="# Define or paste the content of your manifest here"
placeholder="Define or paste the content of your manifest here"
read-only="true"
hide-title="true"
height="{{ $ctrl.expanded ? '800px' : '500px' }}"

View File

@@ -19,7 +19,7 @@
on-change="($ctrl.onChangeFileContent)"
ng-required="true"
yml="true"
placeholder="# Define or paste the content of your manifest file here"
placeholder="Define or paste the content of your manifest file here"
>
<editor-description>
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>

View File

@@ -13,7 +13,7 @@
on-change="($ctrl.onChangeFileContent)"
ng-required="true"
yml="true"
placeholder="# Define or paste the content of your manifest file here"
placeholder="Define or paste the content of your manifest file here"
>
<editor-description>
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>

View File

@@ -18,7 +18,6 @@ class KubernetesNamespaceService {
this.deleteAsync = this.deleteAsync.bind(this);
this.getJSONAsync = this.getJSONAsync.bind(this);
this.updateFinalizeAsync = this.updateFinalizeAsync.bind(this);
this.refreshCacheAsync = this.refreshCacheAsync.bind(this);
}
/**
@@ -85,15 +84,9 @@ class KubernetesNamespaceService {
if (name) {
return this.$async(this.getAsync, name);
}
const cachedAllowedNamespaces = this.LocalStorage.getAllowedNamespaces();
if (cachedAllowedNamespaces) {
updateNamespaces(cachedAllowedNamespaces);
return cachedAllowedNamespaces;
} else {
const allowedNamespaces = await this.getAllAsync();
this.LocalStorage.storeAllowedNamespaces(allowedNamespaces);
return allowedNamespaces;
}
const allowedNamespaces = await this.getAllAsync();
updateNamespaces(allowedNamespaces);
return allowedNamespaces;
}
/**
@@ -104,7 +97,6 @@ class KubernetesNamespaceService {
const payload = KubernetesNamespaceConverter.createPayload(namespace);
const params = {};
const data = await this.KubernetesNamespaces().create(params, payload).$promise;
await this.refreshCacheAsync();
return data;
} catch (err) {
throw new PortainerError('Unable to create namespace', err);
@@ -115,14 +107,6 @@ class KubernetesNamespaceService {
return this.$async(this.createAsync, namespace);
}
async refreshCacheAsync() {
this.LocalStorage.deleteAllowedNamespaces();
const allowedNamespaces = await this.getAllAsync();
this.LocalStorage.storeAllowedNamespaces(allowedNamespaces);
updateNamespaces(allowedNamespaces);
return allowedNamespaces;
}
/**
* DELETE
*/

View File

@@ -20,8 +20,9 @@
on-publishing-mode-click="(ctrl.onPublishingModeClick)"
is-primary="true"
namespaces="ctrl.state.namespaces"
namespace="ctrl.state.namespace"
namespace="ctrl.state.namespaceName"
on-change-namespace-dropdown="(ctrl.onChangeNamespaceDropdown)"
is-apps-loading="ctrl.state.isAppsLoading"
is-system-resources="ctrl.state.isSystemResources"
set-system-resources="(ctrl.setSystemResources)"
>
@@ -36,8 +37,9 @@
refresh-callback="ctrl.getApplications"
remove-action="ctrl.removeStacksAction"
namespaces="ctrl.state.namespaces"
namespace="ctrl.state.namespace"
namespace="ctrl.state.namespaceName"
on-change-namespace-dropdown="(ctrl.onChangeNamespaceDropdown)"
is-apps-loading="ctrl.state.isAppsLoading"
is-system-resources="ctrl.state.isSystemResources"
set-system-resources="(ctrl.setSystemResources)"
>

View File

@@ -13,11 +13,11 @@ class KubernetesApplicationsController {
$async,
$state,
$scope,
Authentication,
Notifications,
KubernetesApplicationService,
HelmService,
KubernetesConfigurationService,
Authentication,
LocalStorage,
StackService,
KubernetesNamespaceService
@@ -25,6 +25,7 @@ class KubernetesApplicationsController {
this.$async = $async;
this.$state = $state;
this.$scope = $scope;
this.Authentication = Authentication;
this.Notifications = Notifications;
this.KubernetesApplicationService = KubernetesApplicationService;
this.HelmService = HelmService;
@@ -142,16 +143,19 @@ class KubernetesApplicationsController {
});
}
onChangeNamespaceDropdown(namespace) {
this.state.namespace = namespace;
this.getApplicationsAsync();
onChangeNamespaceDropdown(namespaceName) {
this.state.namespaceName = namespaceName;
// save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}'
this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName);
return this.$async(this.getApplicationsAsync);
}
async getApplicationsAsync() {
try {
this.state.isAppsLoading = true;
const [applications, configurations] = await Promise.all([
this.KubernetesApplicationService.get(this.state.namespace),
this.KubernetesConfigurationService.get(this.state.namespace),
this.KubernetesApplicationService.get(this.state.namespaceName),
this.KubernetesConfigurationService.get(this.state.namespaceName),
]);
const configuredApplications = KubernetesConfigurationHelper.getApplicationConfigurations(applications, configurations);
const { helmApplications, nonHelmApplications } = KubernetesApplicationHelper.getNestedApplications(configuredApplications);
@@ -163,6 +167,8 @@ class KubernetesApplicationsController {
this.$scope.$apply();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
} finally {
this.state.isAppsLoading = false;
}
}
@@ -184,14 +190,25 @@ class KubernetesApplicationsController {
stacks: [],
ports: [],
namespaces: [],
namespace: '',
namespaceName: '',
isSystemResources: undefined,
};
this.user = this.Authentication.getUserDetails();
this.state.namespaces = await this.KubernetesNamespaceService.get();
this.state.namespaces = this.state.namespaces.filter((n) => n.Status !== 'Terminating');
const savedNamespace = this.LocalStorage.getNamespaceFilter(this.endpoint.Id, this.user.ID); // could be null if not found, and '' if all namepsaces is selected
const preferredNamespace = savedNamespace === null ? 'default' : savedNamespace;
this.state.namespaces = this.state.namespaces.filter((n) => n.Status === 'Active');
this.state.namespaces = _.sortBy(this.state.namespaces, 'Name');
this.state.namespace = this.state.namespaces.length ? (this.state.namespaces.find((n) => n.Name === 'default') ? 'default' : this.state.namespaces[0].Name) : '';
// set all namespaces ('') if there are no namespaces, or if all namespaces is selected
if (!this.state.namespaces.length || preferredNamespace === '') {
this.state.namespaceName = '';
} else {
// otherwise, set the preferred namespaceName if it exists, otherwise set the first namespaceName
this.state.namespaceName = this.state.namespaces.find((n) => n.Name === preferredNamespace) ? preferredNamespace : this.state.namespaces[0].Name;
}
await this.getApplications();

View File

@@ -114,11 +114,11 @@
value="ctrl.stackFileContent"
yml="true"
identifier="kubernetes-deploy-editor"
placeholder="# Define or paste the content of your manifest file here"
placeholder="Define or paste the content of your manifest file here"
on-change="(ctrl.onChangeFileContent)"
>
<editor-description>
<div class="text-muted small flex gap-1" ng-show="ctrl.stack.IsComposeFormat">
<div class="flex gap-1" ng-show="ctrl.stack.IsComposeFormat">
<pr-icon icon="'alert-circle'" mode="'warning'" class-name="'!mt-1'"></pr-icon>
<div>
<p>
@@ -127,17 +127,17 @@
conversion tool which enables this. The reason for this is because Kompose now poses a security risk, since it has a number of Common Vulnerabilities and
Exposures (CVEs).
</p>
<p
>Unfortunately, while the Kompose project has a maintainer and is part of the CNCF, it is not being actively maintained. Releases are very infrequent and
new pull requests to the project (including ones we've submitted) are taking months to be merged, with new CVEs arising in the meantime.</p
>
<p>
Unfortunately, while the Kompose project has a maintainer and is part of the CNCF, it is not being actively maintained. Releases are very infrequent and new
pull requests to the project (including ones we've submitted) are taking months to be merged, with new CVEs arising in the meantime.
</p>
<p>
We advise installing your own instance of Kompose in a sandbox environment, performing conversions of your Docker Compose files to Kubernetes manifests and
using those manifests to set up applications.
</p>
</div>
</div>
<span class="text-muted small" ng-show="!ctrl.stack.IsComposeFormat">
<span ng-show="!ctrl.stack.IsComposeFormat">
<p class="vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
@@ -212,7 +212,7 @@
</div>
<!-- #end region IMAGE FIELD -->
<div class="col-sm-12 !p-0">
<div class="col-sm-12 mb-4 !p-0">
<annotations-be-teaser></annotations-be-teaser>
</div>

View File

@@ -1208,22 +1208,31 @@ class KubernetesCreateApplicationController {
]);
this.nodesLimits = nodesLimits;
const nonSystemNamespaces = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
const nonSystemNamespaces = _.filter(
resourcePools,
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
);
this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name);
this.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
this.formValues.ResourcePool = this.resourcePools[0];
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
if (!this.formValues.ResourcePool) {
return;
}
// this.state.nodes.memory and this.state.nodes.cpu are used to calculate the slider limits, so set them before calling updateSliders()
_.forEach(nodes, (item) => {
this.state.nodes.memory += filesizeParser(item.Memory);
this.state.nodes.cpu += item.CPU;
});
if (this.resourcePools.length) {
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
this.updateNamespaceLimits(namespaceWithQuota);
this.updateSliders(namespaceWithQuota);
}
this.formValues.ResourcePool = this.resourcePools[0];
if (!this.formValues.ResourcePool) {
return;
}
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
this.nodeNumber = nodes.length;
@@ -1281,9 +1290,6 @@ class KubernetesCreateApplicationController {
this.formValues.IsPublishingService = this.formValues.PublishedPorts.length > 0;
this.oldFormValues = angular.copy(this.formValues);
this.updateNamespaceLimits(namespaceWithQuota);
this.updateSliders(namespaceWithQuota);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {

View File

@@ -290,7 +290,6 @@
<!-- table -->
<kubernetes-application-services-table
services="ctrl.application.Services"
namespaces="ctrl.allNamespaces"
application="ctrl.application"
public-url="ctrl.state.publicUrl"
></kubernetes-application-services-table>

View File

@@ -196,7 +196,10 @@ class KubernetesCreateConfigurationController {
try {
const resourcePools = await this.KubernetesResourcePoolService.get();
this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.resourcePools = _.filter(
resourcePools,
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
);
this.formValues.ResourcePool = this.resourcePools[0];
await this.getConfigurations();

View File

@@ -165,7 +165,10 @@ class KubernetesConfigureController {
const allResourcePools = await this.KubernetesResourcePoolService.get();
const resourcePools = _.filter(
allResourcePools,
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
(resourcePool) =>
!KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) &&
!KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name) &&
resourcePool.Namespace.Status === 'Active'
);
ingressesToDel.forEach((ingress) => {

Some files were not shown because too many files have changed in this diff Show More