Compare commits

...

60 Commits

Author SHA1 Message Date
Simon Meng
e421e04dd1 fix(stack) ignore username and password when authentication is disabled EE-161 2021-06-22 13:30:54 +12:00
Chaim Lev-Ari
a0e3129f0d fix(stacks): clear directory if success 2021-06-20 12:43:35 +03:00
Chaim Lev-Ari
f487a067c7 feat(stacks): don't accept prune and env on update git 2021-06-11 07:23:49 +03:00
Chaim Lev-Ari
37dd521b9e feat(stacks): backup stack folder before cloning 2021-06-11 07:23:26 +03:00
Chaim Lev-Ari
27cfb692f4 feat(stacks): set stopped stacks status to activate when updating 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
4b054f9dab fix(stacks): reload state when deployed 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
6cbbba596a fix(stacks): show same size description 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
a5c5d34de8 feat(stack): show loading after confirmation 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
75da9ac2aa feat(stacks): redeploy git stacks form
[EE-666]
2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
fea8ea6c73 feat(stacks): redeploy git stack api (#5112) 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
d982f89167 feat(git): save git config when creating stack (#5048)
* feat(git): save git config when creating stack

* chore(fs): test fileExists

* fix(git): fix tests to use CloneRepository

* refactor(git): move options to new object
2021-06-06 13:02:24 +03:00
testA113
2fb17c9cf9 Merge pull request #4983 from portainer/feat/EE-352/CE-truncate-image-name-in-tables
feat(k8s): truncate image name in tables
2021-06-04 15:20:26 +12:00
dbuduev
c8d78ad15f Merge pull request #5146 from portainer/feat/EE-872/test-scaffolding
feat(bolt): implement bolt db test store EE-872
2021-06-04 13:44:56 +12:00
Dennis Buduev
96a6129d8a feat(bolt): implement boltdb test store EE-872 2021-06-04 13:33:18 +12:00
Alice Groux
b8660ed2a0 feat(k8s/applications): reorder placement policies and select mandatory by default (#5063) 2021-06-03 13:42:44 +02:00
Chaim Lev-Ari
9ec1f2ed6d fix(endpoints): set sysctl setting for new endpoints (#5028) 2021-06-03 11:36:54 +02:00
yi-portainer
8bfa5132cd Merge branch 'release/2.5' into develop 2021-06-03 20:39:54 +12:00
wheresolivia
cafcebe27e Merge pull request #4668 from portainer/feat-4667-custom-portainer-folder
chore(dev-build): custom portainer data folder
2021-06-03 13:28:33 +12:00
wheresolivia
ea6df891c3 Merge pull request #5014 from portainer/feat/EE-445/resourcepool-namespace
feat(k8s): replace resourcepool with namespace EE-445
2021-06-02 11:30:20 +12:00
Chaim Lev-Ari
230f8fddc3 fix(kube): replace remaining resource pool texts 2021-06-01 11:56:47 +03:00
Chaim Lev-Ari
6734f0ab74 feat(k8s): replace resource pool with name space 2021-06-01 11:52:05 +03:00
Chaim Lev-Ari
3e60167aeb feat(k8s/applications): default to isolated application 2021-06-01 11:52:05 +03:00
Chaim Lev-Ari
8a4902f15a feat(k8s/applications): rephrase descriptions 2021-06-01 11:52:05 +03:00
yi-portainer
1d46f2bb35 * update portainer version to 2.5.1 2021-05-28 10:21:29 +12:00
yi-portainer
dde0467b89 Merge branch 'release/2.5' into develop 2021-05-28 10:16:38 +12:00
wheresolivia
a2a197b14b Merge pull request #5033 from portainer/fix/CE-575/type-downgrade-error
fix(portainer): Fix the typo in the downgrade error message
2021-05-27 16:46:48 +12:00
cong meng
ee403ca32a fix(image) Confirmation modal on builder output view EE-816 (#5114)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-27 13:52:02 +12:00
fhanportainer
d7fcfee2a2 fix(templates): checking windows endpoint and template properties. (#5108)
* fix(templates): checking windows endpoint and template properties.

* fix(templates): removed debug code.

* fix(templates): fixed type issue in custom template.
2021-05-27 08:56:13 +12:00
cong meng
3018801fc0 fix(image) Confirmation modal on builder output view EE-816 (#5107)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-26 17:11:32 +12:00
fhanportainer
6bfbf58cdb fix(template): fixed disabled deploy button EE-812 (#5105) 2021-05-25 18:55:50 +02:00
dbuduev
3568fe9e52 feat(git) git clone improvements [EE-451] (#5070) 2021-05-24 17:27:07 +12:00
yi-portainer
2270de73ee Merge branch 'release/2.5' into develop 2021-05-24 08:53:10 +12:00
Chaim Lev-Ari
819faa3948 fix(k8s/proxy): proxy healthz request to k8s api (#5090) 2021-05-21 00:20:08 +02:00
wheresolivia
ef8794c2b9 Merge pull request #5079 from portainer/fix/EE-769/code-editor-prompt-on-change
fix(stacks): check for editor change before setting as dirty
2021-05-20 18:44:46 +12:00
Felix Han
5618794927 fix(k8s-config): check for config editor change before setting as dirty 2021-05-20 11:46:17 +12:00
Felix Han
47d462f085 fix(web-editor): check for editor change before setting as dirty. 2021-05-20 10:22:07 +12:00
zees-dev
0114766d50 Merge pull request #5086 from portainer/revert-5084-feat/EE-341/EE-777/oauth-memberships-teaser
Revert "feat(oauth/team-memberships): EE oauth team memberships teaser"
2021-05-20 10:21:11 +12:00
Stéphane Busso
2b94aa5aa6 Revert "feat(oauth/team-memberships): EE oauth team memberships teaser" 2021-05-20 10:03:59 +12:00
cong meng
746e738f1d Merge pull request #5084 from portainer/feat/EE-341/EE-777/oauth-memberships-teaser
feat(oauth/team-memberships): EE oauth team memberships teaser
2021-05-20 09:21:10 +12:00
zees-dev
29f5008c5f EE oauth team memberships feature teaser 2021-05-19 16:15:46 +12:00
Felix Han
e54d99fd3d fix(stacks): remove line breaks in web editors value 2021-05-19 12:09:11 +12:00
Chaim Lev-Ari
b3784792fe fix(stacks): show containers only for standalone (#5080) 2021-05-18 23:06:04 +02:00
Chaim Lev-Ari
87e7d8ada8 fix(stacks): check for editor change before setting as dirty 2021-05-18 14:08:23 +03:00
yi-portainer
af03d91e39 Merge branch 'release/2.5' into develop 2021-05-18 17:02:31 +12:00
yi-portainer
71635834c7 * update portainer version to 2.5.0
(cherry picked from commit 43702c2516)
2021-05-13 18:32:42 +12:00
yi-portainer
43702c2516 * update portainer version to 2.5.0 2021-05-13 18:30:34 +12:00
Chaim Lev-Ari
a21798f518 fix(docker/containers): show sysctl control (#5051) 2021-05-12 02:29:35 +02:00
dbuduev
3641158daf fix: docker-compose use custom config.json to access private images (#5058)
cherry-picking commit a6b289c9.

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
2021-05-11 23:05:00 +02:00
Chaim Lev-Ari
0ac6274712 fix(docker/services): create a service webhook (#5052) 2021-05-11 10:59:42 +12:00
Chaim Lev-Ari
886d6764be fix(docker): set image pulls as valid if failed fetching (#5055) 2021-05-11 09:24:29 +12:00
Chaim Lev-Ari
39e24ec93f fix(docker): set image pulls as valid if failed fetching (#5007) 2021-05-07 15:38:58 +12:00
Chaim Lev-Ari
b7980f1b60 fix(k8s/ingress): remove only selected ingress (#5035)
* fix(k8s/ingress): remove only selected ingress

* fix(k8s/ingress): remove ingress from namespace
2021-05-07 09:49:56 +12:00
Maxime Bajeux
ce04944ce6 fix(portainer): Fix the type in the downgrade error message 2021-05-05 11:44:00 +02:00
Hui
564bea7575 fix(ACI): ACI UAC breaks when redeploying container with same name asone already existing EE-645 (#5030)
* add existing continer instance checking logic

* modify response status code and err message

* return json instead of plain text for err msg

* Update api/http/proxy/factory/azure/containergroup.go

* Update api/http/proxy/factory/azure/containergroup.go

* Update api/http/proxy/factory/azure/containergroup.go

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2021-05-05 20:26:31 +12:00
Chaim Lev-Ari
dcc77e50e5 fix(docker/images): show image selector advanced mode (#5032) 2021-05-05 20:16:59 +12:00
Stéphane Busso
317ebe2bfc Revert "feat(edge) EE-596 Update the version of agent to 2.4.0 in agent deploy command on the adding edge screen (#5021)" (#5031)
This reverts commit 7e2ce3ffc2.
2021-05-05 16:24:20 +12:00
cong meng
7e2ce3ffc2 feat(edge) EE-596 Update the version of agent to 2.4.0 in agent deploy command on the adding edge screen (#5021)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-29 16:25:09 +12:00
alice groux
872a8262f1 feat(k8s): add full name on hovering over the image name 2021-04-14 14:59:17 +02:00
alice groux
c339afb562 feat(k8s): cut image name to 64 chars with truncate filter in all applications datatables 2021-04-13 16:09:37 +02:00
Chaim Lev-Ari
78661b50ca chore(dev-build): custom portainer data folder 2021-04-12 08:49:07 +03:00
121 changed files with 2073 additions and 612 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ api/cmd/portainer/portainer*
__debug_bin
api/docs
.idea
.env

BIN
api/archive/testdata/sample_archive.zip vendored Normal file

Binary file not shown.

View File

@@ -3,10 +3,13 @@ package archive
import (
"archive/zip"
"bytes"
"fmt"
"github.com/pkg/errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
@@ -52,3 +55,60 @@ func extractFileFromArchive(file *zip.File, dest string) error {
return outFile.Close()
}
// UnzipFile will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2).
func UnzipFile(src string, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
p := filepath.Join(dest, f.Name)
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if !strings.HasPrefix(p, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", p)
}
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(p, os.ModePerm)
continue
}
err = unzipFile(f, p)
if err != nil {
return err
}
}
return nil
}
func unzipFile(f *zip.File, p string) error {
// Make File
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
return errors.Wrapf(err, "unzipFile: can't make a path %s", p)
}
outFile, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return errors.Wrapf(err, "unzipFile: can't create file %s", p)
}
defer outFile.Close()
rc, err := f.Open()
if err != nil {
return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name)
}
defer rc.Close()
_, err = io.Copy(outFile, rc)
if err != nil {
return errors.Wrapf(err, "unzipFile: can't copy an archived file content")
}
return nil
}

32
api/archive/zip_test.go Normal file
View File

@@ -0,0 +1,32 @@
package archive
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestUnzipFile(t *testing.T) {
dir, err := ioutil.TempDir("", "unzip-test-")
assert.NoError(t, err)
defer os.RemoveAll(dir)
/*
Archive structure.
├── 0
│ ├── 1
│ │ └── 2.txt
│ └── 1.txt
└── 0.txt
*/
err = UnzipFile("./testdata/sample_archive.zip", dir)
assert.NoError(t, err)
archiveDir := dir + "/sample_archive"
assert.FileExists(t, filepath.Join(archiveDir, "0.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1", "2.txt"))
}

View File

@@ -0,0 +1,73 @@
package bolttest
import (
"io/ioutil"
"log"
"os"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/filesystem"
)
var errTempDir = errors.New("can't create a temp dir")
func MustNewTestStore(init bool) (*bolt.Store, func()) {
store, teardown, err := NewTestStore(init)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
}
log.Fatal(err)
}
return store, teardown
}
func NewTestStore(init bool) (*bolt.Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
dataStorePath, err := ioutil.TempDir("", "boltdb")
if err != nil {
return nil, nil, errors.Wrap(errTempDir, err.Error())
}
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
return nil, nil, err
}
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
return nil, nil, err
}
err = store.Open()
if err != nil {
return nil, nil, err
}
if init {
err = store.Init()
if err != nil {
return nil, nil, err
}
}
teardown := func() {
teardown(store, dataStorePath)
}
return store, teardown, nil
}
func teardown(store *bolt.Store, dataStorePath string) {
err := store.Close()
if err != nil {
log.Fatalln(err)
}
err = os.RemoveAll(dataStorePath)
if err != nil {
log.Fatalln(err)
}
}

View File

@@ -4,5 +4,5 @@ import "errors"
var (
ErrObjectNotFound = errors.New("Object not found inside the database")
ErrWrongDBEdition = errors.New("The Portainer database is set for Portainer Business Edition, please follow the instructions in our documention to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrWrongDBEdition = errors.New("The Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
)

View File

@@ -76,7 +76,7 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
}
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
composeWrapper := exec.NewComposeWrapper(assetsPath, dataStorePath, proxyManager)
if composeWrapper != nil {
return composeWrapper
}
@@ -240,6 +240,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
@@ -301,6 +302,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,

View File

@@ -16,17 +16,19 @@ import (
// ComposeWrapper is a wrapper for docker-compose binary
type ComposeWrapper struct {
binaryPath string
dataPath string
proxyManager *proxy.Manager
}
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper {
func NewComposeWrapper(binaryPath, dataPath string, proxyManager *proxy.Manager) *ComposeWrapper {
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
return nil
}
return &ComposeWrapper{
binaryPath: binaryPath,
dataPath: dataPath,
proxyManager: proxyManager,
}
}
@@ -84,6 +86,8 @@ func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpo
var stderr bytes.Buffer
cmd := exec.Command(program, args...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONFIG=%s", w.dataPath))
cmd.Stderr = &stderr
out, err := cmd.Output()

View File

@@ -42,7 +42,7 @@ func Test_UpAndDown(t *testing.T) {
stack, endpoint := setup(t)
w := NewComposeWrapper("", nil)
w := NewComposeWrapper("", "", nil)
err := w.Up(stack, endpoint)
if err != nil {

View File

@@ -279,13 +279,7 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
// FileExists checks for the existence of the specified file.
func (service *Service) FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
return FileExists(filePath)
}
// KeyPairFilesExist checks for the existence of the key files.
@@ -510,3 +504,31 @@ func (service *Service) GetTemporaryPath() (string, error) {
func (service *Service) GetDatastorePath() string {
return service.dataStorePath
}
// FileExists checks for the existence of the specified file.
func FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
func MoveDirectory(originalPath, newPath string) error {
if _, err := os.Stat(originalPath); err != nil {
return err
}
alreadyExists, err := FileExists(newPath)
if err != nil {
return err
}
if alreadyExists {
return errors.New("Target path already exists")
}
return os.Rename(originalPath, newPath)
}

View File

@@ -0,0 +1,55 @@
package filesystem
import (
"fmt"
"math/rand"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_fileSystemService_FileExists_whenFileExistsShouldReturnTrue(t *testing.T) {
service := createService(t)
testHelperFileExists_fileExists(t, service.FileExists)
}
func Test_fileSystemService_FileExists_whenFileNotExistsShouldReturnFalse(t *testing.T) {
service := createService(t)
testHelperFileExists_fileNotExists(t, service.FileExists)
}
func Test_FileExists_whenFileExistsShouldReturnTrue(t *testing.T) {
testHelperFileExists_fileExists(t, FileExists)
}
func Test_FileExists_whenFileNotExistsShouldReturnFalse(t *testing.T) {
testHelperFileExists_fileNotExists(t, FileExists)
}
func testHelperFileExists_fileExists(t *testing.T, checker func(path string) (bool, error)) {
file, err := os.CreateTemp("", t.Name())
assert.NoError(t, err, "CreateTemp should not fail")
t.Cleanup(func() {
os.RemoveAll(file.Name())
})
exists, err := checker(file.Name())
assert.NoError(t, err, "FileExists should not fail")
assert.True(t, exists)
}
func testHelperFileExists_fileNotExists(t *testing.T, checker func(path string) (bool, error)) {
filePath := path.Join(os.TempDir(), fmt.Sprintf("%s%d", t.Name(), rand.Int()))
err := os.RemoveAll(filePath)
assert.NoError(t, err, "RemoveAll should not fail")
exists, err := checker(filePath)
assert.NoError(t, err, "FileExists should not fail")
assert.False(t, exists)
}

View File

@@ -0,0 +1,49 @@
package filesystem
import (
"fmt"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
// temporary function until upgrade to 1.16
func tempDir(t *testing.T) string {
tmpDir, err := os.MkdirTemp("", "dir")
assert.NoError(t, err, "MkdirTemp should not fail")
return tmpDir
}
func Test_movePath_shouldFailIfOriginalPathDoesntExist(t *testing.T) {
tmpDir := tempDir(t)
missingPath := path.Join(tmpDir, "missing")
targetPath := path.Join(tmpDir, "target")
defer os.RemoveAll(tmpDir)
err := MoveDirectory(missingPath, targetPath)
assert.Error(t, err, "move directory should fail when target path exists")
}
func Test_movePath_shouldFailIfTargetPathDoesExist(t *testing.T) {
originalPath := tempDir(t)
missingPath := tempDir(t)
defer os.RemoveAll(originalPath)
defer os.RemoveAll(missingPath)
err := MoveDirectory(originalPath, missingPath)
assert.Error(t, err, "move directory should fail when target path exists")
}
func Test_movePath_success(t *testing.T) {
originalPath := tempDir(t)
defer os.RemoveAll(originalPath)
err := MoveDirectory(originalPath, fmt.Sprintf("%s-old", originalPath))
assert.NoError(t, err)
}

View File

@@ -0,0 +1,22 @@
package filesystem
import (
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func createService(t *testing.T) *Service {
dataStorePath := path.Join(os.TempDir(), t.Name())
service, err := NewService(dataStorePath, "")
assert.NoError(t, err, "NewService should not fail")
t.Cleanup(func() {
os.RemoveAll(dataStorePath)
})
return service
}

219
api/git/azure.go Normal file
View File

@@ -0,0 +1,219 @@
package git
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
)
const (
azureDevOpsHost = "dev.azure.com"
visualStudioHostSuffix = ".visualstudio.com"
)
func isAzureUrl(s string) bool {
return strings.Contains(s, azureDevOpsHost) ||
strings.Contains(s, visualStudioHostSuffix)
}
type azureOptions struct {
organisation, project, repository string
// a user may pass credentials in a repository URL,
// for example https://<username>:<password>@<domain>/<path>
username, password string
}
type azureDownloader struct {
client *http.Client
baseUrl string
}
func NewAzureDownloader(client *http.Client) *azureDownloader {
return &azureDownloader{
client: client,
baseUrl: "https://dev.azure.com",
}
}
func (a *azureDownloader) download(ctx context.Context, destination string, options cloneOptions) error {
zipFilepath, err := a.downloadZipFromAzureDevOps(ctx, options)
if err != nil {
return errors.Wrap(err, "failed to download a zip file from Azure DevOps")
}
defer os.Remove(zipFilepath)
err = archive.UnzipFile(zipFilepath, destination)
if err != nil {
return errors.Wrap(err, "failed to unzip file")
}
return nil
}
func (a *azureDownloader) downloadZipFromAzureDevOps(ctx context.Context, options cloneOptions) (string, error) {
config, err := parseUrl(options.repositoryUrl)
if err != nil {
return "", errors.WithMessage(err, "failed to parse url")
}
downloadUrl, err := a.buildDownloadUrl(config, options.referenceName)
if err != nil {
return "", errors.WithMessage(err, "failed to build download url")
}
zipFile, err := ioutil.TempFile("", "azure-git-repo-*.zip")
if err != nil {
return "", errors.WithMessage(err, "failed to create temp file")
}
defer zipFile.Close()
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
if options.username != "" || options.password != "" {
req.SetBasicAuth(options.username, options.password)
} else if config.username != "" || config.password != "" {
req.SetBasicAuth(config.username, config.password)
}
if err != nil {
return "", errors.WithMessage(err, "failed to create a new HTTP request")
}
res, err := a.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 {
return "", fmt.Errorf("failed to download zip with a status \"%v\"", res.Status)
}
_, err = io.Copy(zipFile, res.Body)
if err != nil {
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
}
return zipFile.Name(), nil
}
func parseUrl(rawUrl string) (*azureOptions, error) {
if strings.HasPrefix(rawUrl, "https://") || strings.HasPrefix(rawUrl, "http://") {
return parseHttpUrl(rawUrl)
}
if strings.HasPrefix(rawUrl, "git@ssh") {
return parseSshUrl(rawUrl)
}
if strings.HasPrefix(rawUrl, "ssh://") {
r := []rune(rawUrl)
return parseSshUrl(string(r[6:])) // remove the prefix
}
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
}
var expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
func parseSshUrl(rawUrl string) (*azureOptions, error) {
path := strings.Split(rawUrl, "/")
unexpectedUrlErr := errors.Errorf("want url %s, got %s", expectedSshUrl, rawUrl)
if len(path) != 4 {
return nil, unexpectedUrlErr
}
return &azureOptions{
organisation: path[1],
project: path[2],
repository: path[3],
}, nil
}
const expectedAzureDevOpsHttpUrl = "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository"
const expectedVisualStudioHttpUrl = "https://organisation.visualstudio.com/project/_git/repository"
func parseHttpUrl(rawUrl string) (*azureOptions, error) {
u, err := url.Parse(rawUrl)
if err != nil {
return nil, errors.Wrap(err, "failed to parse HTTP url")
}
opt := azureOptions{}
switch {
case u.Host == azureDevOpsHost:
path := strings.Split(u.Path, "/")
if len(path) != 5 {
return nil, errors.Errorf("want url %s, got %s", expectedAzureDevOpsHttpUrl, u)
}
opt.organisation = path[1]
opt.project = path[2]
opt.repository = path[4]
case strings.HasSuffix(u.Host, visualStudioHostSuffix):
path := strings.Split(u.Path, "/")
if len(path) != 4 {
return nil, errors.Errorf("want url %s, got %s", expectedVisualStudioHttpUrl, u)
}
opt.organisation = strings.TrimSuffix(u.Host, visualStudioHostSuffix)
opt.project = path[1]
opt.repository = path[3]
default:
return nil, errors.Errorf("unknown azure host in url \"%s\"", rawUrl)
}
opt.username = u.User.Username()
opt.password, _ = u.User.Password()
return &opt, nil
}
func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName string) (string, error) {
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
a.baseUrl,
url.PathEscape(config.organisation),
url.PathEscape(config.project),
url.PathEscape(config.repository))
u, err := url.Parse(rawUrl)
if err != nil {
return "", errors.Wrapf(err, "failed to parse download url path %s", rawUrl)
}
q := u.Query()
// scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0
q.Set("scopePath", "/")
q.Set("download", "true")
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
q.Set("$format", "zip")
q.Set("recursionLevel", "full")
q.Set("api-version", "6.0")
u.RawQuery = q.Encode()
return u.String(), nil
}
const (
branchPrefix = "refs/heads/"
tagPrefix = "refs/tags/"
)
func formatReferenceName(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return strings.TrimPrefix(name, branchPrefix)
}
if strings.HasPrefix(name, tagPrefix) {
return strings.TrimPrefix(name, tagPrefix)
}
return name
}
func getVersionType(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return "branch"
}
if strings.HasPrefix(name, tagPrefix) {
return "tag"
}
return "commit"
}

View File

@@ -0,0 +1,93 @@
package git
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/assert"
)
func TestService_ClonePublicRepository_Azure(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService()
type args struct {
repositoryURLFormat string
referenceName string
username string
password string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Clone Azure DevOps repo branch",
args: args{
repositoryURLFormat: "https://:%s@portainer.visualstudio.com/Playground/_git/dev_integration",
referenceName: "refs/heads/main",
username: "",
password: pat,
},
wantErr: false,
},
{
name: "Clone Azure DevOps repo tag",
args: args{
repositoryURLFormat: "https://:%s@portainer.visualstudio.com/Playground/_git/dev_integration",
referenceName: "refs/tags/v1.1",
username: "",
password: pat,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
err = service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "")
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
})
}
}
func TestService_ClonePrivateRepository_Azure(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService()
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := "https://portainer.visualstudio.com/Playground/_git/dev_integration"
err = service.CloneRepository(dst, repositoryUrl, "refs/heads/main", "", pat)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
func getRequiredValue(t *testing.T, name string) string {
value, ok := os.LookupEnv(name)
if !ok {
t.Fatalf("can't find required env var \"%s\"", name)
}
return value
}
func ensureIntegrationTest(t *testing.T) {
if _, ok := os.LookupEnv("INTEGRATION_TEST"); !ok {
t.Skip("skip an integration test")
}
}

250
api/git/azure_test.go Normal file
View File

@@ -0,0 +1,250 @@
package git
import (
"context"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func Test_buildDownloadUrl(t *testing.T) {
a := NewAzureDownloader(nil)
u, err := a.buildDownloadUrl(&azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
}, "refs/heads/main")
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/items?scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0&versionDescriptor.versionType=branch")
actualUrl, _ := url.Parse(u)
if assert.NoError(t, err) {
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
assert.Equal(t, expectedUrl.Scheme, actualUrl.Scheme)
assert.Equal(t, expectedUrl.Path, actualUrl.Path)
assert.Equal(t, expectedUrl.Query(), actualUrl.Query())
}
}
func Test_parseAzureUrl(t *testing.T) {
type args struct {
url string
}
tests := []struct {
name string
args args
want *azureOptions
wantErr bool
}{
{
name: "Expected SSH URL format starting with ssh://",
args: args{
url: "ssh://git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
},
wantErr: false,
},
{
name: "Expected SSH URL format starting with git@ssh",
args: args{
url: "git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
},
wantErr: false,
},
{
name: "Unexpected SSH URL format",
args: args{
url: "git@ssh.dev.azure.com:v3/Organisation/Repository",
},
wantErr: true,
},
{
name: "Expected HTTPS URL format",
args: args{
url: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
username: "Organisation",
},
wantErr: false,
},
{
name: "HTTPS URL with credentials",
args: args{
url: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
username: "username",
password: "password",
},
wantErr: false,
},
{
name: "HTTPS URL with password",
args: args{
url: "https://:password@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
password: "password",
},
wantErr: false,
},
{
name: "Visual Studio HTTPS URL with credentials",
args: args{
url: "https://username:password@organisation.visualstudio.com/project/_git/repository",
},
want: &azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
username: "username",
password: "password",
},
wantErr: false,
},
{
name: "Unexpected HTTPS URL format",
args: args{
url: "https://Organisation@dev.azure.com/Project/_git/Repository",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseUrl(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("parseUrl() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
})
}
}
func Test_isAzureUrl(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Is Azure url",
args: args{
s: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
},
want: true,
},
{
name: "Is Azure url",
args: args{
s: "https://portainer.visualstudio.com/project/_git/repository",
},
want: true,
},
{
name: "Is NOT Azure url",
args: args{
s: "https://github.com/Organisation/Repository",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isAzureUrl(tt.args.s))
})
}
}
func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
type args struct {
options cloneOptions
}
type basicAuth struct {
username, password string
}
tests := []struct {
name string
args args
want *basicAuth
}{
{
name: "username, password embedded",
args: args{
options: cloneOptions{
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
},
},
want: &basicAuth{
username: "username",
password: "password",
},
},
{
name: "username, password embedded, clone options take precedence",
args: args{
options: cloneOptions{
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
username: "u",
password: "p",
},
},
want: &basicAuth{
username: "u",
password: "p",
},
},
{
name: "no credentials",
args: args{
options: cloneOptions{
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var zipRequestAuth *basicAuth
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if username, password, ok := r.BasicAuth(); ok {
zipRequestAuth = &basicAuth{username, password}
}
w.WriteHeader(http.StatusNotFound) // this makes function under test to return an error
}))
defer server.Close()
a := &azureDownloader{
client: server.Client(),
baseUrl: server.URL,
}
_, err := a.downloadZipFromAzureDevOps(context.Background(), tt.args.options)
assert.Error(t, err)
assert.Equal(t, tt.want, zipRequestAuth)
})
}
}

View File

@@ -1,21 +1,72 @@
package git
import (
"context"
"crypto/tls"
"net/http"
"net/url"
"strings"
"os"
"path/filepath"
"time"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/transport/client"
githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
"github.com/pkg/errors"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/client"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
)
type cloneOptions struct {
repositoryUrl string
username string
password string
referenceName string
depth int
}
type downloader interface {
download(ctx context.Context, dst string, opt cloneOptions) error
}
type gitClient struct {
preserveGitDirectory bool
}
func (c gitClient) download(ctx context.Context, dst string, opt cloneOptions) error {
gitOptions := git.CloneOptions{
URL: opt.repositoryUrl,
Depth: opt.depth,
}
if opt.password != "" || opt.username != "" {
gitOptions.Auth = &githttp.BasicAuth{
Username: opt.username,
Password: opt.password,
}
}
if opt.referenceName != "" {
gitOptions.ReferenceName = plumbing.ReferenceName(opt.referenceName)
}
_, err := git.PlainCloneContext(ctx, dst, false, &gitOptions)
if err != nil {
return errors.Wrap(err, "failed to clone git repository")
}
if !c.preserveGitDirectory {
os.RemoveAll(filepath.Join(dst, ".git"))
}
return nil
}
// Service represents a service for managing Git.
type Service struct {
httpsCli *http.Client
azure downloader
git downloader
}
// NewService initializes a new service.
@@ -31,32 +82,29 @@ func NewService() *Service {
return &Service{
httpsCli: httpsCli,
azure: NewAzureDownloader(httpsCli),
git: gitClient{},
}
}
// ClonePublicRepository clones a public git repository using the specified URL in the specified
// CloneRepository clones a git repository using the specified URL in the specified
// destination folder.
func (service *Service) ClonePublicRepository(repositoryURL, referenceName string, destination string) error {
return cloneRepository(repositoryURL, referenceName, destination)
}
// ClonePrivateRepositoryWithBasicAuth clones a private git repository using the specified URL in the specified
// destination folder. It will use the specified username and password for basic HTTP authentication.
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
credentials := username + ":" + url.PathEscape(password)
repositoryURL = strings.Replace(repositoryURL, "://", "://"+credentials+"@", 1)
return cloneRepository(repositoryURL, referenceName, destination)
}
func cloneRepository(repositoryURL, referenceName, destination string) error {
options := &git.CloneOptions{
URL: repositoryURL,
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
options := cloneOptions{
repositoryUrl: repositoryURL,
username: username,
password: password,
referenceName: referenceName,
depth: 1,
}
if referenceName != "" {
options.ReferenceName = plumbing.ReferenceName(referenceName)
return service.cloneRepository(destination, options)
}
func (service *Service) cloneRepository(destination string, options cloneOptions) error {
if isAzureUrl(options.repositoryUrl) {
return service.azure.download(context.TODO(), destination, options)
}
_, err := git.PlainClone(destination, false, options)
return err
return service.git.download(context.TODO(), destination, options)
}

View File

@@ -0,0 +1,27 @@
package git
import (
"os"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService()
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := "https://github.com/portainer/private-test-repository.git"
err = service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, pat)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}

176
api/git/git_test.go Normal file
View File

@@ -0,0 +1,176 @@
package git
import (
"context"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/stretchr/testify/assert"
)
var bareRepoDir string
func TestMain(m *testing.M) {
if err := testMain(m); err != nil {
log.Fatal(err)
}
}
// testMain does extra setup/teardown before/after testing.
// The function is separated from TestMain due to necessity to call os.Exit/log.Fatal in the latter.
func testMain(m *testing.M) error {
dir, err := ioutil.TempDir("", "git-repo-")
if err != nil {
return errors.Wrap(err, "failed to create a temp dir")
}
defer os.RemoveAll(dir)
bareRepoDir = filepath.Join(dir, "test-clone.git")
file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0755)
if err != nil {
return errors.Wrap(err, "failed to open an archive")
}
err = archive.ExtractTarGz(file, dir)
if err != nil {
return errors.Wrapf(err, "failed to extract file from the archive to a folder %s\n", dir)
}
m.Run()
return nil
}
func Test_ClonePublicRepository_Shallow(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.CloneRepository(dir, repositoryURL, referenceName, "", "")
assert.NoError(t, err)
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: false}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.CloneRepository(dir, repositoryURL, referenceName, "", "")
assert.NoError(t, err)
assert.NoDirExists(t, filepath.Join(dir, ".git"))
}
func Test_cloneRepository(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.cloneRepository(dir, cloneOptions{
repositoryUrl: repositoryURL,
referenceName: referenceName,
depth: 10,
})
assert.NoError(t, err)
assert.Equal(t, 3, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func getCommitHistoryLength(t *testing.T, err error, dir string) int {
repo, err := git.PlainOpen(dir)
if err != nil {
t.Fatalf("can't open a git repo at %s with error %v", dir, err)
}
iter, err := repo.Log(&git.LogOptions{All: true})
if err != nil {
t.Fatalf("can't get a commit history iterator with error %v", err)
}
count := 0
err = iter.ForEach(func(_ *object.Commit) error {
count++
return nil
})
if err != nil {
t.Fatalf("can't iterate over the commit history with error %v", err)
}
return count
}
type testDownloader struct {
called bool
}
func (t *testDownloader) download(_ context.Context, _ string, _ cloneOptions) error {
t.called = true
return nil
}
func Test_cloneRepository_azure(t *testing.T) {
tests := []struct {
name string
url string
called bool
}{
{
name: "Azure HTTP URL",
url: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
called: true,
},
{
name: "Azure SSH URL",
url: "git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
called: true,
},
{
name: "Something else",
url: "https://example.com",
called: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
azure := &testDownloader{}
git := &testDownloader{}
s := &Service{azure: azure, git: git}
s.cloneRepository("", cloneOptions{repositoryUrl: tt.url, depth: 1})
// if azure API is called, git isn't and vice versa
assert.Equal(t, tt.called, azure.called)
assert.Equal(t, tt.called, !git.called)
})
}
}

BIN
api/git/testdata/azure-repo.zip vendored Normal file

Binary file not shown.

Binary file not shown.

7
api/git/types/types.go Normal file
View File

@@ -0,0 +1,7 @@
package gittypes
type RepoConfig struct {
URL string
ReferenceName string
ConfigFilePath string
}

View File

@@ -3,7 +3,7 @@ module github.com/portainer/portainer/api
go 1.13
require (
github.com/Microsoft/go-winio v0.4.14
github.com/Microsoft/go-winio v0.4.16
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/boltdb/bolt v1.3.1
github.com/containerd/containerd v1.3.1 // indirect
@@ -13,12 +13,13 @@ require (
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
github.com/docker/docker v0.0.0-00010101000000-000000000000
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
github.com/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.1
github.com/imdario/mergo v0.3.8 // indirect
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.8
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
@@ -30,14 +31,14 @@ require (
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/src-d/go-git.v4 v4.13.1
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
)
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456

View File

@@ -11,10 +11,10 @@ github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI=
github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
@@ -47,7 +47,7 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVl
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -92,6 +92,15 @@ github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.1.0 h1:4pl5BV4o7ZG/lterP4S6WzJ6xr49Ba5ET9ygheTYahk=
github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc=
github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM=
github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
@@ -105,7 +114,6 @@ github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dp
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@@ -147,11 +155,13 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 h1:l5rH/CnVVu+HPxjtxjM90nHrm4nov3j3RF9/62UjgLs=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
@@ -168,8 +178,8 @@ github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46O
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
@@ -177,13 +187,14 @@ github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@@ -205,6 +216,7 @@ github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -218,10 +230,8 @@ github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c h1:iOMba/KmaXg
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -248,9 +258,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@@ -258,15 +267,10 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
@@ -274,8 +278,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -288,10 +292,9 @@ golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -308,12 +311,10 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -324,27 +325,16 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -356,13 +346,11 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE=
@@ -373,25 +361,22 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=

View File

@@ -236,16 +236,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
customTemplate.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
err = handler.cloneGitRepository(gitCloneParams)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return nil, err
}

View File

@@ -1,17 +0,0 @@
package customtemplates
type cloneRepositoryParameters struct {
url string
referenceName string
path string
authentication bool
username string
password string
}
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
}
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
}

View File

@@ -212,16 +212,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
projectPath := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
err = handler.cloneGitRepository(gitCloneParams)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return nil, err
}

View File

@@ -1,17 +0,0 @@
package edgestacks
type cloneRepositoryParameters struct {
url string
referenceName string
path string
authentication bool
username string
password string
}
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
}
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
}

View File

@@ -77,5 +77,5 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
}
func isKubernetesRequest(requestURL string) bool {
return strings.HasPrefix(requestURL, "/api")
return strings.HasPrefix(requestURL, "/api") || strings.HasPrefix(requestURL, "/healthz")
}

View File

@@ -471,6 +471,7 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,

View File

@@ -169,19 +169,10 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
err = handler.cloneGitRepository(gitCloneParams)
err = handler.cloneAndSaveConfig(stack, projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.ComposeFilePathInRepository, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
}

View File

@@ -173,21 +173,12 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
err = handler.cloneGitRepository(gitCloneParams)
err = handler.cloneAndSaveConfig(stack, projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.ComposeFilePathInRepository, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
}
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)

View File

@@ -1,17 +0,0 @@
package stacks
type cloneRepositoryParameters struct {
url string
referenceName string
path string
authentication bool
username string
password string
}
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
}
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
}

