Compare commits
43 Commits
feat/EE-60
...
feat/EE-78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e56a6f51f | ||
|
|
ef150819ad | ||
|
|
48f5e3b909 | ||
|
|
70d11dc8ad | ||
|
|
26f4d37aff | ||
|
|
ea6df891c3 | ||
|
|
230f8fddc3 | ||
|
|
6734f0ab74 | ||
|
|
3e60167aeb | ||
|
|
8a4902f15a | ||
|
|
dde0467b89 | ||
|
|
a2a197b14b | ||
|
|
ee403ca32a | ||
|
|
d7fcfee2a2 | ||
|
|
3018801fc0 | ||
|
|
6bfbf58cdb | ||
|
|
3568fe9e52 | ||
|
|
2270de73ee | ||
|
|
819faa3948 | ||
|
|
ef8794c2b9 | ||
|
|
5618794927 | ||
|
|
47d462f085 | ||
|
|
0114766d50 | ||
|
|
2b94aa5aa6 | ||
|
|
746e738f1d | ||
|
|
29f5008c5f | ||
|
|
e54d99fd3d | ||
|
|
b3784792fe | ||
|
|
87e7d8ada8 | ||
|
|
af03d91e39 | ||
|
|
71635834c7 | ||
|
|
43702c2516 | ||
|
|
a21798f518 | ||
|
|
3641158daf | ||
|
|
0ac6274712 | ||
|
|
886d6764be | ||
|
|
39e24ec93f | ||
|
|
b7980f1b60 | ||
|
|
ce04944ce6 | ||
|
|
564bea7575 | ||
|
|
dcc77e50e5 | ||
|
|
317ebe2bfc | ||
|
|
7e2ce3ffc2 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ api/cmd/portainer/portainer*
|
||||
__debug_bin
|
||||
|
||||
api/docs
|
||||
.idea
|
||||
.env
|
||||
|
||||
BIN
api/archive/testdata/sample_archive.zip
vendored
Normal file
BIN
api/archive/testdata/sample_archive.zip
vendored
Normal file
Binary file not shown.
@@ -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
32
api/archive/zip_test.go
Normal 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"))
|
||||
|
||||
}
|
||||
@@ -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/")
|
||||
)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) updateSettingsToDB31() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacySettings.OAuthSettings.SSO = false
|
||||
legacySettings.OAuthSettings.LogoutURI = ""
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
)
|
||||
|
||||
var (
|
||||
testingDBStorePath string
|
||||
testingDBFileName string
|
||||
dummyLogoURL string
|
||||
dbConn *bolt.DB
|
||||
settingsService *settings.Service
|
||||
)
|
||||
|
||||
func setup() error {
|
||||
testingDBStorePath, _ = os.Getwd()
|
||||
testingDBFileName = "portainer-ee-mig-30.db"
|
||||
dummyLogoURL = "example.com"
|
||||
var err error
|
||||
dbConn, err = initTestingDBConn(testingDBStorePath, testingDBFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dummySettingsObj := map[string]interface{}{
|
||||
"LogoURL": dummyLogoURL,
|
||||
}
|
||||
settingsService, err = initTestingSettingsService(dbConn, dummySettingsObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUpdateSettingsToDB31(t *testing.T) {
|
||||
if err := setup(); err != nil {
|
||||
t.Errorf("failed to complete testing setups, err: %v", err)
|
||||
}
|
||||
defer dbConn.Close()
|
||||
defer os.Remove(testingDBFileName)
|
||||
m := &Migrator{
|
||||
db: dbConn,
|
||||
settingsService: settingsService,
|
||||
}
|
||||
if err := m.updateSettingsToDB31(); err != nil {
|
||||
t.Errorf("failed to update settings: %v", err)
|
||||
}
|
||||
updatedSettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
t.Errorf("failed to retrieve the updated settings: %v", err)
|
||||
}
|
||||
if updatedSettings.LogoURL != dummyLogoURL {
|
||||
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
|
||||
}
|
||||
if updatedSettings.OAuthSettings.SSO != false {
|
||||
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
|
||||
}
|
||||
if updatedSettings.OAuthSettings.LogoutURI != "" {
|
||||
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
)
|
||||
|
||||
// initTestingDBConn creates a raw bolt DB connection
|
||||
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
|
||||
func initTestingDBConn(storePath, fileName string) (*bolt.DB, error) {
|
||||
databasePath := path.Join(storePath, fileName)
|
||||
dbConn, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbConn, nil
|
||||
}
|
||||
|
||||
// initTestingDBConn creates a settings service with raw bolt DB connection
|
||||
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
|
||||
func initTestingSettingsService(dbConn *bolt.DB, preSetObj map[string]interface{}) (*settings.Service, error) {
|
||||
internalDBConn := &internal.DbConnection{
|
||||
DB: dbConn,
|
||||
}
|
||||
settingsService, err := settings.NewService(internalDBConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//insert a obj
|
||||
if err := internal.UpdateObject(internalDBConn, "settings", []byte("SETTINGS"), preSetObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return settingsService, nil
|
||||
}
|
||||
@@ -358,13 +358,5 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.5.0
|
||||
if m.currentDBVersion < 31 {
|
||||
err := m.updateSettingsToDB31()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
219
api/git/azure.go
Normal file
219
api/git/azure.go
Normal 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"
|
||||
}
|
||||
92
api/git/azure_integration_test.go
Normal file
92
api/git/azure_integration_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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.ClonePublicRepository(repositoryUrl, tt.args.referenceName, dst)
|
||||
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.ClonePrivateRepositoryWithBasicAuth(repositoryUrl, "refs/heads/main", dst, "", 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
250
api/git/azure_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,71 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"github.com/pkg/errors"
|
||||
"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/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 +81,37 @@ 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
|
||||
// destination folder.
|
||||
func (service *Service) ClonePublicRepository(repositoryURL, referenceName string, destination string) error {
|
||||
return cloneRepository(repositoryURL, referenceName, destination)
|
||||
func (service *Service) ClonePublicRepository(repositoryURL, referenceName, destination string) error {
|
||||
return service.cloneRepository(destination, cloneOptions{
|
||||
repositoryUrl: repositoryURL,
|
||||
referenceName: referenceName,
|
||||
depth: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
// destination folder. It will use the specified Username and Password for basic HTTP authentication.
|
||||
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName, destination, username, password string) error {
|
||||
return service.cloneRepository(destination, cloneOptions{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
referenceName: referenceName,
|
||||
depth: 1,
|
||||
})
|
||||
}
|
||||
|
||||
func cloneRepository(repositoryURL, referenceName, destination string) error {
|
||||
options := &git.CloneOptions{
|
||||
URL: repositoryURL,
|
||||
func (service *Service) cloneRepository(destination string, options cloneOptions) error {
|
||||
if isAzureUrl(options.repositoryUrl) {
|
||||
return service.azure.download(context.TODO(), destination, options)
|
||||
}
|
||||
|
||||
if referenceName != "" {
|
||||
options.ReferenceName = plumbing.ReferenceName(referenceName)
|
||||
}
|
||||
|
||||
_, err := git.PlainClone(destination, false, options)
|
||||
return err
|
||||
return service.git.download(context.TODO(), destination, options)
|
||||
}
|
||||
|
||||
26
api/git/git_integration_test.go
Normal file
26
api/git/git_integration_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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.ClonePrivateRepositoryWithBasicAuth(repositoryUrl, "refs/heads/main", dst, username, pat)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, filepath.Join(dst, "README.md"))
|
||||
}
|
||||
173
api/git/git_test.go
Normal file
173
api/git/git_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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.ClonePublicRepository(repositoryURL, referenceName, dir)
|
||||
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.ClonePublicRepository(repositoryURL, referenceName, dir)
|
||||
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
BIN
api/git/testdata/azure-repo.zip
vendored
Normal file
Binary file not shown.
BIN
api/git/testdata/test-clone-git-repo.tar.gz
vendored
Normal file
BIN
api/git/testdata/test-clone-git-repo.tar.gz
vendored
Normal file
Binary file not shown.
11
api/go.mod
11
api/go.mod
@@ -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
|
||||
|
||||
101
api/go.sum
101
api/go.sum
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ type publicSettingsResponse struct {
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
|
||||
// The URL used for oauth login
|
||||
OAuthLoginURI string `json:"OAuthLoginURI" example:"https://gitlab.com/oauth"`
|
||||
// The URL used for oauth logout
|
||||
OAuthLogoutURI string `json:"OAuthLogoutURI" example:"https://gitlab.com/oauth/logout"`
|
||||
// Whether telemetry is enabled
|
||||
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
|
||||
}
|
||||
@@ -36,32 +34,20 @@ type publicSettingsResponse struct {
|
||||
func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve the settings from the database", Err: err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: settings.EnableTelemetry,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
settings.OAuthSettings.AuthorizationURI,
|
||||
settings.OAuthSettings.ClientID,
|
||||
settings.OAuthSettings.RedirectURI,
|
||||
settings.OAuthSettings.Scopes),
|
||||
}
|
||||
|
||||
publicSettings := generatePublicSettings(settings)
|
||||
return response.JSON(w, publicSettings)
|
||||
}
|
||||
|
||||
func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResponse {
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: appSettings.LogoURL,
|
||||
AuthenticationMethod: appSettings.AuthenticationMethod,
|
||||
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: appSettings.EnableTelemetry,
|
||||
}
|
||||
//if OAuth authentication is on, compose the related fields from application settings
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
|
||||
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
|
||||
publicSettings.OAuthLoginURI = fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
|
||||
appSettings.OAuthSettings.AuthorizationURI,
|
||||
appSettings.OAuthSettings.ClientID,
|
||||
appSettings.OAuthSettings.RedirectURI,
|
||||
appSettings.OAuthSettings.Scopes)
|
||||
//control prompt=login param according to the SSO setting
|
||||
if !appSettings.OAuthSettings.SSO {
|
||||
publicSettings.OAuthLoginURI += "&prompt=login"
|
||||
}
|
||||
}
|
||||
return publicSettings
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
dummyOAuthClientID = "1a2b3c4d"
|
||||
dummyOAuthScopes = "scopes"
|
||||
dummyOAuthAuthenticationURI = "example.com/auth"
|
||||
dummyOAuthRedirectURI = "example.com/redirect"
|
||||
dummyOAuthLogoutURI = "example.com/logout"
|
||||
)
|
||||
|
||||
var (
|
||||
dummyOAuthLoginURI string
|
||||
mockAppSettings *portainer.Settings
|
||||
)
|
||||
|
||||
func setup() {
|
||||
dummyOAuthLoginURI = fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
|
||||
dummyOAuthAuthenticationURI,
|
||||
dummyOAuthClientID,
|
||||
dummyOAuthRedirectURI,
|
||||
dummyOAuthScopes)
|
||||
mockAppSettings = &portainer.Settings{
|
||||
AuthenticationMethod: portainer.AuthenticationOAuth,
|
||||
OAuthSettings: portainer.OAuthSettings{
|
||||
AuthorizationURI: dummyOAuthAuthenticationURI,
|
||||
ClientID: dummyOAuthClientID,
|
||||
Scopes: dummyOAuthScopes,
|
||||
RedirectURI: dummyOAuthRedirectURI,
|
||||
LogoutURI: dummyOAuthLogoutURI,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePublicSettingsWithSSO(t *testing.T) {
|
||||
setup()
|
||||
mockAppSettings.OAuthSettings.SSO = true
|
||||
publicSettings := generatePublicSettings(mockAppSettings)
|
||||
if publicSettings.AuthenticationMethod != portainer.AuthenticationOAuth {
|
||||
t.Errorf("wrong AuthenticationMethod, want: %d, got: %d", portainer.AuthenticationOAuth, publicSettings.AuthenticationMethod)
|
||||
}
|
||||
if publicSettings.OAuthLoginURI != dummyOAuthLoginURI {
|
||||
t.Errorf("wrong OAuthLoginURI when SSO is switched on, want: %s, got: %s", dummyOAuthLoginURI, publicSettings.OAuthLoginURI)
|
||||
}
|
||||
if publicSettings.OAuthLogoutURI != dummyOAuthLogoutURI {
|
||||
t.Errorf("wrong OAuthLogoutURI, want: %s, got: %s", dummyOAuthLogoutURI, publicSettings.OAuthLogoutURI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePublicSettingsWithoutSSO(t *testing.T) {
|
||||
setup()
|
||||
mockAppSettings.OAuthSettings.SSO = false
|
||||
publicSettings := generatePublicSettings(mockAppSettings)
|
||||
if publicSettings.AuthenticationMethod != portainer.AuthenticationOAuth {
|
||||
t.Errorf("wrong AuthenticationMethod, want: %d, got: %d", portainer.AuthenticationOAuth, publicSettings.AuthenticationMethod)
|
||||
}
|
||||
expectedOAuthLoginURI := dummyOAuthLoginURI + "&prompt=login"
|
||||
if publicSettings.OAuthLoginURI != expectedOAuthLoginURI {
|
||||
t.Errorf("wrong OAuthLoginURI when SSO is switched off, want: %s, got: %s", expectedOAuthLoginURI, publicSettings.OAuthLoginURI)
|
||||
}
|
||||
if publicSettings.OAuthLogoutURI != dummyOAuthLogoutURI {
|
||||
t.Errorf("wrong OAuthLogoutURI, want: %s, got: %s", dummyOAuthLogoutURI, publicSettings.OAuthLogoutURI)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -336,7 +336,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"`
|
||||
}
|
||||
@@ -489,8 +489,6 @@ type (
|
||||
Scopes string `json:"Scopes"`
|
||||
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
|
||||
DefaultTeamID TeamID `json:"DefaultTeamID"`
|
||||
SSO bool `json:"SSO"`
|
||||
LogoutURI string `json:"LogoutURI"`
|
||||
}
|
||||
|
||||
// Pair defines a key/value string pair
|
||||
@@ -1329,9 +1327,9 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.4.0"
|
||||
APIVersion = "2.5.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 31
|
||||
DBVersion = 27
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -18,7 +18,7 @@ const _KubernetesApplicationFormValues = Object.freeze({
|
||||
AutoScaler: {},
|
||||
Containers: [],
|
||||
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
|
||||
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
|
||||
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.ISOLATED,
|
||||
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
|
||||
Configurations: [], // KubernetesApplicationConfigurationFormValue list
|
||||
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> >
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> > Pods >
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a ui-sref="kubernetes.applications">Applications</a> > 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> >
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> > 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>
|
||||
|
||||
@@ -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> >
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> > {{ 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>
|
||||
|
||||
@@ -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> >
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> > Pods >
|
||||
|
||||
@@ -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> >
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.state.transition.namespace })">{{ ctrl.state.transition.namespace }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.state.transition.applicationName, namespace: ctrl.state.transition.namespace })">{{
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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> >
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a> >
|
||||
<a ui-sref="kubernetes.configurations">Configurations</a> > {{ 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>
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> >
|
||||
<kubernetes-view-header title="Namespace access management" state="kubernetes.resourcePools.resourcePool.access" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({id: ctrl.pool.Namespace.Name})">{{ ctrl.pool.Namespace.Name }}</a> > 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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> > 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> > 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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> > {{ 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> > {{ 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
> <a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.state.transition.namespace })">{{ ctrl.state.transition.namespace }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> > Stacks > {{ ctrl.state.transition.name }} > Logs
|
||||
</kubernetes-view-header>
|
||||
|
||||
@@ -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> >
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })">{{ ctrl.volume.ResourcePool.Namespace.Name }}</a> >
|
||||
<a ui-sref="kubernetes.volumes">Volumes</a> > {{ 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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.name || $ctrl.state.provider !== $ctrl.template.Type"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.name || !$ctrl.state.deployable"
|
||||
ng-click="$ctrl.createTemplate()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
@@ -64,7 +64,7 @@
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="$ctrl.unselectTemplate($ctrl.template)">Hide</button>
|
||||
<div class="cols-sm-12 small text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px; margin-top: 5px;">{{ $ctrl.state.formValidationError }}</div>
|
||||
<div class="cols-sm-12 small text-danger" ng-if="$ctrl.state.provider !== $ctrl.template.Type" style="margin-left: 5px; margin-top: 5px;"
|
||||
<div class="cols-sm-12 small text-danger" ng-if="!$ctrl.state.deployable" style="margin-left: 5px; margin-top: 5px;"
|
||||
>This template type cannot be deployed on this endpoint.</div
|
||||
>
|
||||
</div>
|
||||
|
||||
24
app/portainer/components/intervalFormat.js
Normal file
24
app/portainer/components/intervalFormat.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import parse from 'parse-duration';
|
||||
angular.module('portainer.app').directive('intervalFormat', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function ($scope, $element, $attrs, ngModel) {
|
||||
ngModel.$validators.invalidIntervalFormat = function (modelValue) {
|
||||
try {
|
||||
return modelValue && modelValue.toUpperCase().match(/^P?(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T?(?=\d+[HMS])(\d+H)?(\d+M)?(\d+S)?)?$/gm) !== null;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
ngModel.$validators.minimumInterval = function (modelValue) {
|
||||
try {
|
||||
return modelValue && parse(modelValue, 'minute') >= 1;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,16 +1,21 @@
|
||||
angular.module('portainer.app').factory('WebhookHelper', [
|
||||
'$location',
|
||||
'API_ENDPOINT_WEBHOOKS',
|
||||
function WebhookHelperFactory($location, API_ENDPOINT_WEBHOOKS) {
|
||||
'API_ENDPOINT_STACKS',
|
||||
function WebhookHelperFactory($location, API_ENDPOINT_WEBHOOKS, API_ENDPOINT_STACKS) {
|
||||
'use strict';
|
||||
|
||||
var helper = {};
|
||||
const protocol = $location.protocol().toLowerCase();
|
||||
const port = $location.port();
|
||||
const displayPort = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : ':' + port;
|
||||
|
||||
helper.returnWebhookUrl = function (token) {
|
||||
var displayPort =
|
||||
($location.protocol().toLowerCase() === 'http' && $location.port() === 80) || ($location.protocol().toLowerCase() === 'https' && $location.port() === 443)
|
||||
? ''
|
||||
: ':' + $location.port();
|
||||
return $location.protocol() + '://' + $location.host() + displayPort + '/' + API_ENDPOINT_WEBHOOKS + '/' + token;
|
||||
return `${protocol}://${$location.host()}${displayPort}/${API_ENDPOINT_WEBHOOKS}/${token}`;
|
||||
};
|
||||
|
||||
helper.returnStackWebhookUrl = function (token) {
|
||||
return `${protocol}://${$location.host()}${displayPort}/${API_ENDPOINT_STACKS}/webhooks/${token}`;
|
||||
};
|
||||
|
||||
return helper;
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
<div
|
||||
><div class="col-sm-12 form-section-title">
|
||||
Single Sign-On
|
||||
</div>
|
||||
<!-- SSO -->
|
||||
<div class="form-group">
|
||||
<label for="use-sso" class="control-label col-sm-2 text-left" style="padding-top: 0;">
|
||||
Use SSO
|
||||
<portainer-tooltip position="bottom" message="When using SSO the OAuth provider is not forced to prompt for credentials."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="switch"> <input id="use-sso" type="checkbox" ng-model="$ctrl.settings.SSO" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !SSO -->
|
||||
|
||||
<!-- HideInternalAuth -->
|
||||
<div class="form-group" ng-if="$ctrl.settings.SSO">
|
||||
<label for="hide-internal-auth" class="control-label col-sm-2 text-left" style="padding-top: 0;">
|
||||
Hide internal authentication prompt
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="switch"> <input id="hide-internal-auth" type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=hide-internal-auth" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !HideInternalAuth -->
|
||||
<div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Automatic user provisioning
|
||||
</div>
|
||||
@@ -133,18 +105,7 @@
|
||||
<input type="text" class="form-control" id="oauth_redirect_uri" ng-model="$ctrl.settings.RedirectURI" placeholder="http://yourportainer.com/" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oauth_logout_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Logout URL
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="URL used by Portainer to redirect the user to the OAuth provider in order to log the user out of the identity provider session."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="oauth_logout_url" ng-model="$ctrl.settings.LogoutURI" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
User identifier
|
||||
|
||||
@@ -279,12 +279,16 @@ angular.module('portainer.app').factory('StackService', [
|
||||
Name: name,
|
||||
RepositoryURL: repositoryOptions.RepositoryURL,
|
||||
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
|
||||
ComposeFile: repositoryOptions.ComposeFilePathInRepository,
|
||||
AdditionalFiles: repositoryOptions.AdditionalFiles,
|
||||
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
|
||||
RepositoryUsername: repositoryOptions.RepositoryUsername,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
Env: env,
|
||||
};
|
||||
if (repositoryOptions.AutoUpdate) {
|
||||
payload.AutoUpdate = repositoryOptions.AutoUpdate;
|
||||
}
|
||||
return Stack.create({ method: 'repository', type: 2, endpointId: endpointId }, payload).$promise;
|
||||
};
|
||||
|
||||
@@ -299,12 +303,18 @@ angular.module('portainer.app').factory('StackService', [
|
||||
SwarmID: swarm.Id,
|
||||
RepositoryURL: repositoryOptions.RepositoryURL,
|
||||
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
|
||||
ComposeFile: repositoryOptions.ComposeFilePathInRepository,
|
||||
AdditionalFiles: repositoryOptions.AdditionalFiles,
|
||||
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
|
||||
RepositoryUsername: repositoryOptions.RepositoryUsername,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
Env: env,
|
||||
};
|
||||
|
||||
if (repositoryOptions.AutoUpdate) {
|
||||
payload.AutoUpdate = repositoryOptions.AutoUpdate;
|
||||
}
|
||||
|
||||
return Stack.create({ method: 'repository', type: 1, endpointId: endpointId }, payload).$promise;
|
||||
})
|
||||
.then(function success(data) {
|
||||
|
||||
@@ -34,13 +34,16 @@ class CustomTemplatesViewController {
|
||||
this.StateManager = StateManager;
|
||||
this.StackService = StackService;
|
||||
|
||||
this.DOCKER_STANDALONE = 'DOCKER_STANDALONE';
|
||||
this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
|
||||
|
||||
this.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
isEditorVisible: false,
|
||||
provider: 0,
|
||||
deployable: false,
|
||||
};
|
||||
|
||||
this.currentUser = {
|
||||
@@ -182,7 +185,8 @@ class CustomTemplatesViewController {
|
||||
this.formValues.name = template.Title ? template.Title : '';
|
||||
this.state.selectedTemplate = template;
|
||||
this.$anchorScroll('view-top');
|
||||
|
||||
const applicationState = this.StateManager.getState();
|
||||
this.state.deployable = this.isDeployable(applicationState.endpoint, template.Type);
|
||||
const file = await this.CustomTemplateService.customTemplateFile(template.Id);
|
||||
this.formValues.fileContent = file;
|
||||
}
|
||||
@@ -224,6 +228,21 @@ class CustomTemplatesViewController {
|
||||
this.formValues.fileContent = cm.getValue();
|
||||
}
|
||||
|
||||
isDeployable(endpoint, templateType) {
|
||||
let deployable = false;
|
||||
switch (templateType) {
|
||||
case 1:
|
||||
deployable = endpoint.mode.provider === this.DOCKER_SWARM_MODE;
|
||||
break;
|
||||
case 2:
|
||||
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return deployable;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
const applicationState = this.StateManager.getState();
|
||||
|
||||
@@ -232,7 +251,6 @@ class CustomTemplatesViewController {
|
||||
apiVersion,
|
||||
} = applicationState;
|
||||
|
||||
this.state.provider = endpointMode.provider === 'DOCKER_STANDALONE' ? 2 : 1;
|
||||
this.getTemplates(endpointMode);
|
||||
this.getNetworks(endpointMode.provider, apiVersion);
|
||||
|
||||
|
||||
@@ -96,8 +96,10 @@ class EditCustomTemplateViewController {
|
||||
}
|
||||
|
||||
editorUpdate(cm) {
|
||||
this.formValues.FileContent = cm.getValue();
|
||||
this.state.isEditorDirty = true;
|
||||
if (this.formValues.FileContent.replace(/(\r\n|\n|\r)/gm, '') !== cm.getValue().replace(/(\r\n|\n|\r)/gm, '')) {
|
||||
this.formValues.FileContent = cm.getValue();
|
||||
this.state.isEditorDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
async uiCanExit() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
|
||||
|
||||
angular
|
||||
@@ -21,7 +21,10 @@ angular
|
||||
StackHelper,
|
||||
ContainerHelper,
|
||||
CustomTemplateService,
|
||||
ContainerService
|
||||
ContainerService,
|
||||
endpoint,
|
||||
WebhookHelper,
|
||||
clipboard
|
||||
) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
@@ -29,10 +32,14 @@ angular
|
||||
StackFile: null,
|
||||
RepositoryURL: '',
|
||||
RepositoryReferenceName: '',
|
||||
RepositoryAutomaticUpdates: true,
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
RepositoryMechanism: 'Interval',
|
||||
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
||||
Env: [],
|
||||
AdditionalFiles: [],
|
||||
ComposeFilePathInRepository: 'docker-compose.yml',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
};
|
||||
@@ -61,6 +68,14 @@ angular
|
||||
$scope.formValues.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addAdditionalFiles = function () {
|
||||
$scope.formValues.AdditionalFiles.push('');
|
||||
};
|
||||
|
||||
$scope.removeAdditionalFiles = function (index) {
|
||||
$scope.formValues.AdditionalFiles.splice(index, 1);
|
||||
};
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
@@ -89,17 +104,32 @@ angular
|
||||
|
||||
if (method === 'repository') {
|
||||
var repositoryOptions = {
|
||||
AdditionalFiles: $scope.formValues.AdditionalFiles,
|
||||
RepositoryURL: $scope.formValues.RepositoryURL,
|
||||
RepositoryReferenceName: $scope.formValues.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository,
|
||||
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPersonalAccessToken,
|
||||
};
|
||||
|
||||
getAutoUpdatesProperty(repositoryOptions);
|
||||
|
||||
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||
}
|
||||
}
|
||||
|
||||
function getAutoUpdatesProperty(repositoryOptions) {
|
||||
if ($scope.formValues.RepositoryAutomaticUpdates) {
|
||||
repositoryOptions.AutoUpdate = {};
|
||||
if ($scope.formValues.RepositoryMechanism === 'Interval') {
|
||||
repositoryOptions.AutoUpdate.Interval = $scope.formValues.RepositoryFetchInterval;
|
||||
} else if ($scope.formValues.RepositoryMechanism === 'Webhook') {
|
||||
repositoryOptions.AutoUpdate.Webhook = $scope.formValues.RepositoryWebhookURL.split('/').reverse()[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createComposeStack(name, method) {
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||
const endpointId = +$state.params.endpointId;
|
||||
@@ -112,17 +142,27 @@ angular
|
||||
return StackService.createComposeStackFromFileUpload(name, stackFile, env, endpointId);
|
||||
} else if (method === 'repository') {
|
||||
var repositoryOptions = {
|
||||
AdditionalFiles: $scope.formValues.AdditionalFiles,
|
||||
RepositoryURL: $scope.formValues.RepositoryURL,
|
||||
RepositoryReferenceName: $scope.formValues.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository,
|
||||
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPersonalAccessToken,
|
||||
};
|
||||
|
||||
getAutoUpdatesProperty(repositoryOptions);
|
||||
|
||||
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.copyWebhook = function () {
|
||||
clipboard.copyText($scope.formValues.RepositoryWebhookURL);
|
||||
$('#copyNotification').show();
|
||||
$('#copyNotification').fadeOut(2000);
|
||||
};
|
||||
|
||||
$scope.deployStack = function () {
|
||||
var name = $scope.formValues.Name;
|
||||
var method = $scope.state.Method;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<form class="form-horizontal" name="createStackForm">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
@@ -175,6 +175,32 @@
|
||||
<input type="text" class="form-control" ng-model="formValues.ComposeFilePathInRepository" id="stack_repository_path" placeholder="docker-compose.yml" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- additional paths-variables -->
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Additional paths</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addAdditionalFiles()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add file
|
||||
</span>
|
||||
</div>
|
||||
<!-- additional-paths-variable-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="variable in formValues.AdditionalFiles track by $index" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">path</span>
|
||||
<input id="ipv6_network_auxaddr_{{ $index }}" type="text" class="form-control" ng-model="formValues.AdditionalFiles[$index]" />
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeAdditionalFiles($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !additional-paths-variable-input-list -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !additional-variables -->
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
@@ -183,22 +209,90 @@
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.RepositoryAuthentication" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="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>
|
||||
<label for="repository_username" class="col-sm-2 control-label text-left">Username</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.RepositoryUsername" name="repository_username" placeholder="git username" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="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="formValues.RepositoryUsername" name="repository_username" placeholder="myGitUser" />
|
||||
</div>
|
||||
<label for="repository_password" class="col-sm-1 control-label text-left">
|
||||
Password
|
||||
<label for="repository_personal_access_token" class="col-sm-2 control-label text-left">
|
||||
Personal Access Token
|
||||
<portainer-tooltip position="bottom" message="Provide a personal access token or password"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-11 col-md-5">
|
||||
<input type="password" class="form-control" ng-model="formValues.RepositoryPassword" name="repository_password" placeholder="myPassword" />
|
||||
<div class="col-sm-3">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
ng-model="formValues.RepositoryPersonalAccessToken"
|
||||
name="repository_personal_access_token"
|
||||
placeholder="personal access token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Automatic updates
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.RepositoryAutomaticUpdates" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-if="formValues.RepositoryAutomaticUpdates">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="text-muted">Enabling automatic updates will store the credentials and it is advisable to use a git service account.</span>
|
||||
</div>
|
||||
<div class="small text-warning" style="margin: 5px 0 10px 0;" ng-if="formValues.RepositoryAutomaticUpdates">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="text-muted">Any changes to this stack made locally in Portainer will be overriden by the definition in git and may cause service interruption.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="formValues.RepositoryAutomaticUpdates">
|
||||
<label for="repository_mechanism" class="col-sm-2 control-label text-left">
|
||||
Mechanism
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<div class="input-group col-sm-10 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="formValues.RepositoryMechanism" uib-btn-radio="'Interval'">Polling</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.RepositoryMechanism" uib-btn-radio="'Webhook'">Webhook</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="formValues.RepositoryMechanism === 'Webhook'">
|
||||
<label for="repository_mechanism" class="col-sm-2 control-label text-left">
|
||||
Webhook
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<span class="text-muted"> {{ formValues.RepositoryWebhookURL | truncatelr }} </span>
|
||||
<button type="button" class="btn btn-sm btn-primary btn-sm space-left" ng-if="formValues.RepositoryWebhookURL" ng-click="copyWebhook()">
|
||||
<span><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy link</span>
|
||||
</button>
|
||||
<span>
|
||||
<i id="copyNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="formValues.RepositoryMechanism === 'Interval'">
|
||||
<label for="repository_fetch_interval" class="col-sm-2 control-label text-left">
|
||||
Fetch interval
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="formValues.RepositoryFetchInterval" name="repository_fetch_interval" placeholder="5m" required interval-format />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="createStackForm.repository_fetch_interval.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="createStackForm.repository_fetch_interval.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="invalidIntervalFormat"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Please enter a valid time interval.</p>
|
||||
<p ng-message="minimumInterval"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum interval is 1m</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -201,12 +201,11 @@ angular.module('portainer.app').controller('StackController', [
|
||||
};
|
||||
|
||||
$scope.editorUpdate = function (cm) {
|
||||
if ($scope.stackFileContent !== cm.getValue()) {
|
||||
if ($scope.stackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== cm.getValue().replace(/(\r\n|\n|\r)/gm, '')) {
|
||||
$scope.state.isEditorDirty = true;
|
||||
$scope.stackFileContent = cm.getValue();
|
||||
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
|
||||
}
|
||||
$scope.stackFileContent = cm.getValue();
|
||||
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
|
||||
$scope.state.isEditorDirty = true;
|
||||
};
|
||||
|
||||
$scope.stopStack = stopStack;
|
||||
@@ -269,8 +268,7 @@ angular.module('portainer.app').controller('StackController', [
|
||||
var stack = data.stack;
|
||||
$scope.groups = data.groups;
|
||||
$scope.stack = stack;
|
||||
$scope.containers = data.containers;
|
||||
$scope.containerNames = ContainerHelper.getContainerNames($scope.containers);
|
||||
$scope.containerNames = ContainerHelper.getContainerNames(data.containers);
|
||||
|
||||
let resourcesPromise = Promise.resolve({});
|
||||
if (stack.Status === 1) {
|
||||
|
||||
@@ -36,6 +36,9 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
StackService,
|
||||
endpoint
|
||||
) {
|
||||
const DOCKER_STANDALONE = 'DOCKER_STANDALONE';
|
||||
const DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
|
||||
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
@@ -240,9 +243,26 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
|
||||
$scope.formValues.name = template.Name ? template.Name : '';
|
||||
$scope.state.selectedTemplate = template;
|
||||
$scope.state.deployable = isDeployable($scope.applicationState.endpoint, template.Type);
|
||||
$anchorScroll('view-top');
|
||||
};
|
||||
|
||||
function isDeployable(endpoint, templateType) {
|
||||
let deployable = false;
|
||||
switch (templateType) {
|
||||
case 1:
|
||||
deployable = endpoint.mode.provider === DOCKER_SWARM_MODE || endpoint.mode.provider === DOCKER_STANDALONE;
|
||||
break;
|
||||
case 2:
|
||||
deployable = endpoint.mode.provider === DOCKER_SWARM_MODE;
|
||||
break;
|
||||
case 3:
|
||||
deployable = endpoint.mode.provider === DOCKER_STANDALONE;
|
||||
break;
|
||||
}
|
||||
return deployable;
|
||||
}
|
||||
|
||||
function createTemplateConfiguration(template) {
|
||||
var network = $scope.formValues.network;
|
||||
var name = $scope.formValues.name;
|
||||
@@ -254,7 +274,6 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
|
||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
$scope.state.provider = endpointMode.provider === 'DOCKER_STANDALONE' ? 2 : 1;
|
||||
|
||||
$q.all({
|
||||
templates: TemplateService.templates(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.4.0",
|
||||
"version": "2.5.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
@@ -91,6 +91,7 @@
|
||||
"lodash-es": "^4.17.15",
|
||||
"moment": "^2.21.0",
|
||||
"ng-file-upload": "~12.2.13",
|
||||
"parse-duration": "^1.0.0",
|
||||
"source-map-loader": "^1.1.2",
|
||||
"spinkit": "^2.0.1",
|
||||
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
||||
|
||||
@@ -7967,6 +7967,11 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5:
|
||||
pbkdf2 "^3.0.3"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
parse-duration@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-1.0.0.tgz#8605651745f61088f6fb14045c887526c291858c"
|
||||
integrity sha512-X4kUkCTHU1N/kEbwK9FpUJ0UZQa90VzeczfS704frR30gljxDG0pSziws06XlK+CGRSo/1wtG1mFIdBFQTMQNw==
|
||||
|
||||
parse-filepath@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
|
||||
|
||||
Reference in New Issue
Block a user