View File

@@ -54,6 +54,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
h.Handle("/stacks/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
h.Handle("/stacks/{id}/git",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdateGit))).Methods(http.MethodPut)
h.Handle("/stacks/{id}/file",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
h.Handle("/stacks/{id}/migrate",

View File

@@ -2,6 +2,7 @@ package stacks
import (
"errors"
"fmt"
"log"
"net/http"
@@ -12,6 +13,7 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
@@ -226,3 +228,18 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
stack.ResourceControl = resourceControl
return response.JSON(w, stack)
}
func (handler *Handler) cloneAndSaveConfig(stack *portainer.Stack, projectPath, repositoryURL, refName, configFilePath string, auth bool, username, password string) error {
err := handler.GitService.CloneRepository(projectPath, repositoryURL, refName, username, password)
if err != nil {
return fmt.Errorf("unable to clone git repository: %w", err)
}
stack.GitConfig = &gittypes.RepoConfig{
URL: repositoryURL,
ReferenceName: refName,
ConfigFilePath: configFilePath,
}
return nil
}

View File

@@ -0,0 +1,29 @@
package stacks
import (
"testing"
portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_stackHandler_cloneAndSaveConfig_shouldCallGitCloneAndSaveConfigOnStack(t *testing.T) {
handler := NewHandler(&security.RequestBouncer{})
handler.GitService = testhelpers.NewGitService()
url := "url"
refName := "ref"
configPath := "path"
stack := &portainer.Stack{}
err := handler.cloneAndSaveConfig(stack, "", url, refName, configPath, false, "", "")
assert.NoError(t, err, "clone and save should not fail")
assert.Equal(t, gittypes.RepoConfig{
URL: url,
ReferenceName: refName,
ConfigFilePath: configPath,
}, *stack.GitConfig)
}

View File

@@ -191,6 +191,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
err = handler.deploySwarmStack(config)
if err != nil {

View File

@@ -0,0 +1,180 @@
package stacks
import (
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/filesystem"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type updateStackGitPayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
}
func (payload *updateStackGitPayload) Validate(r *http.Request) error {
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
// PUT request on /api/stacks/:id/git?endpointId=<endpointId>
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
if stack.GitConfig == nil {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is not created from git", err}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
// The EndpointID property is not available for these stacks, this API endpoint
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
var payload updateStackGitPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to move git repository directory", err}
}
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err = handler.GitService.CloneRepository(stack.ProjectPath, stack.GitConfig.URL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
restoreError := filesystem.MoveDirectory(backupProjectPath, stack.ProjectPath)
if restoreError != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: failed restoring backup folder]", restoreError)
}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
}
defer func() {
err = handler.FileService.RemoveDirectory(backupProjectPath)
if err != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: unable to remove git repository directory]", err)
}
}()
httpErr := handler.deployStack(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.Type == portainer.DockerSwarmStack {
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if httpErr != nil {
return httpErr
}
err := handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
return nil
}
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
err := handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
return nil
}

View File

@@ -1,17 +0,0 @@
package templates
type cloneRepositoryParameters struct {
url string
referenceName string
path string
authentication bool
username string
password string
}
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
}
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
}

View File

@@ -63,12 +63,7 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
defer handler.cleanUp(projectPath)
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
path: projectPath,
}
err = handler.cloneGitRepository(gitCloneParams)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
}

View File

@@ -23,6 +23,36 @@ func (transport *Transport) proxyContainerGroupRequest(request *http.Request) (*
}
func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request) (*http.Response, error) {
//add a lock before processing existense check
transport.mutex.Lock()
defer transport.mutex.Unlock()
//generate a temp http GET request based on the current PUT request
validationRequest := &http.Request{
Method: http.MethodGet,
URL: request.URL,
Header: http.Header{
"Authorization": []string{request.Header.Get("Authorization")},
},
}
//fire the request to Azure API to validate if there is an existing container instance with the same name
//positive - reject the request
//negative - continue the process
validationResponse, err := http.DefaultTransport.RoundTrip(validationRequest)
if err != nil {
return validationResponse, err
}
if validationResponse.StatusCode >= 200 && validationResponse.StatusCode < 300 {
resp := &http.Response{}
errObj := map[string]string{
"message": "A container instance with the same name already exists inside the selected resource group",
}
err = responseutils.RewriteResponse(resp, errObj, http.StatusConflict)
return resp, err
}
response, err := http.DefaultTransport.RoundTrip(request)
if err != nil {
return response, err

View File

@@ -0,0 +1,12 @@
package testhelpers
type gitService struct{}
// NewGitService creates new mock for portainer.GitService.
func NewGitService() *gitService {
return &gitService{}
}
func (service *gitService) CloneRepository(destination string, repositoryURL, referenceName string, username, password string) error {
return nil
}

View File

@@ -3,6 +3,8 @@ package portainer
import (
"io"
"time"
gittypes "github.com/portainer/portainer/api/git/types"
)
type (
@@ -336,7 +338,7 @@ type (
// Whether non-administrator should be able to use container capabilities
AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"`
// Whether non-administrator should be able to use sysctl settings
AllowSysctlSettingForRegularUsers bool `json:"AllowSysctlSettingForRegularUsers" example:"true"`
AllowSysctlSettingForRegularUsers bool `json:"allowSysctlSettingForRegularUsers" example:"true"`
// Whether host management features are enabled
EnableHostManagementFeatures bool `json:"enableHostManagementFeatures" example:"true"`
}
@@ -697,6 +699,8 @@ type (
UpdateDate int64 `example:"1587399600"`
// The username which last updated this stack
UpdatedBy string `example:"bob"`
// The git config of this stack
GitConfig *gittypes.RepoConfig
}
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
@@ -1138,8 +1142,7 @@ type (
// GitService represents a service for managing Git
GitService interface {
ClonePublicRepository(repositoryURL, referenceName string, destination string) error
ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error
CloneRepository(destination string, repositoryURL, referenceName, username, password string) error
}
// JWTService represents a service for managing JWT tokens
@@ -1327,7 +1330,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.4.0"
APIVersion = "2.5.1"
// DBVersion is the version number of the Portainer database
DBVersion = 27
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -1023,3 +1023,8 @@ json-tree .branch-preview {
overflow-y: auto;
}
/* !uib-typeahead override */
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}

View File

@@ -23,6 +23,7 @@ export default class porImageRegistryContainerController {
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed loading DockerHub pull rate limits', e);
this.setValidity(true);
}
} else {
this.setValidity(true);

View File

@@ -1,6 +1,6 @@
<!-- use registry -->
<div ng-if="$ctrl.model.UseRegistry">
<div class="form-group">
<div>
<div class="form-group" ng-if="$ctrl.model.UseRegistry">
<label for="image_registry" class="control-label text-left" ng-class="$ctrl.labelClass">
Registry
</label>

View File

@@ -972,9 +972,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
async function shouldShowSysctls() {
const { allowSysctlSettingForRegularUsers } = $scope.applicationState.application;
return allowSysctlSettingForRegularUsers || Authentication.isAdmin();
return endpoint.SecuritySettings.allowSysctlSettingForRegularUsers || Authentication.isAdmin();
}
async function checkIfContainerCapabilitiesEnabled() {

View File

@@ -79,6 +79,7 @@ angular.module('portainer.docker').controller('BuildImageController', [
Notifications.error('An error occured during build', { msg: 'Please check build logs output' });
} else {
Notifications.success('Image successfully built');
$scope.state.isEditorDirty = false;
}
})
.catch(function error(err) {

View File

@@ -499,7 +499,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
const resourceControl = data.Portainer.ResourceControl;
const userId = Authentication.getUserDetails().ID;
const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
const webhookPromise = $q.when(endpoint.Type !== 4 && $scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, endpoint.ID));
const webhookPromise = $q.when(endpoint.Type !== 4 && $scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, endpoint.Id));
return $q.all([rcPromise, webhookPromise]);
})
.then(function success() {

View File

@@ -5,7 +5,9 @@ export class EditEdgeStackFormController {
}
editorUpdate(cm) {
this.model.StackFileContent = cm.getValue();
this.isEditorDirty = true;
if (this.model.StackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== cm.getValue().replace(/(\r\n|\n|\r)/gm, '')) {
this.model.StackFileContent = cm.getValue();
this.isEditorDirty = true;
}
}
}

View File

@@ -136,78 +136,7 @@
</div>
<!-- !upload -->
<!-- repository -->
<div ng-show="$ctrl.state.Method === 'repository'">
<div class="col-sm-12 form-section-title">
Git repository
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can use the URL of a git repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-sm-2 control-label text-left">Repository URL</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
ng-model="$ctrl.formValues.RepositoryURL"
id="stack_repository_url"
placeholder="https://github.com/portainer/portainer-compose"
/>
</div>
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Specify a reference of the repository using the following syntax: branches with
<code>refs/heads/branch_name</code> or tags with <code>refs/tags/tag_name</code>. If not specified, will use the default <code>HEAD</code> reference normally the
<code>master</code> branch.
</span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-sm-2 control-label text-left">Repository reference</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.formValues.RepositoryReferenceName" id="stack_repository_reference_name" placeholder="refs/heads/master" />
</div>
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Indicate the path to the Compose file from the root of your repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Compose path</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.formValues.ComposeFilePathInRepository" id="stack_repository_path" placeholder="docker-compose.yml" />
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Authentication
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.formValues.RepositoryAuthentication" /><i></i> </label>
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.RepositoryAuthentication">
<span class="col-sm-12 text-muted small">
If your git account has 2FA enabled, you may receive an
<code>authentication required</code> error when deploying your stack. In this case, you will need to provide a personal-access token instead of your password.
</span>
</div>
<div class="form-group" ng-if="$ctrl.formValues.RepositoryAuthentication">
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
<div class="col-sm-11 col-md-5">
<input type="text" class="form-control" ng-model="$ctrl.formValues.RepositoryUsername" name="repository_username" placeholder="myGitUser" />
</div>
<label for="repository_password" class="col-sm-1 control-label text-left">
Password
</label>
<div class="col-sm-11 col-md-5">
<input type="password" class="form-control" ng-model="$ctrl.formValues.RepositoryPassword" name="repository_password" placeholder="myPassword" />
</div>
</div>
</div>
<git-form ng-show="$ctrl.state.Method === 'repository'" model="$ctrl.formValues" on-change="($ctrl.onChangeFormValues)"></git-form>
<!-- !repository -->
<!-- template -->
<div ng-show="$ctrl.state.Method === 'template'">

View File

@@ -40,6 +40,7 @@ export class CreateEdgeStackViewController {
this.onChangeTemplate = this.onChangeTemplate.bind(this);
this.onChangeTemplateAsync = this.onChangeTemplateAsync.bind(this);
this.onChangeMethod = this.onChangeMethod.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
}
async uiCanExit() {
@@ -161,6 +162,10 @@ export class CreateEdgeStackViewController {
return this.EdgeStackService.createStackFromGitRepository(name, repositoryOptions, this.formValues.Groups);
}
onChangeFormValues(values) {
this.formValues = values;
}
editorUpdate(cm) {
this.formValues.StackFileContent = cm.getValue();
this.state.isEditorDirty = true;

View File

@@ -116,7 +116,7 @@
>
<td ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
<td>{{ item.Name }}</td>
<td>{{ item.Image }}</td>
<td title="{{ item.Image }}">{{ item.Image | truncate: 64 }}</td>
<td>{{ item.ImagePullPolicy }}</td>
<td
><span class="label label-{{ item.Status | kubernetesPodStatusColor }}">{{ item.Status }}</span></td

View File

@@ -94,7 +94,7 @@
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool')">
Resource pool
Namespace
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && $ctrl.state.reverseOrder"></i>
</a>
@@ -147,8 +147,8 @@
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
</td>
<td
>{{ item.Image }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
<td title="{{ item.Image }}"
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
>
<td>{{ item.ApplicationType | kubernetesApplicationTypeText }}</td>
<td ng-if="item.ApplicationType !== $ctrl.KubernetesApplicationTypes.POD">

View File

@@ -89,7 +89,7 @@
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool')">
Resource pool
Namespace
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && $ctrl.state.reverseOrder"></i>
</a>

View File

@@ -87,7 +87,7 @@
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Namespace')">
Resource Pool
Namespace
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && $ctrl.state.reverseOrder"></i>
</a>

View File

@@ -92,7 +92,7 @@
><a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a></td
>
<td>{{ item.StackName }}</td>
<td>{{ item.Image }}</td>
<td title="{{ item.Image }}">{{ item.Image | truncate: 64 }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>

View File

@@ -77,7 +77,7 @@
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool')">
Resource pool
Namespace
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && $ctrl.state.reverseOrder"></i>
</a>
@@ -118,8 +118,8 @@
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
</td>
<td
>{{ item.Image }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
<td title="{{ item.Image }}"
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
>
<td>{{ item.CPU | kubernetesApplicationCPUValue }}</td>
<td>{{ item.Memory | humansize }}</td>

View File

@@ -107,8 +107,8 @@
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="$ctrl.isExternalApplication(item)">external</span>
</td>
<td>{{ item.StackName }}</td>
<td
>{{ item.Image }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
<td title="{{ item.Image }}"
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
>
<td>{{ item.CPU | kubernetesApplicationCPUValue }}</td>
<td>{{ item.Memory | humansize }}</td>

View File

@@ -55,7 +55,7 @@
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add resource pool
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace
</button>
</div>
<div class="searchBar">
@@ -130,7 +130,7 @@
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="4" class="text-center text-muted">No resource pool available.</td>
<td colspan="4" class="text-center text-muted">No namespace available.</td>
</tr>
</tbody>
</table>

View File

@@ -84,7 +84,7 @@
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool.Namespace.Name')">
Resource pool
Namespace
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool.Namespace.Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool.Namespace.Name' && $ctrl.state.reverseOrder"></i>
</a>

View File

@@ -43,8 +43,10 @@ class KubernetesConfigurationDataController {
}
async editorUpdateAsync(cm) {
this.formValues.DataYaml = cm.getValue();
this.isEditorDirty = true;
if (this.formValues.DataYaml !== cm.getValue()) {
this.formValues.DataYaml = cm.getValue();
this.isEditorDirty = true;
}
}
editorUpdate(cm) {

View File

@@ -2,7 +2,7 @@
<a ui-sref="kubernetes.dashboard({endpointId: $ctrl.endpointId})" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.resourcePools({endpointId: $ctrl.endpointId})" ui-sref-active="active">Resource pools <span class="menu-icon fa fa-layer-group fa-fw"></span></a>
<a ui-sref="kubernetes.resourcePools({endpointId: $ctrl.endpointId})" ui-sref-active="active">Namespaces <span class="menu-icon fa fa-layer-group fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.applications({endpointId: $ctrl.endpointId})" ui-sref-active="active">Applications <span class="menu-icon fa fa-laptop-code fa-fw"></span></a>

View File

@@ -77,12 +77,11 @@ export function KubernetesIngressService($async, KubernetesIngresses) {
});
}
function _delete(ingress) {
function _delete(namespace, ingressClassName) {
return $async(async () => {
try {
const params = new KubernetesCommonParams();
params.id = ingress.Name;
const namespace = ingress.Namespace;
params.id = ingressClassName;
await KubernetesIngresses(namespace).delete(params).$promise;
} catch (err) {
throw new PortainerError('Unable to delete ingress', err);

View File

@@ -18,12 +18,12 @@ const _KubernetesApplicationFormValues = Object.freeze({
AutoScaler: {},
Containers: [],
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.ISOLATED,
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
Configurations: [], // KubernetesApplicationConfigurationFormValue list
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
PlacementType: KubernetesApplicationPlacementTypes.PREFERRED,
PlacementType: KubernetesApplicationPlacementTypes.MANDATORY,
Placements: [], // KubernetesApplicationPlacementFormValue list
OriginalIngresses: undefined,
});

View File

@@ -93,7 +93,7 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
const patch = _.without(newIngresses, ...create);
const createPromises = _.map(create, (i) => KubernetesIngressService.create(i));
const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i));
const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i.Namespace, i.Name));
const patchPromises = _.map(patch, (ing) => {
const old = _.find(oldIngresses, { Name: ing.Name });
ing.Paths = angular.copy(old.Paths);

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Application console" state="kubernetes.applications.application.console" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> &gt;
<a ui-sref="kubernetes.applications">Applications</a> &gt;
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> &gt; Pods &gt;

View File

@@ -2,7 +2,7 @@
<a ui-sref="kubernetes.applications">Applications</a> &gt; Create an application
</kubernetes-view-header>
<kubernetes-view-header ng-if="ctrl.state.isEdit" title="Edit application" state="kubernetes.applications.application.edit" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> &gt;
<a ui-sref="kubernetes.applications">Applications</a> &gt;
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> &gt; Edit
@@ -44,7 +44,7 @@
>
</div>
<p ng-if="ctrl.state.alreadyExists"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> An application with the same name already exists inside the selected resource pool.</p
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> An application with the same name already exists inside the selected namespace.</p
>
</div>
</div>
@@ -84,11 +84,11 @@
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Resource pool
Namespace
</div>
<!-- #region RESOURCE POOL -->
<!-- #region NAMESPACE -->
<div class="form-group">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
<div class="col-sm-11">
<select
class="form-control"
@@ -103,8 +103,8 @@
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-danger">
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
resource pool.
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<!-- #endregion -->
@@ -554,39 +554,6 @@
<!-- access policy options -->
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED)">
<input
type="radio"
id="data_access_shared"
ng-value="ctrl.ApplicationDataAccessPolicies.SHARED"
ng-model="ctrl.formValues.DataAccessPolicy"
ng-change="ctrl.resetDeploymentType()"
/>
<label for="data_access_shared">
<div class="boxselector_header">
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
Shared
</div>
<p>All the instances of this application will use the same data</p>
</label>
</div>
<div style="color: #767676;" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
<input type="radio" id="data_access_shared" disabled />
<label
for="data_access_shared"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676;"
>
<div class="boxselector_header">
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
Shared
</div>
<p>All the instances of this application will use the same data</p>
</label>
</div>
<div
ng-if="
(!ctrl.state.isEdit && !ctrl.state.persistedFoldersUseExistingVolumes) ||
@@ -605,7 +572,7 @@
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
Isolated
</div>
<p>Every instance of this application will use their own data</p>
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
</label>
</div>
<div
@@ -625,7 +592,40 @@
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
Isolated
</div>
<p>Every instance of this application will use their own data</p>
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
</label>
</div>
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED)">
<input
type="radio"
id="data_access_shared"
ng-value="ctrl.ApplicationDataAccessPolicies.SHARED"
ng-model="ctrl.formValues.DataAccessPolicy"
ng-change="ctrl.resetDeploymentType()"
/>
<label for="data_access_shared">
<div class="boxselector_header">
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
Shared
</div>
<p>Application will be deployed as a Deployment with a shared storage access</p>
</label>
</div>
<div style="color: #767676;" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
<input type="radio" id="data_access_shared" disabled />
<label
for="data_access_shared"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676;"
>
<div class="boxselector_header">
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
Shared
</div>
<p>Application will be deployed as a Deployment with a shared storage access</p>
</label>
</div>
</div>
@@ -648,16 +648,16 @@
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
A resource quota is set on this resource pool, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums
are inherited from the resource pool quota.
A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums are
inherited from the namespace quota.
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-danger">
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
resource pool.
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
@@ -768,7 +768,7 @@
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
Global
</div>
<p>Deploy an instance of this container on each node of the cluster</p>
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
</label>
</div>
<div ng-if="ctrl.supportGlobalDeployment()">
@@ -784,7 +784,7 @@
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
Global
</div>
<p>Deploy an instance of this container on each node of the cluster</p>
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
</label>
</div>
</div>
@@ -1037,16 +1037,6 @@
<!-- placement policy options -->
<div class="form-group" style="margin-bottom: 0;" ng-if="ctrl.formValues.Placements.length">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="placement_soft" ng-value="ctrl.ApplicationPlacementTypes.PREFERRED" ng-model="ctrl.formValues.PlacementType" />
<label for="placement_soft">
<div class="boxselector_header">
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
Preferred
</div>
<p>Schedule this application on nodes that match the rules if possible</p>
</label>
</div>
<div>
<input type="radio" id="placement_hard" ng-value="ctrl.ApplicationPlacementTypes.MANDATORY" ng-model="ctrl.formValues.PlacementType" />
<label for="placement_hard">
@@ -1057,6 +1047,16 @@
<p>Schedule this application <b>ONLY</b> on nodes that match <b>ALL</b> Rules</p>
</label>
</div>
<div>
<input type="radio" id="placement_soft" ng-value="ctrl.ApplicationPlacementTypes.PREFERRED" ng-model="ctrl.formValues.PlacementType" />
<label for="placement_soft">
<div class="boxselector_header">
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
Preferred
</div>
<p>Schedule this application on nodes that match the rules if possible</p>
</label>
</div>
</div>
</div>
<!-- !placement policy options -->

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Application details" state="kubernetes.applications.application" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> &gt;
<a ui-sref="kubernetes.applications">Applications</a> &gt; {{ ctrl.application.Name }}
</kubernetes-view-header>
@@ -29,7 +29,7 @@
<td>{{ ctrl.application.StackName }}</td>
</tr>
<tr>
<td>Resource pool</td>
<td>Namespace</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Application logs" state="kubernetes.applications.application.logs" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> &gt;
<a ui-sref="kubernetes.applications">Applications</a> &gt;
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> &gt; Pods &gt;

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Application stats" state="kubernetes.applications.application.stats" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.state.transition.namespace })">{{ ctrl.state.transition.namespace }}</a> &gt;
<a ui-sref="kubernetes.applications">Applications</a> &gt;
<a ui-sref="kubernetes.applications.application({ name: ctrl.state.transition.applicationName, namespace: ctrl.state.transition.namespace })">{{

View File

@@ -37,19 +37,19 @@
>
</div>
<p ng-if="ctrl.state.alreadyExist"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A configuration with the same name already exists inside the selected resource pool.</p
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A configuration with the same name already exists inside the selected namespace.</p
>
</div>
</div>
<!-- !name -->
<div class="col-sm-12 form-section-title">
Resource pool
Namespace
</div>
<!-- resource-pool -->
<div class="form-group">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
<div class="col-sm-11">
<select
class="form-control"
@@ -62,8 +62,8 @@
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-warning">
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This resource pool has exhausted its resource capacity and you will not be able to deploy the configuration. Contact your administrator to expand the capacity of
the resource pool.
This namespace has exhausted its resource capacity and you will not be able to deploy the configuration. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<!-- !resource-pool -->

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Configuration details" state="kubernetes.configurations.configuration" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a> &gt;
<a ui-sref="kubernetes.configurations">Configurations</a> &gt; {{ ctrl.configuration.Name }}
</kubernetes-view-header>
@@ -24,7 +24,7 @@
</td>
</tr>
<tr>
<td>Resource Pool</td>
<td>Namespace</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>

View File

@@ -152,7 +152,7 @@ class KubernetesConfigureController {
ingressesToDel.forEach((ingress) => {
resourcePools.forEach((resourcePool) => {
promises.push(this.KubernetesIngressService.delete({ IngressClass: ingress, Namespace: resourcePool.Namespace.Name }));
promises.push(this.KubernetesIngressService.delete(resourcePool.Namespace.Name, ingress.Name));
});
});

View File

@@ -42,7 +42,7 @@
<i class="fa fa-layer-group"></i>
</div>
<div class="title">{{ ctrl.pools.length }}</div>
<div class="comment">{{ ctrl.pools.length === 1 ? 'Resource pool' : 'Resource pools' }}</div>
<div class="comment">{{ ctrl.pools.length === 1 ? 'Namespace' : 'Namespaces' }}</div>
</rd-widget-body>
</rd-widget>
</a>

View File

@@ -17,7 +17,7 @@
<form class="form-horizontal" style="margin-top: 20px;">
<div class="form-group">
<label for="target_node" class="col-lg-1 col-sm-2 control-label text-left">Resource pool</label>
<label for="target_node" class="col-lg-1 col-sm-2 control-label text-left">Namespace</label>
<div class="col-lg-11 col-sm-10">
<select class="form-control" ng-model="ctrl.formValues.Namespace" ng-options="namespace.Name as namespace.Name for namespace in ctrl.namespaces"></select>
</div>

View File

@@ -79,7 +79,7 @@ class KubernetesDeployController {
this.namespaces = namespaces;
this.formValues.Namespace = this.namespaces[0].Name;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load resource pools data');
this.Notifications.error('Failure', err, 'Unable to load namespaces data');
}
}

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Resource pool access management" state="kubernetes.resourcePools.resourcePool.access" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<kubernetes-view-header title="Namespace access management" state="kubernetes.resourcePools.resourcePool.access" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({id: ctrl.pool.Namespace.Name})">{{ ctrl.pool.Namespace.Name }}</a> &gt; Access management
</kubernetes-view-header>
@@ -9,7 +9,7 @@
<div class="row" ng-if="ctrl.pool">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title-text="Resource pool"></rd-widget-header>
<rd-widget-header icon="fa-plug" title-text="Namespace"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>

View File

@@ -78,7 +78,7 @@ class KubernetesResourcePoolAccessController {
}
this.availableUsersAndTeams = _.without(endpointAccesses.authorizedUsersAndTeams, ...this.authorizedUsersAndTeams);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool information');
this.Notifications.error('Failure', err, 'Unable to retrieve namespace information');
} finally {
this.state.viewReady = true;
}

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Create a resource pool" state="kubernetes.resourcePools.new" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt; Create a resource pool
<kubernetes-view-header title="Create a namespace" state="kubernetes.resourcePools.new" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt; Create a namespace
</kubernetes-view-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
@@ -36,7 +36,7 @@
with an alphanumeric character.</p
>
</div>
<p ng-if="ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A resource pool with the same name already exists.</p>
<p ng-if="ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A namespace with the same name already exists.</p>
</div>
</div>
<!-- #endregion -->
@@ -50,7 +50,7 @@
<div class="col-sm-12 small text-muted">
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
A resource pool segments the underyling physical Kubernetes cluster into smaller virtual clusters. You should assign a capped limit of resources to this pool or
A namespace segments the underlying physical Kubernetes cluster into smaller virtual clusters. You should assign a capped limit of resources to this namespace or
disable for the safe operation of your platform.
</p>
</div>
@@ -137,8 +137,8 @@
<div class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use of
load balancers in this resource pool.
You can set a quota on the amount of external load balancers that can be created inside this namespace. Set this quota to 0 to effectively disable the use of load
balancers in this namespace.
</span>
</div>
<div class="form-group">
@@ -164,7 +164,7 @@
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to effectively
prevent the usage of a specific storage option inside this resource pool.
prevent the usage of a specific storage option inside this namespace.
</span>
</div>
<div class="col-sm-12 form-section-title">
@@ -195,7 +195,7 @@
<div class="col-sm-12 small text-muted">
The ingress feature must be enabled in the
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})">endpoint configuration view</a> to be able to register ingresses inside this
resource pool.
namespace.
</div>
</div>
@@ -230,7 +230,7 @@
Hostnames
<portainer-tooltip
position="bottom"
message="Hostnames associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via one of these hostname."
message="Hostnames associated to the ingress inside this namespace. Users will be able to expose and access their applications over the ingress via one of these hostname."
>
</portainer-tooltip>
</label>
@@ -351,7 +351,7 @@
ng-click="ctrl.createResourcePool()"
button-spinner="ctrl.state.actionInProgress"
>
<span ng-hide="ctrl.state.actionInProgress">Create resource pool</span>
<span ng-hide="ctrl.state.actionInProgress">Create namespace</span>
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>

View File

@@ -115,7 +115,7 @@ class KubernetesCreateResourcePoolController {
}
}
/* #region CREATE RESOURCE POOL */
/* #region CREATE NAMESPACE */
async createResourcePoolAsync() {
this.state.actionInProgress = true;
try {
@@ -123,10 +123,10 @@ class KubernetesCreateResourcePoolController {
const owner = this.Authentication.getUserDetails().username;
this.formValues.Owner = owner;
await this.KubernetesResourcePoolService.create(this.formValues);
this.Notifications.success('Resource pool successfully created', this.formValues.Name);
this.Notifications.success('Namespace successfully created', this.formValues.Name);
this.$state.go('kubernetes.resourcePools');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create resource pool');
this.Notifications.error('Failure', err, 'Unable to create namespace');
} finally {
this.state.actionInProgress = false;
}
@@ -151,12 +151,12 @@ class KubernetesCreateResourcePoolController {
}
/* #endregion */
/* #region GET RESOURCE POOLS */
/* #region GET NAMESPACES */
async getResourcePoolsAsync() {
try {
this.resourcePools = await this.KubernetesResourcePoolService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve resource pools');
this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
}
}

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Resource pool details" state="kubernetes.resourcePools.resourcePool" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt; {{ ctrl.pool.Namespace.Name }}
<kubernetes-view-header title="Namespace details" state="kubernetes.resourcePools.resourcePool" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt; {{ ctrl.pool.Namespace.Name }}
</kubernetes-view-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
@@ -11,7 +11,7 @@
<rd-widget-body classes="no-padding">
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
<uib-tab-heading> <i class="fa fa-layer-group space-right" aria-hidden="true"></i> Resource pool </uib-tab-heading>
<uib-tab-heading> <i class="fa fa-layer-group space-right" aria-hidden="true"></i> Namespace </uib-tab-heading>
<form class="form-horizontal" autocomplete="off" name="resourcePoolEditForm" style="padding: 20px; margin-top: 10px;">
<!-- name-input -->
<div class="form-group">
@@ -39,7 +39,7 @@
<div ng-if="ctrl.formValues.HasQuota">
<kubernetes-resource-reservation
ng-if="ctrl.pool.Quota"
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this namespace."
cpu="ctrl.state.cpuUsed"
memory="ctrl.state.memoryUsed"
cpu-limit="ctrl.formValues.CpuLimit"
@@ -128,8 +128,8 @@
<div class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use
of load balancers in this resource pool.
You can set a quota on the amount of external load balancers that can be created inside this namespace. Set this quota to 0 to effectively disable the use of
load balancers in this namespace.
</span>
</div>
<div class="form-group">
@@ -154,7 +154,7 @@
<div class="col-sm-12 small text-muted">
The ingress feature must be enabled in the
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})">endpoint configuration view</a> to be able to register ingresses inside
this resource pool.
this namespace.
</div>
</div>
@@ -189,7 +189,7 @@
Hostnames
<portainer-tooltip
position="bottom"
message="Hostnames associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via one of these hostname."
message="Hostnames associated to the ingress inside this namespace. Users will be able to expose and access their applications over the ingress via one of these hostname."
>
</portainer-tooltip>
</label>
@@ -307,7 +307,7 @@
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to
effectively prevent the usage of a specific storage option inside this resource pool.
effectively prevent the usage of a specific storage option inside this namespace.
</span>
</div>
<div class="col-sm-12 form-section-title">
@@ -342,7 +342,7 @@
ng-click="ctrl.updateResourcePool()"
button-spinner="ctrl.state.actionInProgress"
>
<span ng-hide="ctrl.state.actionInProgress">Update resource pool</span>
<span ng-hide="ctrl.state.actionInProgress">Update namespace</span>
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
</button>
</div>
@@ -389,7 +389,7 @@
order-by="Name"
refresh-callback="ctrl.getApplications"
loading="ctrl.state.applicationsLoading"
title-text="Applications running in this resource pool"
title-text="Applications running in this namespace"
title-icon="fa-laptop-code"
>
</kubernetes-resource-pool-applications-datatable>

View File

@@ -179,16 +179,16 @@ class KubernetesResourcePoolController {
return false;
}
/* #region UPDATE RESOURCE POOL */
/* #region UPDATE NAMESPACE */
async updateResourcePoolAsync() {
this.state.actionInProgress = true;
try {
this.checkDefaults();
await this.KubernetesResourcePoolService.patch(this.savedFormValues, this.formValues);
this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name);
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create resource pool');
this.Notifications.error('Failure', err, 'Unable to create namespace');
} finally {
this.state.actionInProgress = false;
}
@@ -204,7 +204,7 @@ class KubernetesResourcePoolController {
if (warnings.quota || warnings.ingress) {
const messages = {
quota:
'Reducing the quota assigned to an "in-use" resource pool may have unintended consequences, including preventing running applications from functioning correctly and potentially even blocking them from running at all.',
'Reducing the quota assigned to an "in-use" namespace may have unintended consequences, including preventing running applications from functioning correctly and potentially even blocking them from running at all.',
ingress: 'Deactivating ingresses may cause applications to be unaccessible. All ingress configurations from affected applications will be removed.',
};
const displayedMessage = `${warnings.quota ? messages.quota : ''}${warnings.quota && warnings.ingress ? '<br/><br/>' : ''}
@@ -232,7 +232,7 @@ class KubernetesResourcePoolController {
this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name);
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events');
this.Notifications.error('Failure', err, 'Unable to retrieve namespace related events');
} finally {
this.state.eventsLoading = false;
}

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Resource pool list" state="kubernetes.resourcePools" view-ready="ctrl.state.viewReady">
Resource pools
<kubernetes-view-header title="Namespace list" state="kubernetes.resourcePools" view-ready="ctrl.state.viewReady">
Namespaces
</kubernetes-view-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>

View File

@@ -21,11 +21,11 @@ class KubernetesResourcePoolsController {
for (const pool of selectedItems) {
try {
await this.KubernetesResourcePoolService.delete(pool);
this.Notifications.success('Resource pool successfully removed', pool.Namespace.Name);
this.Notifications.success('Namespace successfully removed', pool.Namespace.Name);
const index = this.resourcePools.indexOf(pool);
this.resourcePools.splice(index, 1);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to remove resource pool');
this.Notifications.error('Failure', err, 'Unable to remove namespace');
} finally {
--actionCount;
if (actionCount === 0) {
@@ -37,7 +37,7 @@ class KubernetesResourcePoolsController {
removeAction(selectedItems) {
this.ModalService.confirmDeletion(
'Do you want to remove the selected resource pool(s)? All the resources associated to the selected resource pool(s) will be removed too.',
'Do you want to remove the selected namespace(s)? All the resources associated to the selected namespace(s) will be removed too.',
(confirmed) => {
if (confirmed) {
return this.$async(this.removeActionAsync, selectedItems);
@@ -50,7 +50,7 @@ class KubernetesResourcePoolsController {
try {
this.resourcePools = await this.KubernetesResourcePoolService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
this.Notifications.error('Failure', err, 'Unable to retreive namespaces');
}
}

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Stacks logs" state="kubernetes.stacks.stack.logs" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a>
<a ui-sref="kubernetes.resourcePools">Namespaces</a>
&gt; <a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.state.transition.namespace })">{{ ctrl.state.transition.namespace }}</a> &gt;
<a ui-sref="kubernetes.applications">Applications</a> &gt; Stacks &gt; {{ ctrl.state.transition.name }} &gt; Logs
</kubernetes-view-header>

View File

@@ -1,5 +1,5 @@
<kubernetes-view-header title="Volume details" state="kubernetes.volumes.volume" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Resource pools</a> &gt;
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt;
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })">{{ ctrl.volume.ResourcePool.Namespace.Name }}</a> &gt;
<a ui-sref="kubernetes.volumes">Volumes</a> &gt; {{ ctrl.volume.PersistentVolumeClaim.Name }}
</kubernetes-view-header>
@@ -27,7 +27,7 @@
</td>
</tr>
<tr>
<td>Resource pool</td>
<td>Namespace</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })">{{ ctrl.volume.ResourcePool.Namespace.Name }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>

View File

@@ -98,7 +98,7 @@ class KubernetesVolumesController {
});
this.storages = buildStorages(storages, volumes);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
this.Notifications.error('Failure', err, 'Unable to retreive namespaces');
}
}

View File

@@ -1,5 +1,7 @@
import _ from 'lodash-es';
import componentsModule from './components';
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
// The unauthenticated event is broadcasted by the jwtInterceptor when
@@ -15,7 +17,7 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat
return await Authentication.init();
}
angular.module('portainer.app', ['portainer.oauth']).config([
angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';

View File

@@ -0,0 +1,20 @@
class GitFormComposeAuthFieldsetController {
/* @ngInject */
constructor() {
this.onChangeField = this.onChangeField.bind(this);
this.onChangeAuth = this.onChangeField('RepositoryAuthentication');
this.onChangeUsername = this.onChangeField('RepositoryUsername');
this.onChangePassword = this.onChangeField('RepositoryPassword');
}
onChangeField(field) {
return (value) => {
this.onChange({
...this.model,
[field]: value,
});
};
}
}
export default GitFormComposeAuthFieldsetController;

View File

@@ -0,0 +1,39 @@
<div class="form-group">
<div class="col-sm-12">
<por-switch-field ng-model="$ctrl.model.RepositoryAuthentication" label="Authentication" on-change="($ctrl.onChangeAuth)"></por-switch-field>
</div>
</div>
<div ng-if="$ctrl.model.RepositoryAuthentication">
<div class="form-group">
<span class="col-sm-12 text-muted small">
If your git account has 2FA enabled, you may receive an <code>authentication required</code> error when deploying your stack. In this case, you will need to provide a
personal-access token instead of your password.
</span>
</div>
<div class="form-group">
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
<div class="col-sm-11 col-md-5">
<input
type="text"
class="form-control"
ng-model="$ctrl.model.RepositoryUsername"
name="repository_username"
placeholder="myGitUser"
ng-change="$ctrl.onChangeUsername($ctrl.model.RepositoryUsername)"
/>
</div>
<label for="repository_password" class="col-sm-1 control-label text-left">
Password
</label>
<div class="col-sm-11 col-md-5">
<input
type="password"
class="form-control"
ng-model="$ctrl.model.RepositoryPassword"
name="repository_password"
placeholder="myPassword"
ng-change="$ctrl.onChangePassword($ctrl.model.RepositoryPassword)"
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
import controller from './git-form-auth-fieldset.controller.js';
export const gitFormAuthFieldset = {
templateUrl: './git-form-auth-fieldset.html',
controller,
bindings: {
model: '<',
onChange: '<',
},
};

View File

@@ -0,0 +1,11 @@
<div class="form-group">
<span class="col-sm-12 text-muted small">
Indicate the path to the Compose file from the root of your repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Compose path</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.value" ng-change="$ctrl.onChange($ctrl.value)" id="stack_repository_path" placeholder="docker-compose.yml" />
</div>
</div>

View File

@@ -0,0 +1,7 @@
export const gitFormComposePathField = {
templateUrl: './git-form-compose-path-field.html',
bindings: {
value: '<',
onChange: '<',
},
};

View File

@@ -0,0 +1,12 @@
<div class="form-group">
<span class="col-sm-12 text-muted small">
Specify a reference of the repository using the following syntax: branches with <code>refs/heads/branch_name</code> or tags with <code>refs/tags/tag_name</code>. If not
specified, will use the default <code>HEAD</code> reference normally the <code>master</code> branch.
</span>
</div>
<div class="form-group">
<label for="stack_repository_reference_name" class="col-sm-2 control-label text-left">Repository reference</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.value" id="stack_repository_reference_name" placeholder="refs/heads/master" ng-change="$ctrl.onChange($ctrl.value)" />
</div>
</div>

View File

@@ -0,0 +1,7 @@
export const gitFormRefField = {
templateUrl: './git-form-ref-field.html',
bindings: {
value: '<',
onChange: '<',
},
};

View File

@@ -0,0 +1,18 @@
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can use the URL of a git repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-sm-2 control-label text-left">Repository URL</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
ng-model="$ctrl.value"
ng-change="$ctrl.onChange($ctrl.value)"
id="stack_repository_url"
placeholder="https://github.com/portainer/portainer-compose"
/>
</div>
</div>

View File

@@ -0,0 +1,7 @@
export const gitFormUrlField = {
templateUrl: './git-form-url-field.html',
bindings: {
value: '<',
onChange: '<',
},
};

View File

@@ -0,0 +1,18 @@
export default class GitFormController {
/* @ngInject */
constructor() {
this.onChangeField = this.onChangeField.bind(this);
this.onChangeURL = this.onChangeField('RepositoryURL');
this.onChangeRefName = this.onChangeField('RepositoryReferenceName');
this.onChangeComposePath = this.onChangeField('ComposeFilePathInRepository');
}
onChangeField(field) {
return (value) => {
this.onChange({
...this.model,
[field]: value,
});
};
}
}

View File

@@ -0,0 +1,9 @@
<div>
<div class="col-sm-12 form-section-title">
Git repository
</div>
<git-form-url-field value="$ctrl.model.RepositoryURL" on-change="($ctrl.onChangeURL)"></git-form-url-field>
<git-form-ref-field value="$ctrl.model.RepositoryReferenceName" on-change="($ctrl.onChangeRefName)"></git-form-ref-field>
<git-form-compose-path-field value="$ctrl.model.ComposeFilePathInRepository" on-change="($ctrl.onChangeComposePath)"></git-form-compose-path-field>
<git-form-auth-fieldset model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-auth-fieldset>
</div>

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