Compare commits
2 Commits
feat/EE-82
...
release/2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f2a90a722 | ||
|
|
1d46f2bb35 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,5 +11,3 @@ api/cmd/portainer/portainer*
|
||||
__debug_bin
|
||||
|
||||
api/docs
|
||||
.idea
|
||||
.env
|
||||
|
||||
BIN
api/archive/testdata/sample_archive.zip
vendored
BIN
api/archive/testdata/sample_archive.zip
vendored
Binary file not shown.
@@ -3,13 +3,10 @@ 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
|
||||
@@ -55,60 +52,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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 documentation 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 documention to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
||||
)
|
||||
|
||||
219
api/git/azure.go
219
api/git/azure.go
@@ -1,219 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
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,71 +1,21 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"github.com/pkg/errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
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.
|
||||
@@ -81,37 +31,32 @@ 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, destination string) error {
|
||||
return service.cloneRepository(destination, cloneOptions{
|
||||
repositoryUrl: repositoryURL,
|
||||
referenceName: referenceName,
|
||||
depth: 1,
|
||||
})
|
||||
func (service *Service) ClonePublicRepository(repositoryURL, referenceName string, destination string) error {
|
||||
return cloneRepository(repositoryURL, referenceName, destination)
|
||||
}
|
||||
|
||||
// ClonePrivateRepositoryWithBasicAuth clones a private git repository using the specified URL in the specified
|
||||
// destination folder. It will use the specified Username and Password for basic HTTP authentication.
|
||||
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName, destination, username, password string) error {
|
||||
return service.cloneRepository(destination, cloneOptions{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
referenceName: referenceName,
|
||||
depth: 1,
|
||||
})
|
||||
// destination folder. It will use the specified username and password for basic HTTP authentication.
|
||||
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
|
||||
credentials := username + ":" + url.PathEscape(password)
|
||||
repositoryURL = strings.Replace(repositoryURL, "://", "://"+credentials+"@", 1)
|
||||
return cloneRepository(repositoryURL, referenceName, destination)
|
||||
}
|
||||
|
||||
func (service *Service) cloneRepository(destination string, options cloneOptions) error {
|
||||
if isAzureUrl(options.repositoryUrl) {
|
||||
return service.azure.download(context.TODO(), destination, options)
|
||||
func cloneRepository(repositoryURL, referenceName, destination string) error {
|
||||
options := &git.CloneOptions{
|
||||
URL: repositoryURL,
|
||||
}
|
||||
|
||||
return service.git.download(context.TODO(), destination, options)
|
||||
if referenceName != "" {
|
||||
options.ReferenceName = plumbing.ReferenceName(referenceName)
|
||||
}
|
||||
|
||||
_, err := git.PlainClone(destination, false, options)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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"))
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var bareRepoDir string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := testMain(m); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// testMain does extra setup/teardown before/after testing.
|
||||
// The function is separated from TestMain due to necessity to call os.Exit/log.Fatal in the latter.
|
||||
func testMain(m *testing.M) error {
|
||||
dir, err := ioutil.TempDir("", "git-repo-")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create a temp dir")
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
bareRepoDir = filepath.Join(dir, "test-clone.git")
|
||||
|
||||
file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open an archive")
|
||||
}
|
||||
err = archive.ExtractTarGz(file, dir)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to extract file from the archive to a folder %s\n", dir)
|
||||
}
|
||||
|
||||
m.Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_ClonePublicRepository_Shallow(t *testing.T) {
|
||||
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
|
||||
repositoryURL := bareRepoDir
|
||||
referenceName := "refs/heads/main"
|
||||
destination := "shallow"
|
||||
|
||||
dir, err := ioutil.TempDir("", destination)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temp dir")
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err = service.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
BIN
api/git/testdata/azure-repo.zip
vendored
Binary file not shown.
BIN
api/git/testdata/test-clone-git-repo.tar.gz
vendored
BIN
api/git/testdata/test-clone-git-repo.tar.gz
vendored
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
package gittypes
|
||||
|
||||
// RepoConfig represents a configuration for a repo
|
||||
type RepoConfig struct {
|
||||
// The repo url
|
||||
URL string `example:"https://github.com/portainer/portainer-ee.git"`
|
||||
// The reference name
|
||||
ReferenceName string `example:"refs/heads/branch_name"`
|
||||
// Path to where the config file is in this url/refName
|
||||
ConfigFilePath string `example:"docker-compose.yml"`
|
||||
// Git credentials
|
||||
Authentication *GitAuthentication
|
||||
ConfigHash []byte
|
||||
}
|
||||
|
||||
type GitAuthentication struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
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.16
|
||||
github.com/Microsoft/go-winio v0.4.14
|
||||
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,13 +13,12 @@ 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/joho/godotenv v1.3.0
|
||||
github.com/imdario/mergo v0.3.8 // indirect
|
||||
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
|
||||
@@ -31,14 +30,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-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
|
||||
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.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
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,15 +92,6 @@ 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=
|
||||
@@ -114,6 +105,7 @@ 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=
|
||||
@@ -155,13 +147,11 @@ 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.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
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.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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
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=
|
||||
@@ -178,8 +168,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-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/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/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=
|
||||
@@ -187,14 +177,13 @@ 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=
|
||||
@@ -216,7 +205,6 @@ 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=
|
||||
@@ -230,8 +218,10 @@ 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=
|
||||
@@ -258,8 +248,9 @@ 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.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
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/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=
|
||||
@@ -267,10 +258,15 @@ 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=
|
||||
@@ -278,8 +274,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.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
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/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=
|
||||
@@ -292,9 +288,10 @@ 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-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
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/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=
|
||||
@@ -311,10 +308,12 @@ 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-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/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/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=
|
||||
@@ -325,16 +324,27 @@ 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=
|
||||
@@ -346,11 +356,13 @@ 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=
|
||||
@@ -361,22 +373,25 @@ 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=
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -113,9 +112,8 @@ type composeStackFromGitRepositoryPayload struct {
|
||||
// Password used in basic authentication. Required when RepositoryAuthentication is true.
|
||||
RepositoryPassword string `example:"myGitPassword"`
|
||||
// Path to the Stack file inside the Git repository
|
||||
ComposeFile string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
AdditionalFiles []string
|
||||
AutoUpdate *portainer.StackAutoUpdate
|
||||
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
|
||||
// A list of environment variables used during stack deployment
|
||||
Env []portainer.Pair
|
||||
}
|
||||
@@ -128,11 +126,8 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
return errors.New("Invalid RepositoryReferenceName")
|
||||
}
|
||||
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
||||
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
||||
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -146,8 +141,8 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
}
|
||||
|
||||
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
|
||||
if payload.ComposeFile == "" {
|
||||
payload.ComposeFile = filesystem.ComposeFileDefaultName
|
||||
if payload.ComposeFilePathInRepository == "" {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
||||
@@ -161,29 +156,16 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFile,
|
||||
AdditionalFiles: payload.AdditionalFiles,
|
||||
AutoUpdate: payload.AutoUpdate,
|
||||
Env: payload.Env,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
},
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
}
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -120,9 +119,7 @@ type swarmStackFromGitRepositoryPayload struct {
|
||||
// Password used in basic authentication. Required when RepositoryAuthentication is true.
|
||||
RepositoryPassword string `example:"myGitPassword"`
|
||||
// Path to the Stack file inside the Git repository
|
||||
ComposeFile string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
AdditionalFiles []string
|
||||
AutoUpdate *portainer.StackAutoUpdate
|
||||
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -135,14 +132,11 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
return errors.New("Invalid RepositoryReferenceName")
|
||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
||||
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
||||
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
||||
}
|
||||
if govalidator.IsNull(payload.ComposeFile) {
|
||||
payload.ComposeFile = filesystem.ComposeFileDefaultName
|
||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -165,30 +159,17 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFile,
|
||||
AdditionalFiles: payload.AdditionalFiles,
|
||||
AutoUpdate: payload.AutoUpdate,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
},
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
}
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
|
||||
@@ -54,8 +54,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/stacks/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/stacks/{id}/git/config",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackGitConfigUpdate))).Methods(http.MethodPost)
|
||||
h.Handle("/stacks/{id}/file",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
|
||||
h.Handle("/stacks/{id}/migrate",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func validateStackAutoUpdate(autoUpdate *portainer.StackAutoUpdate) error {
|
||||
if autoUpdate == nil {
|
||||
return nil
|
||||
}
|
||||
if autoUpdate.Interval == "" && autoUpdate.Webhook == "" {
|
||||
return errors.New("Both Interval and Webhook fields are empty")
|
||||
}
|
||||
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) {
|
||||
return errors.New("Invalid Webhook format")
|
||||
}
|
||||
if autoUpdate.Interval != "" {
|
||||
if _, err := time.ParseDuration(autoUpdate.Interval); err != nil {
|
||||
return errors.New("Invalid Interval format")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ValidateStackAutoUpdate_Valid(t *testing.T) {
|
||||
mock := &portainer.StackAutoUpdate{
|
||||
Webhook: "8dce8c2f-9ca1-482b-ad20-271e86536ada",
|
||||
Interval: "5h30m40s10ms",
|
||||
}
|
||||
err := validateStackAutoUpdate(mock)
|
||||
assert.NoError(t, err)
|
||||
mock = nil
|
||||
err = validateStackAutoUpdate(mock)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ValidateStackAutoUpdate_InValid(t *testing.T) {
|
||||
mock := &portainer.StackAutoUpdate{}
|
||||
err := validateStackAutoUpdate(mock)
|
||||
assert.Error(t, err)
|
||||
mock.Webhook = "fake-web-hook"
|
||||
err = validateStackAutoUpdate(mock)
|
||||
assert.Error(t, err)
|
||||
mock.Webhook = ""
|
||||
mock.Interval = "1dd2hh3mm"
|
||||
err = validateStackAutoUpdate(mock)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
type stackGitConfigUpdatePayload struct {
|
||||
AutoUpdate *portainer.StackAutoUpdate
|
||||
Env []portainer.Pair
|
||||
RepositoryReferenceName string
|
||||
}
|
||||
|
||||
func (payload *stackGitConfigUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
return errors.New("Invalid RepositoryReferenceName")
|
||||
}
|
||||
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id Stacks
|
||||
// @summary Update and redeploy an existing stack (with Git config)
|
||||
// @description Update and redeploy an existing stack (with Git config)
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags stacks
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "Stack identifier"
|
||||
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
|
||||
// @param body body stackGitConfigUpdatePayload true "Stack Git config"
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id}/git [post]
|
||||
func (handler *Handler) stackGitConfigUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
var payload stackGitConfigUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||
} else if stack.GitConfig == nil {
|
||||
msg := "No Git config in the found stack"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: msg, Err: errors.New(msg)}
|
||||
}
|
||||
|
||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||
// The EndpointID property is not available for these stacks, this API endpoint
|
||||
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
|
||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
|
||||
}
|
||||
if endpointID != int(stack.EndpointID) {
|
||||
stack.EndpointID = portainer.EndpointID(endpointID)
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
//update retrieved stack data based on the payload
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
stack.AutoUpdate = payload.AutoUpdate
|
||||
stack.Env = payload.Env
|
||||
|
||||
//save the updated stack to DB
|
||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package portainer
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -683,12 +681,6 @@ type (
|
||||
SwarmID string `json:"SwarmId" example:"jpofkc0i9uo9wtx1zesuk649w"`
|
||||
// Path to the Stack file
|
||||
EntryPoint string `json:"EntryPoint" example:"docker-compose.yml"`
|
||||
// Additional Stack files
|
||||
AdditionalFiles []string `json:"AdditionalFiles" example:""`
|
||||
// Stack auto update settings
|
||||
AutoUpdate *StackAutoUpdate `json:"AutoUpdate" example:""`
|
||||
// Git settings
|
||||
GitConfig *gittypes.RepoConfig `json:"GitConfig"`
|
||||
// A list of environment variables used during stack deployment
|
||||
Env []Pair `json:"Env" example:""`
|
||||
//
|
||||
@@ -707,13 +699,6 @@ type (
|
||||
UpdatedBy string `example:"bob"`
|
||||
}
|
||||
|
||||
// StackAutoUpdate represents the git auto sync config for stack deployment
|
||||
StackAutoUpdate struct {
|
||||
Interval string
|
||||
Webhook string //a UUID generated from client
|
||||
JobID string
|
||||
}
|
||||
|
||||
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
|
||||
StackID int
|
||||
|
||||
@@ -1342,7 +1327,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.5.0"
|
||||
APIVersion = "2.5.1"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 27
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
||||
@@ -71,13 +71,13 @@
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="$ctrl.copy()"
|
||||
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0].line) || !$ctrl.state.filteredLogs.length"
|
||||
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length"
|
||||
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="$ctrl.copySelection()"
|
||||
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0].line) || !$ctrl.state.filteredLogs.length || !$ctrl.state.selectedLines.length"
|
||||
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length || !$ctrl.state.selectedLines.length"
|
||||
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy selected lines</button
|
||||
>
|
||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.clearSelection()" ng-disabled="$ctrl.state.selectedLines.length === 0"
|
||||
@@ -97,9 +97,9 @@
|
||||
<div class="row" style="height: 54%;">
|
||||
<div class="col-sm-12" style="height: 100%;">
|
||||
<pre ng-class="{ wrap_lines: $ctrl.state.wrapLines }" class="log_viewer" scroll-glue="$ctrl.state.autoScroll" force-glue>
|
||||
<div ng-repeat="log in $ctrl.state.filteredLogs = ($ctrl.data | filter:{ 'line': $ctrl.state.search }) track by $index" class="line" ng-if="log.line"><p class="inner_line" ng-click="$ctrl.selectLine(log.line)" ng-class="{ 'line_selected': $ctrl.state.selectedLines.indexOf(log.line) > -1 }"><span ng-repeat="span in log.spans" ng-style="{ 'color': span.foregroundColor, 'background-color': span.backgroundColor }">{{ span.text }}</span></p></div>
|
||||
<div ng-repeat="line in $ctrl.state.filteredLogs = ($ctrl.data | filter:$ctrl.state.search) track by $index" class="line" ng-if="line"><p class="inner_line" ng-click="$ctrl.selectLine(line)" ng-class="{ 'line_selected': $ctrl.state.selectedLines.indexOf(line) > -1 }">{{ line }}</p></div>
|
||||
<div ng-if="!$ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
|
||||
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0].line" class="line"><p class="inner_line">No logs available</p></div>
|
||||
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]" class="line"><p class="inner_line">No logs available</p></div>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ angular.module('portainer.docker').controller('LogViewerController', [
|
||||
};
|
||||
|
||||
this.copy = function () {
|
||||
clipboard.copyText(this.state.filteredLogs.map((log) => log.line));
|
||||
clipboard.copyText(this.state.filteredLogs);
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(2000);
|
||||
};
|
||||
|
||||
@@ -1,137 +1,20 @@
|
||||
import tokenize from '@nxmix/tokenize-ansi';
|
||||
import x256 from 'x256';
|
||||
|
||||
const FOREGROUND_COLORS_BY_ANSI = {
|
||||
black: x256.colors[0],
|
||||
red: x256.colors[1],
|
||||
green: x256.colors[2],
|
||||
yellow: x256.colors[3],
|
||||
blue: x256.colors[4],
|
||||
magenta: x256.colors[5],
|
||||
cyan: x256.colors[6],
|
||||
white: x256.colors[7],
|
||||
brightBlack: x256.colors[8],
|
||||
brightRed: x256.colors[9],
|
||||
brightGreen: x256.colors[10],
|
||||
brightYellow: x256.colors[11],
|
||||
brightBlue: x256.colors[12],
|
||||
brightMagenta: x256.colors[13],
|
||||
brightCyan: x256.colors[14],
|
||||
brightWhite: x256.colors[15],
|
||||
};
|
||||
|
||||
const BACKGROUND_COLORS_BY_ANSI = {
|
||||
bgBlack: x256.colors[0],
|
||||
bgRed: x256.colors[1],
|
||||
bgGreen: x256.colors[2],
|
||||
bgYellow: x256.colors[3],
|
||||
bgBlue: x256.colors[4],
|
||||
bgMagenta: x256.colors[5],
|
||||
bgCyan: x256.colors[6],
|
||||
bgWhite: x256.colors[7],
|
||||
bgBrightBlack: x256.colors[8],
|
||||
bgBrightRed: x256.colors[9],
|
||||
bgBrightGreen: x256.colors[10],
|
||||
bgBrightYellow: x256.colors[11],
|
||||
bgBrightBlue: x256.colors[12],
|
||||
bgBrightMagenta: x256.colors[13],
|
||||
bgBrightCyan: x256.colors[14],
|
||||
bgBrightWhite: x256.colors[15],
|
||||
};
|
||||
|
||||
angular.module('portainer.docker').factory('LogHelper', [
|
||||
function LogHelperFactory() {
|
||||
'use strict';
|
||||
var helper = {};
|
||||
|
||||
function stripHeaders(logs) {
|
||||
logs = logs.substring(8);
|
||||
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
function stripEscapeCodes(logs) {
|
||||
return logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||
}
|
||||
|
||||
function cssColorFromRgb(rgb) {
|
||||
const [r, g, b] = rgb;
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
function extendedColorForToken(token) {
|
||||
const colorMode = token[1];
|
||||
|
||||
if (colorMode === 2) {
|
||||
return cssColorFromRgb(token.slice(2));
|
||||
}
|
||||
|
||||
if (colorMode === 5 && x256.colors[token[2]]) {
|
||||
return cssColorFromRgb(x256.colors[token[2]]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Return an array with each log including a line and styled spans for each entry.
|
||||
// Return an array with each line being an entry.
|
||||
// It will also remove any ANSI code related character sequences.
|
||||
// If the skipHeaders param is specified, it will strip the 8 first characters of each line.
|
||||
helper.formatLogs = function (logs, skipHeaders) {
|
||||
logs = logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||
|
||||
if (skipHeaders) {
|
||||
logs = stripHeaders(logs);
|
||||
logs = logs.substring(8);
|
||||
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||
}
|
||||
|
||||
const tokens = tokenize(logs);
|
||||
const formattedLogs = [];
|
||||
|
||||
let foregroundColor = null;
|
||||
let backgroundColor = null;
|
||||
let line = '';
|
||||
let spans = [];
|
||||
|
||||
for (const token of tokens) {
|
||||
const type = token[0];
|
||||
|
||||
if (FOREGROUND_COLORS_BY_ANSI[type]) {
|
||||
foregroundColor = cssColorFromRgb(FOREGROUND_COLORS_BY_ANSI[type]);
|
||||
} else if (type === 'moreColor') {
|
||||
foregroundColor = extendedColorForToken(token);
|
||||
} else if (type === 'fgDefault') {
|
||||
foregroundColor = null;
|
||||
} else if (BACKGROUND_COLORS_BY_ANSI[type]) {
|
||||
backgroundColor = cssColorFromRgb(BACKGROUND_COLORS_BY_ANSI[type]);
|
||||
} else if (type === 'bgMoreColor') {
|
||||
backgroundColor = extendedColorForToken(token);
|
||||
} else if (type === 'bgDefault') {
|
||||
backgroundColor = null;
|
||||
} else if (type === 'reset') {
|
||||
foregroundColor = null;
|
||||
backgroundColor = null;
|
||||
} else if (type === 'text') {
|
||||
const tokenLines = token[1].split('\n');
|
||||
|
||||
for (let i = 0; i < tokenLines.length; i++) {
|
||||
if (i !== 0) {
|
||||
formattedLogs.push({ line, spans });
|
||||
|
||||
line = '';
|
||||
spans = [];
|
||||
}
|
||||
|
||||
const text = stripEscapeCodes(tokenLines[i]);
|
||||
|
||||
line += text;
|
||||
spans.push({ foregroundColor, backgroundColor, text });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (line) {
|
||||
formattedLogs.push({ line, spans });
|
||||
}
|
||||
|
||||
return formattedLogs;
|
||||
return logs.split('\n');
|
||||
};
|
||||
|
||||
return helper;
|
||||
|
||||
@@ -100,16 +100,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
|
||||
},
|
||||
};
|
||||
|
||||
const applicationStats = {
|
||||
name: 'kubernetes.applications.application.stats',
|
||||
url: '/:pod/:container/stats',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubernetesApplicationStatsView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const stacks = {
|
||||
name: 'kubernetes.stacks',
|
||||
url: '/stacks',
|
||||
@@ -269,7 +259,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
|
||||
$stateRegistryProvider.register(applicationEdit);
|
||||
$stateRegistryProvider.register(applicationConsole);
|
||||
$stateRegistryProvider.register(applicationLogs);
|
||||
$stateRegistryProvider.register(applicationStats);
|
||||
$stateRegistryProvider.register(stacks);
|
||||
$stateRegistryProvider.register(stack);
|
||||
$stateRegistryProvider.register(stackLogs);
|
||||
|
||||
@@ -131,13 +131,6 @@
|
||||
</td>
|
||||
<td>{{ item.CreationDate | getisodate }}</td>
|
||||
<td>
|
||||
<a
|
||||
ng-if="item.Status === 'Running' && $ctrl.useServerMetrics"
|
||||
ui-sref="kubernetes.applications.application.stats({ pod: item.PodName, container: item.Name })"
|
||||
style="margin-right: 10px;"
|
||||
>
|
||||
<i class="fa fa-chart-area" aria-hidden="true"></i> Stats
|
||||
</a>
|
||||
<a ui-sref="kubernetes.applications.application.logs({ pod: item.PodName, container: item.Name })"> <i class="fa fa-file-alt" aria-hidden="true"></i> Logs </a>
|
||||
<a ng-if="item.Status === 'Running'" ui-sref="kubernetes.applications.application.console({ pod: item.PodName, container: item.Name })" style="margin-left: 10px;">
|
||||
<i class="fa fa-terminal" aria-hidden="true"></i> Console
|
||||
|
||||
@@ -9,6 +9,5 @@ angular.module('portainer.kubernetes').component('kubernetesContainersDatatable'
|
||||
orderBy: '@',
|
||||
refreshCallback: '<',
|
||||
isPod: '<',
|
||||
useServerMetrics: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('ResourcePool')">
|
||||
Namespace
|
||||
Resource pool
|
||||
<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')">
|
||||
Namespace
|
||||
Resource pool
|
||||
<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')">
|
||||
Namespace
|
||||
Resource Pool
|
||||
<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')">
|
||||
Namespace
|
||||
Resource pool
|
||||
<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 namespace
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add resource pool
|
||||
</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 namespace available.</td>
|
||||
<td colspan="4" class="text-center text-muted">No resource pool available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('ResourcePool.Namespace.Name')">
|
||||
Namespace
|
||||
Resource pool
|
||||
<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>
|
||||
|
||||
@@ -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">Namespaces <span class="menu-icon fa fa-layer-group fa-fw"></span></a>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -132,14 +132,4 @@ angular
|
||||
};
|
||||
return values[text] || text;
|
||||
};
|
||||
})
|
||||
.filter('kubernetesApplicationIngressEmptyHostname', function () {
|
||||
'use strict';
|
||||
return function (value) {
|
||||
if (value === '') {
|
||||
return '<use IP>';
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ class KubernetesFormValidationHelper {
|
||||
const groupped = _.groupBy(names);
|
||||
const res = {};
|
||||
_.forEach(names, (name, index) => {
|
||||
if (name && groupped[name].length > 1) {
|
||||
if (groupped[name].length > 1 && name) {
|
||||
res[index] = name;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,8 +29,6 @@ class KubernetesResourceReservationHelper {
|
||||
let res = parseInt(cpu, 10);
|
||||
if (_.endsWith(cpu, 'm')) {
|
||||
res /= 1000;
|
||||
} else if (_.endsWith(cpu, 'n')) {
|
||||
res /= 1000000000;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,22 @@ import _ from 'lodash-es';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
import {
|
||||
KubernetesResourcePoolIngressClassAnnotationFormValue,
|
||||
KubernetesResourcePoolIngressClassFormValue,
|
||||
KubernetesResourcePoolIngressClassHostFormValue,
|
||||
} from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesResourcePoolIngressClassAnnotationFormValue, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngress, KubernetesIngressRule } from './models';
|
||||
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
||||
import { KubernetesIngressClassAnnotation, KubernetesIngressClassRewriteTargetAnnotations } from './constants';
|
||||
|
||||
export class KubernetesIngressConverter {
|
||||
// TODO: refactor @LP
|
||||
// currently only allows the first non-empty host to be used as the "configured" host.
|
||||
// As we currently only allow a single host to be used for a Portianer-managed ingress
|
||||
// it's working as the only non-empty host will be the one defined by the admin
|
||||
// but it will take a random existing host for non Portainer ingresses (CLI deployed)
|
||||
// Also won't support multiple hosts if we make it available in the future
|
||||
static apiToModel(data) {
|
||||
let host = undefined;
|
||||
const paths = _.flatMap(data.spec.rules, (rule) => {
|
||||
host = host || rule.host; // TODO: refactor @LP - read above
|
||||
return !rule.http
|
||||
? []
|
||||
: _.map(rule.http.paths, (path) => {
|
||||
@@ -37,11 +41,7 @@ export class KubernetesIngressConverter {
|
||||
? data.metadata.annotations[KubernetesIngressClassAnnotation]
|
||||
: data.spec.ingressClassName;
|
||||
res.Paths = paths;
|
||||
res.Hosts = _.uniq(_.map(data.spec.rules, 'host'));
|
||||
const idx = _.findIndex(res.Hosts, (h) => h === undefined);
|
||||
if (idx >= 0) {
|
||||
res.Hosts.splice(idx, 1, '');
|
||||
}
|
||||
res.Host = host;
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class KubernetesIngressConverter {
|
||||
_.extend(res.Annotations, KubernetesIngressClassRewriteTargetAnnotations[formValues.IngressClass.Type]);
|
||||
}
|
||||
res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name;
|
||||
res.Hosts = formValues.Hosts;
|
||||
res.Host = formValues.Host;
|
||||
res.Paths = formValues.Paths;
|
||||
return res;
|
||||
}
|
||||
@@ -96,13 +96,7 @@ export class KubernetesIngressConverter {
|
||||
if (ingress) {
|
||||
fv.Selected = true;
|
||||
fv.WasSelected = true;
|
||||
fv.Hosts = _.map(ingress.Hosts, (host) => {
|
||||
const hfv = new KubernetesResourcePoolIngressClassHostFormValue();
|
||||
hfv.Host = host;
|
||||
hfv.PreviousHost = host;
|
||||
hfv.IsNew = false;
|
||||
return hfv;
|
||||
});
|
||||
fv.Host = ingress.Host;
|
||||
const [[rewriteKey]] = _.toPairs(KubernetesIngressClassRewriteTargetAnnotations[ic.Type]);
|
||||
const annotations = _.map(_.toPairs(ingress.Annotations), ([key, value]) => {
|
||||
if (key === rewriteKey) {
|
||||
@@ -134,17 +128,16 @@ export class KubernetesIngressConverter {
|
||||
p.Host = '';
|
||||
}
|
||||
});
|
||||
const hostsWithRules = [];
|
||||
const groups = _.groupBy(data.Paths, 'Host');
|
||||
let rules = _.map(groups, (paths, host) => {
|
||||
const updatedHost = _.find(data.Hosts, (h) => {
|
||||
return h === host || h.PreviousHost === host;
|
||||
});
|
||||
host = updatedHost.Host || updatedHost;
|
||||
if (updatedHost.NeedsDeletion) {
|
||||
return;
|
||||
}
|
||||
const rules = _.map(groups, (paths, host) => {
|
||||
const rule = new KubernetesIngressRuleCreatePayload();
|
||||
|
||||
if (host === 'undefined' || _.isEmpty(host)) {
|
||||
host = data.Host;
|
||||
}
|
||||
if (host === data.PreviousHost && host !== data.Host) {
|
||||
host = data.Host;
|
||||
}
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(rule, 'host', host);
|
||||
rule.http.paths = _.map(paths, (p) => {
|
||||
const path = new KubernetesIngressRulePathCreatePayload();
|
||||
@@ -153,27 +146,11 @@ export class KubernetesIngressConverter {
|
||||
path.backend.servicePort = p.Port;
|
||||
return path;
|
||||
});
|
||||
hostsWithRules.push(host);
|
||||
return rule;
|
||||
});
|
||||
rules = _.without(rules, undefined);
|
||||
const keptHosts = _.without(
|
||||
_.map(data.Hosts, (h) => (h.NeedsDeletion ? undefined : h.Host || h)),
|
||||
undefined
|
||||
);
|
||||
const hostsWithoutRules = _.without(keptHosts, ...hostsWithRules);
|
||||
const emptyRules = _.map(hostsWithoutRules, (host) => {
|
||||
return { host: host };
|
||||
});
|
||||
rules = _.concat(rules, emptyRules);
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(res, 'spec.rules', rules);
|
||||
} else if (data.Hosts) {
|
||||
res.spec.rules = [];
|
||||
_.forEach(data.Hosts, (host) => {
|
||||
if (!host.NeedsDeletion) {
|
||||
res.spec.rules.push({ host: host.Host });
|
||||
}
|
||||
});
|
||||
} else if (data.Host) {
|
||||
res.spec.rules = [{ host: data.Host }];
|
||||
} else {
|
||||
delete res.spec.rules;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ export function KubernetesIngress() {
|
||||
Name: '',
|
||||
Namespace: '',
|
||||
Annotations: {},
|
||||
// Host: undefined,
|
||||
Hosts: [],
|
||||
// PreviousHost: undefined, // only use for RP ingress host edit
|
||||
Host: undefined,
|
||||
PreviousHost: undefined, // only use for RP ingress host edit
|
||||
Paths: [],
|
||||
IngressClassName: '',
|
||||
};
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import PortainerError from 'Portainer/error';
|
||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||
|
||||
class KubernetesMetricsService {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesMetrics) {
|
||||
this.$async = $async;
|
||||
this.KubernetesMetrics = KubernetesMetrics;
|
||||
|
||||
this.capabilitiesAsync = this.capabilitiesAsync.bind(this);
|
||||
this.getPodAsync = this.getPodAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
async capabilitiesAsync(endpointID) {
|
||||
try {
|
||||
await this.KubernetesMetrics().capabilities({ endpointId: endpointID }).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve metrics', err);
|
||||
}
|
||||
}
|
||||
|
||||
capabilities(endpointID) {
|
||||
return this.$async(this.capabilitiesAsync, endpointID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats
|
||||
*
|
||||
* @param {string} namespace
|
||||
* @param {string} podName
|
||||
*/
|
||||
async getPodAsync(namespace, podName) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = podName;
|
||||
const data = await this.KubernetesMetrics(namespace).getPod(params).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve pod stats', err);
|
||||
}
|
||||
}
|
||||
|
||||
getPod(namespace, podName) {
|
||||
return this.$async(this.getPodAsync, namespace, podName);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesMetricsService;
|
||||
angular.module('portainer.kubernetes').service('KubernetesMetricsService', KubernetesMetricsService);
|
||||
@@ -1,27 +0,0 @@
|
||||
angular.module('portainer.kubernetes').factory('KubernetesMetrics', [
|
||||
'$resource',
|
||||
'API_ENDPOINT_ENDPOINTS',
|
||||
'EndpointProvider',
|
||||
function KubernetesMetrics($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return function (namespace) {
|
||||
const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/metrics.k8s.io/v1beta1';
|
||||
const podUrl = `${url}${namespace ? '/namespaces/:namespace' : ''}/pods/:id`;
|
||||
|
||||
return $resource(
|
||||
url,
|
||||
{
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
namespace: namespace,
|
||||
},
|
||||
{
|
||||
capabilities: { method: 'GET' },
|
||||
getPod: {
|
||||
method: 'GET',
|
||||
url: podUrl,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
},
|
||||
]);
|
||||
@@ -18,7 +18,7 @@ const _KubernetesApplicationFormValues = Object.freeze({
|
||||
AutoScaler: {},
|
||||
Containers: [],
|
||||
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
|
||||
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.ISOLATED,
|
||||
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
|
||||
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
|
||||
Configurations: [], // KubernetesApplicationConfigurationFormValue list
|
||||
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
|
||||
|
||||
@@ -17,7 +17,7 @@ export function KubernetesResourcePoolIngressClassFormValue(ingressClass) {
|
||||
IngressClass: ingressClass,
|
||||
RewriteTarget: false,
|
||||
Annotations: [], // KubernetesResourcePoolIngressClassAnnotationFormValue
|
||||
Hosts: [],
|
||||
Host: undefined,
|
||||
Selected: false,
|
||||
WasSelected: false,
|
||||
AdvancedConfig: false,
|
||||
@@ -31,12 +31,3 @@ export function KubernetesResourcePoolIngressClassAnnotationFormValue() {
|
||||
Value: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function KubernetesResourcePoolIngressClassHostFormValue() {
|
||||
return {
|
||||
Host: '',
|
||||
PreviousHost: '',
|
||||
NeedsDeletion: false,
|
||||
IsNew: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</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">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</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 namespace.</p
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> An application with the same name already exists inside the selected resource pool.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,11 +84,11 @@
|
||||
<!-- #endregion -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Namespace
|
||||
Resource pool
|
||||
</div>
|
||||
<!-- #region NAMESPACE -->
|
||||
<!-- #region RESOURCE POOL -->
|
||||
<div class="form-group">
|
||||
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
|
||||
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</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 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.
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
@@ -554,6 +554,39 @@
|
||||
<!-- 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) ||
|
||||
@@ -572,7 +605,7 @@
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Isolated
|
||||
</div>
|
||||
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
|
||||
<p>Every instance of this application will use their own data</p>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -592,40 +625,7 @@
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Isolated
|
||||
</div>
|
||||
<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>
|
||||
<p>Every instance of this application will use their own data</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 namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums are
|
||||
inherited from the namespace quota.
|
||||
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.
|
||||
</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 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.
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -768,7 +768,7 @@
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Global
|
||||
</div>
|
||||
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
|
||||
<p>Deploy an instance of this container 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>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
|
||||
<p>Deploy an instance of this container on each node of the cluster</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1264,14 +1264,7 @@
|
||||
tooltip-enable="ctrl.disableLoadBalancerEdit()"
|
||||
uib-tooltip="Edition is not allowed while the Load Balancer is in 'Pending' state"
|
||||
>
|
||||
<div
|
||||
class="input-group input-group-sm"
|
||||
ng-class="{
|
||||
striked: publishedPort.NeedsDeletion,
|
||||
'col-sm-2': ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS,
|
||||
'col-sm-3': ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.INGRESS
|
||||
}"
|
||||
>
|
||||
<div class="col-sm-3 input-group input-group-sm" ng-class="{ striked: publishedPort.NeedsDeletion }">
|
||||
<span class="input-group-addon">container port</span>
|
||||
<input
|
||||
type="number"
|
||||
@@ -1334,7 +1327,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-2 input-group input-group-sm"
|
||||
class="col-sm-3 input-group input-group-sm"
|
||||
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||
ng-if="
|
||||
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||
@@ -1356,28 +1349,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-2 input-group input-group-sm"
|
||||
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||
ng-if="
|
||||
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
|
||||
"
|
||||
>
|
||||
<span class="input-group-addon">hostname</span>
|
||||
<select
|
||||
class="form-control"
|
||||
name="ingress_hostname_{{ $index }}"
|
||||
ng-model="publishedPort.IngressHost"
|
||||
ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in ctrl.ingressHostnames"
|
||||
ng-change="ctrl.onChangePublishedPorts()"
|
||||
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a hostname</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-2 input-group input-group-sm"
|
||||
class="col-sm-3 input-group input-group-sm"
|
||||
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||
ng-if="
|
||||
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||
|
||||
@@ -320,7 +320,7 @@ class KubernetesCreateApplicationController {
|
||||
const p = new KubernetesApplicationPublishedPortFormValue();
|
||||
const ingresses = this.filteredIngresses;
|
||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
|
||||
if (this.formValues.PublishedPorts.length) {
|
||||
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
||||
}
|
||||
@@ -331,7 +331,7 @@ class KubernetesCreateApplicationController {
|
||||
const ingresses = this.filteredIngresses;
|
||||
_.forEach(this.formValues.PublishedPorts, (p) => {
|
||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -388,8 +388,7 @@ class KubernetesCreateApplicationController {
|
||||
onChangePortMappingIngress(index) {
|
||||
const publishedPort = this.formValues.PublishedPorts[index];
|
||||
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
||||
this.ingressHostnames = ingress.Hosts;
|
||||
publishedPort.IngressHost = this.ingressHostnames.length ? this.ingressHostnames[0] : [];
|
||||
publishedPort.IngressHost = ingress.Host;
|
||||
this.onChangePublishedPorts();
|
||||
}
|
||||
|
||||
@@ -781,7 +780,6 @@ class KubernetesCreateApplicationController {
|
||||
|
||||
refreshIngresses(namespace) {
|
||||
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
||||
this.ingressHostnames = this.filteredIngresses.length ? this.filteredIngresses[0].Hosts : [];
|
||||
if (!this.publishViaIngressEnabled()) {
|
||||
if (this.savedFormValues) {
|
||||
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<kubernetes-view-header title="Application details" state="kubernetes.applications.application" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</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>Namespace</td>
|
||||
<td>Resource pool</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>
|
||||
@@ -587,7 +587,6 @@
|
||||
table-key="kubernetes.application.containers"
|
||||
is-pod="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD"
|
||||
order-by="{{ ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD ? 'Name' : 'PodName' }}"
|
||||
use-server-metrics="ctrl.state.useServerMetrics"
|
||||
>
|
||||
</kubernetes-containers-datatable>
|
||||
</div>
|
||||
|
||||
@@ -106,8 +106,7 @@ class KubernetesApplicationController {
|
||||
KubernetesStackService,
|
||||
KubernetesPodService,
|
||||
KubernetesNodeService,
|
||||
KubernetesNamespaceHelper,
|
||||
EndpointProvider
|
||||
KubernetesNamespaceHelper
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
@@ -126,8 +125,6 @@ class KubernetesApplicationController {
|
||||
|
||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
|
||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||
this.KubernetesServiceTypes = KubernetesServiceTypes;
|
||||
this.KubernetesPodContainerTypes = KubernetesPodContainerTypes;
|
||||
@@ -338,7 +335,6 @@ class KubernetesApplicationController {
|
||||
placementWarning: false,
|
||||
expandedNote: false,
|
||||
useIngress: false,
|
||||
useServerMetrics: this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.UseServerMetrics,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('application');
|
||||
|
||||
@@ -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">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</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,82 +0,0 @@
|
||||
<kubernetes-view-header title="Application stats" state="kubernetes.applications.application.stats" view-ready="ctrl.state.viewReady">
|
||||
<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 })">{{
|
||||
ctrl.state.transition.applicationName
|
||||
}}</a>
|
||||
> Pods > {{ ctrl.state.transition.podName }} > Containers > {{ ctrl.state.transition.containerName }} > Stats
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<information-panel ng-if="!ctrl.state.getMetrics" title-text="Unable to retrieve container metrics">
|
||||
<span class="small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Portainer was unable to retrieve any metrics associated to that container. Please contact your administrator to ensure that the Kubernetes metrics feature is properly
|
||||
configured.
|
||||
</span>
|
||||
</information-panel>
|
||||
<div class="row" ng-if="ctrl.state.getMetrics">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-info-circle" title-text="About statistics"> </rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
This view displays real-time statistics about the container <b>{{ ctrl.state.transition.containerName | trimcontainername }}</b
|
||||
>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left">
|
||||
Refresh rate
|
||||
</label>
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<select id="refreshRate" ng-model="ctrl.state.refreshRate" ng-change="ctrl.changeUpdateRepeater()" class="form-control">
|
||||
<option value="30">30s</option>
|
||||
<option value="60">60s</option>
|
||||
</select>
|
||||
</div>
|
||||
<span>
|
||||
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.networkStatsUnavailable">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted"> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i> Network stats are unavailable for this container. </span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="ctrl.state.getMetrics">
|
||||
<div class="col-lg-6 col-md-12 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-chart-area" title-text="Memory usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="memoryChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12 col-sm-12" ng-if="!ctrl.state.networkStatsUnavailable">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-chart-area" title-text="CPU usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="cpuChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +0,0 @@
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationStatsView', {
|
||||
templateUrl: './stats.html',
|
||||
controller: 'KubernetesApplicationStatsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
||||
@@ -1,164 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
||||
|
||||
class KubernetesApplicationStatsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $interval, $document, Notifications, KubernetesPodService, KubernetesNodeService, KubernetesMetricsService, ChartService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$interval = $interval;
|
||||
this.$document = $document;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesMetricsService = KubernetesMetricsService;
|
||||
this.ChartService = ChartService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
}
|
||||
|
||||
changeUpdateRepeater() {
|
||||
var cpuChart = this.cpuChart;
|
||||
var memoryChart = this.memoryChart;
|
||||
|
||||
this.stopRepeater();
|
||||
this.setUpdateRepeater(cpuChart, memoryChart);
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(1500);
|
||||
}
|
||||
|
||||
updateCPUChart() {
|
||||
const label = moment(this.stats.read).format('HH:mm:ss');
|
||||
|
||||
this.ChartService.UpdateCPUChart(label, this.stats.CPUUsage, this.cpuChart);
|
||||
}
|
||||
|
||||
updateMemoryChart() {
|
||||
const label = moment(this.stats.read).format('HH:mm:ss');
|
||||
|
||||
this.ChartService.UpdateMemoryChart(label, this.stats.MemoryUsage, this.stats.MemoryCache, this.memoryChart);
|
||||
}
|
||||
|
||||
stopRepeater() {
|
||||
var repeater = this.repeater;
|
||||
if (angular.isDefined(repeater)) {
|
||||
this.$interval.cancel(repeater);
|
||||
repeater = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
setUpdateRepeater() {
|
||||
const refreshRate = this.state.refreshRate;
|
||||
|
||||
this.repeater = this.$interval(async () => {
|
||||
try {
|
||||
await this.getStats();
|
||||
|
||||
this.updateCPUChart();
|
||||
this.updateMemoryChart();
|
||||
} catch (error) {
|
||||
this.stopRepeater();
|
||||
this.Notifications.error('Failure', error);
|
||||
}
|
||||
}, refreshRate * 1000);
|
||||
}
|
||||
|
||||
initCharts() {
|
||||
const cpuChartCtx = $('#cpuChart');
|
||||
const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
|
||||
this.cpuChart = cpuChart;
|
||||
|
||||
const memoryChartCtx = $('#memoryChart');
|
||||
const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
|
||||
this.memoryChart = memoryChart;
|
||||
|
||||
this.updateCPUChart();
|
||||
this.updateMemoryChart();
|
||||
this.setUpdateRepeater();
|
||||
}
|
||||
|
||||
getStats() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const stats = await this.KubernetesMetricsService.getPod(this.state.transition.namespace, this.state.transition.podName);
|
||||
const container = _.find(stats.containers, { name: this.state.transition.containerName });
|
||||
if (container) {
|
||||
const memory = filesizeParser(container.usage.memory);
|
||||
const cpu = KubernetesResourceReservationHelper.parseCPU(container.usage.cpu);
|
||||
this.stats = {
|
||||
read: stats.timestamp,
|
||||
preread: '',
|
||||
MemoryCache: 0,
|
||||
MemoryUsage: memory,
|
||||
NumProcs: '',
|
||||
isWindows: false,
|
||||
PreviousCPUTotalUsage: 0,
|
||||
CPUUsage: (cpu / this.nodeCPU) * 100,
|
||||
CPUCores: 0,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application stats');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.stopRepeater();
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
autoRefresh: false,
|
||||
refreshRate: '30',
|
||||
viewReady: false,
|
||||
transition: {
|
||||
podName: this.$transition$.params().pod,
|
||||
containerName: this.$transition$.params().container,
|
||||
namespace: this.$transition$.params().namespace,
|
||||
applicationName: this.$transition$.params().name,
|
||||
},
|
||||
getMetrics: false,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.KubernetesMetricsService.getPod(this.state.transition.namespace, this.state.transition.podName);
|
||||
} catch (error) {
|
||||
this.state.getMetrics = false;
|
||||
this.state.viewReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const podRaw = await this.KubernetesPodService.get(this.state.transition.namespace, this.state.transition.podName);
|
||||
const pod = KubernetesPodConverter.apiToModel(podRaw.Raw);
|
||||
if (pod) {
|
||||
const node = await this.KubernetesNodeService.get(pod.Node);
|
||||
this.nodeCPU = node.CPU;
|
||||
} else {
|
||||
throw new Error('Unable to find pod');
|
||||
}
|
||||
await this.getStats();
|
||||
this.state.getMetrics = true;
|
||||
|
||||
this.$document.ready(() => {
|
||||
this.initCharts();
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application stats');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationStatsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationStatsController', KubernetesApplicationStatsController);
|
||||
@@ -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 namespace.</p
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A configuration with the same name already exists inside the selected resource pool.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Namespace
|
||||
Resource pool
|
||||
</div>
|
||||
|
||||
<!-- resource-pool -->
|
||||
<div class="form-group">
|
||||
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
|
||||
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</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 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.
|
||||
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.
|
||||
</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">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</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>Namespace</td>
|
||||
<td>Resource Pool</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>
|
||||
|
||||
@@ -196,27 +196,7 @@
|
||||
<label class="control-label text-left">
|
||||
Enable features using metrics server
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" ng-change="ctrl.enableMetricsServer()" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="ctrl.state.metrics.pending && ctrl.state.metrics.userClick" class="col-sm-12 small text-muted" style="margin-top: 5px;">
|
||||
Checking metrics API... <i class="fa fa-spinner fa-spin" style="margin-left: 2px;"></i>
|
||||
</div>
|
||||
<div
|
||||
ng-if="!ctrl.state.metrics.pending && ctrl.state.metrics.isServerRunning && ctrl.state.metrics.userClick"
|
||||
class="col-sm-12 small text-muted"
|
||||
style="margin-top: 5px;"
|
||||
>
|
||||
<i class="fa fa-check green-icon" aria-hidden="true" style="margin-right: 2px;"></i> Successfully reached metrics API
|
||||
</div>
|
||||
<div
|
||||
ng-if="!ctrl.state.metrics.pending && !ctrl.state.metrics.isServerRunning && ctrl.state.metrics.userClick"
|
||||
class="col-sm-12 small text-muted"
|
||||
style="margin-top: 5px;"
|
||||
>
|
||||
<i class="fa fa-times red-icon" aria-hidden="true" style="margin-right: 2px;"></i> Unable to reach metrics API, make sure metrics server is properly deployed inside
|
||||
that cluster.
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ class KubernetesConfigureController {
|
||||
ModalService,
|
||||
KubernetesNamespaceHelper,
|
||||
KubernetesResourcePoolService,
|
||||
KubernetesIngressService,
|
||||
KubernetesMetricsService
|
||||
KubernetesIngressService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
@@ -42,7 +41,6 @@ class KubernetesConfigureController {
|
||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
this.KubernetesMetricsService = KubernetesMetricsService;
|
||||
|
||||
this.IngressClassTypes = KubernetesIngressClassTypes;
|
||||
|
||||
@@ -141,20 +139,32 @@ class KubernetesConfigureController {
|
||||
}
|
||||
|
||||
async removeIngressesAcrossNamespaces() {
|
||||
const promises = [];
|
||||
const ingressesToDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
|
||||
const allResourcePools = await this.KubernetesResourcePoolService.get();
|
||||
const resourcePools = _.filter(
|
||||
allResourcePools,
|
||||
(resourcePool) =>
|
||||
!this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !this.KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
|
||||
);
|
||||
|
||||
ingressesToDel.forEach((ingress) => {
|
||||
resourcePools.forEach((resourcePool) => {
|
||||
promises.push(this.KubernetesIngressService.delete(resourcePool.Namespace.Name, ingress.Name));
|
||||
if (!ingressesToDel.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
const oldEndpointID = this.EndpointProvider.endpointID();
|
||||
this.EndpointProvider.setEndpointID(this.endpoint.Id);
|
||||
|
||||
try {
|
||||
const allResourcePools = await this.KubernetesResourcePoolService.get();
|
||||
const resourcePools = _.filter(
|
||||
allResourcePools,
|
||||
(resourcePool) =>
|
||||
!this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !this.KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
|
||||
);
|
||||
|
||||
ingressesToDel.forEach((ingress) => {
|
||||
resourcePools.forEach((resourcePool) => {
|
||||
promises.push(this.KubernetesIngressService.delete(resourcePool.Namespace.Name, ingress.Name));
|
||||
});
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
this.EndpointProvider.setEndpointID(oldEndpointID);
|
||||
}
|
||||
|
||||
const responses = await Promise.allSettled(promises);
|
||||
responses.forEach((respons) => {
|
||||
@@ -163,27 +173,6 @@ class KubernetesConfigureController {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableMetricsServer() {
|
||||
if (this.formValues.UseServerMetrics) {
|
||||
this.state.metrics.userClick = true;
|
||||
this.state.metrics.pending = true;
|
||||
this.KubernetesMetricsService.capabilities(this.endpoint.Id)
|
||||
.then(() => {
|
||||
this.state.metrics.isServerRunning = true;
|
||||
this.state.metrics.pending = false;
|
||||
this.formValues.UseServerMetrics = true;
|
||||
})
|
||||
.catch(() => {
|
||||
this.state.metrics.isServerRunning = false;
|
||||
this.state.metrics.pending = false;
|
||||
this.formValues.UseServerMetrics = false;
|
||||
});
|
||||
} else {
|
||||
this.state.metrics.userClick = false;
|
||||
this.formValues.UseServerMetrics = false;
|
||||
}
|
||||
}
|
||||
|
||||
async configureAsync() {
|
||||
try {
|
||||
@@ -245,11 +234,6 @@ class KubernetesConfigureController {
|
||||
duplicates: {
|
||||
ingressClasses: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
metrics: {
|
||||
pending: false,
|
||||
isServerRunning: false,
|
||||
userClick: false,
|
||||
},
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
|
||||
@@ -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 ? 'Namespace' : 'Namespaces' }}</div>
|
||||
<div class="comment">{{ ctrl.pools.length === 1 ? 'Resource pool' : 'Resource pools' }}</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">Namespace</label>
|
||||
<label for="target_node" class="col-lg-1 col-sm-2 control-label text-left">Resource pool</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 namespaces data');
|
||||
this.Notifications.error('Failure', err, 'Unable to load resource pools data');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<kubernetes-view-header title="Namespace access management" state="kubernetes.resourcePools.resourcePool.access" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> >
|
||||
<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> >
|
||||
<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="Namespace"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-plug" title-text="Resource pool"></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 namespace information');
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool information');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 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>
|
||||
|
||||
<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 namespace with the same name already exists.</p>
|
||||
<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>
|
||||
</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 namespace segments the underlying physical Kubernetes cluster into smaller virtual clusters. You should assign a capped limit of resources to this namespace or
|
||||
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
|
||||
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 namespace. Set this quota to 0 to effectively disable the use of load
|
||||
balancers in this namespace.
|
||||
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.
|
||||
</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 namespace.
|
||||
prevent the usage of a specific storage option inside this resource pool.
|
||||
</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
|
||||
namespace.
|
||||
resource pool.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses">
|
||||
<div class="text-muted col-sm-12" style="width: 100%;">
|
||||
<div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i> {{ ic.IngressClass.Name }}
|
||||
@@ -225,58 +225,30 @@
|
||||
|
||||
<div ng-if="ic.Selected">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Hostnames
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
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>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addHostname(ic)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<div ng-repeat="item in ic.Hosts track by $index" style="margin-top: 2px;">
|
||||
<div class="form-inline">
|
||||
<div class="col-sm-10 input-group input-group-sm">
|
||||
<span class="input-group-addon">Hostname</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
||||
ng-model="item.Host"
|
||||
ng-change="ctrl.onChangeIngressHostname()"
|
||||
placeholder="foo"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeHostname(ic, $index)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="
|
||||
resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid ||
|
||||
ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
|
||||
"
|
||||
>
|
||||
<ng-messages for="resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This hostname is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="control-label text-left col-sm-2">
|
||||
Hostname
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Hostname associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via this hostname."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" name="ingress_host_{{ $index }}" ng-model="ic.Host" placeholder="host.com" ng-change="ctrl.onChangeIngressHostname()" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="resourcePoolCreationForm['ingress_host_' + $index].$invalid || ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolCreationForm['ingress_host_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
This host is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
@@ -351,7 +323,7 @@
|
||||
ng-click="ctrl.createResourcePool()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Create namespace</span>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Create resource pool</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,7 @@ import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import {
|
||||
KubernetesResourcePoolFormValues,
|
||||
KubernetesResourcePoolIngressClassAnnotationFormValue,
|
||||
KubernetesResourcePoolIngressClassHostFormValue,
|
||||
} from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
@@ -38,43 +34,17 @@ class KubernetesCreateResourcePoolController {
|
||||
|
||||
onChangeIngressHostname() {
|
||||
const state = this.state.duplicates.ingressHosts;
|
||||
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
||||
const hostnames = _.map(hosts, 'Host');
|
||||
const hostnamesWithoutRemoved = _.filter(hostnames, (h) => !h.NeedsDeletion);
|
||||
const allHosts = _.flatMap(this.allIngresses, 'Hosts');
|
||||
const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnamesWithoutRemoved);
|
||||
_.forEach(hostnames, (host, idx) => {
|
||||
if (host !== undefined && _.includes(allHosts, host)) {
|
||||
formDuplicates[idx] = host;
|
||||
|
||||
const hosts = _.map(this.formValues.IngressClasses, 'Host');
|
||||
const allHosts = _.map(this.allIngresses, 'Host');
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(hosts);
|
||||
_.forEach(hosts, (host, idx) => {
|
||||
if (_.includes(allHosts, host) && host !== undefined) {
|
||||
duplicates[idx] = host;
|
||||
}
|
||||
});
|
||||
const duplicates = {};
|
||||
let count = 0;
|
||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
||||
duplicates[ic.IngressClass.Name] = {};
|
||||
_.forEach(ic.Hosts, (hostFV, hostIdx) => {
|
||||
if (hostFV.Host === formDuplicates[count]) {
|
||||
duplicates[ic.IngressClass.Name][hostIdx] = hostFV.Host;
|
||||
}
|
||||
count++;
|
||||
});
|
||||
});
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = false;
|
||||
_.forIn(duplicates, (value) => {
|
||||
if (Object.keys(value).length > 0) {
|
||||
state.hasRefs = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addHostname(ingressClass) {
|
||||
ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
|
||||
removeHostname(ingressClass, index) {
|
||||
ingressClass.Hosts.splice(index, 1);
|
||||
this.onChangeIngressHostname();
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
@@ -115,7 +85,7 @@ class KubernetesCreateResourcePoolController {
|
||||
}
|
||||
}
|
||||
|
||||
/* #region CREATE NAMESPACE */
|
||||
/* #region CREATE RESOURCE POOL */
|
||||
async createResourcePoolAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
@@ -123,10 +93,10 @@ class KubernetesCreateResourcePoolController {
|
||||
const owner = this.Authentication.getUserDetails().username;
|
||||
this.formValues.Owner = owner;
|
||||
await this.KubernetesResourcePoolService.create(this.formValues);
|
||||
this.Notifications.success('Namespace successfully created', this.formValues.Name);
|
||||
this.Notifications.success('Resource pool successfully created', this.formValues.Name);
|
||||
this.$state.go('kubernetes.resourcePools');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create namespace');
|
||||
this.Notifications.error('Failure', err, 'Unable to create resource pool');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
@@ -151,12 +121,12 @@ class KubernetesCreateResourcePoolController {
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region GET NAMESPACES */
|
||||
/* #region GET RESOURCE POOLS */
|
||||
async getResourcePoolsAsync() {
|
||||
try {
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pools');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,11 +168,6 @@ class KubernetesCreateResourcePoolController {
|
||||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
|
||||
}
|
||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
||||
if (ic.Hosts.length === 0) {
|
||||
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 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>
|
||||
|
||||
<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> Namespace </uib-tab-heading>
|
||||
<uib-tab-heading> <i class="fa fa-layer-group space-right" aria-hidden="true"></i> Resource pool </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 namespace."
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
|
||||
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 namespace. Set this quota to 0 to effectively disable the use of
|
||||
load balancers in this namespace.
|
||||
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.
|
||||
</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 namespace.
|
||||
this resource pool.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses">
|
||||
<div class="text-muted col-sm-12" style="width: 100%;">
|
||||
<div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i> {{ ic.IngressClass.Name }}
|
||||
@@ -184,57 +184,34 @@
|
||||
|
||||
<div ng-if="ic.Selected">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Hostnames
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
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>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addHostname(ic)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
|
||||
</span>
|
||||
<label class="control-label text-left col-sm-2">
|
||||
Hostname
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Hostname associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via this hostname."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
class="form-control"
|
||||
name="ingress_host_{{ $index }}"
|
||||
ng-model="ic.Host"
|
||||
placeholder="host.com"
|
||||
ng-change="ctrl.onChangeIngressHostname()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<div ng-repeat="item in ic.Hosts track by $index" style="margin-top: 2px;">
|
||||
<div class="form-inline">
|
||||
<div class="col-sm-10 input-group input-group-sm" ng-class="{ striked: item.NeedsDeletion }">
|
||||
<span class="input-group-addon">Hostname</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
||||
ng-model="item.Host"
|
||||
ng-change="ctrl.onChangeIngressHostname()"
|
||||
placeholder="foo"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
|
||||
<button ng-if="!item.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeHostname(ic, $index)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button ng-if="item.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreHostname(item)">
|
||||
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="resourcePoolEditForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid || item.Duplicate"
|
||||
>
|
||||
<ng-messages for="resourcePoolEditForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="item.Duplicate">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
This hostname is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="resourcePoolEditForm['ingress_host_' + $index].$invalid || ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolEditForm['ingress_host_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
This host is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -307,7 +284,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 namespace.
|
||||
effectively prevent the usage of a specific storage option inside this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
@@ -342,7 +319,7 @@
|
||||
ng-click="ctrl.updateResourcePool()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Update namespace</span>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Update resource pool</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -389,7 +366,7 @@
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.getApplications"
|
||||
loading="ctrl.state.applicationsLoading"
|
||||
title-text="Applications running in this namespace"
|
||||
title-text="Applications running in this resource pool"
|
||||
title-icon="fa-laptop-code"
|
||||
>
|
||||
</kubernetes-resource-pool-applications-datatable>
|
||||
|
||||
@@ -4,11 +4,7 @@ import filesizeParser from 'filesize-parser';
|
||||
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
import {
|
||||
KubernetesResourcePoolFormValues,
|
||||
KubernetesResourcePoolIngressClassAnnotationFormValue,
|
||||
KubernetesResourcePoolIngressClassHostFormValue,
|
||||
} from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
@@ -66,6 +62,22 @@ class KubernetesResourcePoolController {
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
onChangeIngressHostname() {
|
||||
const state = this.state.duplicates.ingressHosts;
|
||||
|
||||
const hosts = _.map(this.formValues.IngressClasses, 'Host');
|
||||
const otherIngresses = _.without(this.allIngresses, ...this.ingresses);
|
||||
const allHosts = _.map(otherIngresses, 'Host');
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(hosts);
|
||||
_.forEach(hosts, (host, idx) => {
|
||||
if (_.includes(allHosts, host) && host !== undefined) {
|
||||
duplicates[idx] = host;
|
||||
}
|
||||
});
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
addAnnotation(ingressClass) {
|
||||
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
||||
@@ -73,59 +85,9 @@ class KubernetesResourcePoolController {
|
||||
|
||||
removeAnnotation(ingressClass, index) {
|
||||
ingressClass.Annotations.splice(index, 1);
|
||||
this.onChangeIngressHostname();
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region INGRESS MANAGEMENT */
|
||||
onChangeIngressHostname() {
|
||||
const state = this.state.duplicates.ingressHosts;
|
||||
const otherIngresses = _.without(this.allIngresses, ...this.ingresses);
|
||||
const allHosts = _.flatMap(otherIngresses, 'Hosts');
|
||||
|
||||
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
||||
const hostsWithoutRemoved = _.filter(hosts, { NeedsDeletion: false });
|
||||
const hostnames = _.map(hostsWithoutRemoved, 'Host');
|
||||
const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnames);
|
||||
_.forEach(hostnames, (host, idx) => {
|
||||
if (host !== undefined && _.includes(allHosts, host)) {
|
||||
formDuplicates[idx] = host;
|
||||
}
|
||||
});
|
||||
const duplicatedHostnames = Object.values(formDuplicates);
|
||||
state.hasRefs = false;
|
||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
||||
_.forEach(ic.Hosts, (hostFV) => {
|
||||
if (_.includes(duplicatedHostnames, hostFV.Host) && hostFV.NeedsDeletion === false) {
|
||||
hostFV.Duplicate = true;
|
||||
state.hasRefs = true;
|
||||
} else {
|
||||
hostFV.Duplicate = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addHostname(ingressClass) {
|
||||
ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
|
||||
removeHostname(ingressClass, index) {
|
||||
if (!ingressClass.Hosts[index].IsNew) {
|
||||
ingressClass.Hosts[index].NeedsDeletion = true;
|
||||
} else {
|
||||
ingressClass.Hosts.splice(index, 1);
|
||||
}
|
||||
this.onChangeIngressHostname();
|
||||
}
|
||||
|
||||
restoreHostname(host) {
|
||||
if (!host.IsNew) {
|
||||
host.NeedsDeletion = false;
|
||||
}
|
||||
}
|
||||
/* #endregion*/
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('resourcePool', index);
|
||||
}
|
||||
@@ -179,16 +141,16 @@ class KubernetesResourcePoolController {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* #region UPDATE NAMESPACE */
|
||||
/* #region UPDATE RESOURCE POOL */
|
||||
async updateResourcePoolAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
this.checkDefaults();
|
||||
await this.KubernetesResourcePoolService.patch(this.savedFormValues, this.formValues);
|
||||
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
|
||||
this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name);
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create namespace');
|
||||
this.Notifications.error('Failure', err, 'Unable to create resource pool');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
@@ -204,7 +166,7 @@ class KubernetesResourcePoolController {
|
||||
if (warnings.quota || warnings.ingress) {
|
||||
const messages = {
|
||||
quota:
|
||||
'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.',
|
||||
'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.',
|
||||
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 +194,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 namespace related events');
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
@@ -350,11 +312,6 @@ class KubernetesResourcePoolController {
|
||||
await this.getIngresses();
|
||||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
|
||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
||||
if (ic.Hosts.length === 0) {
|
||||
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<kubernetes-view-header title="Namespace list" state="kubernetes.resourcePools" view-ready="ctrl.state.viewReady">
|
||||
Namespaces
|
||||
<kubernetes-view-header title="Resource pool list" state="kubernetes.resourcePools" view-ready="ctrl.state.viewReady">
|
||||
Resource pools
|
||||
</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('Namespace successfully removed', pool.Namespace.Name);
|
||||
this.Notifications.success('Resource pool 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 namespace');
|
||||
this.Notifications.error('Failure', err, 'Unable to remove resource pool');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
@@ -37,7 +37,7 @@ class KubernetesResourcePoolsController {
|
||||
|
||||
removeAction(selectedItems) {
|
||||
this.ModalService.confirmDeletion(
|
||||
'Do you want to remove the selected namespace(s)? All the resources associated to the selected namespace(s) will be removed too.',
|
||||
'Do you want to remove the selected resource pool(s)? All the resources associated to the selected resource pool(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 namespaces');
|
||||
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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">Namespaces</a>
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</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">Namespaces</a> >
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</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>Namespace</td>
|
||||
<td>Resource pool</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 namespaces');
|
||||
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
@@ -55,7 +55,6 @@
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@nxmix/tokenize-ansi": "^3.0.0",
|
||||
"@uirouter/angularjs": "1.0.11",
|
||||
"angular": "1.8.0",
|
||||
"angular-clipboard": "^1.6.2",
|
||||
@@ -98,7 +97,6 @@
|
||||
"toastr": "^2.1.4",
|
||||
"ui-select": "^0.19.8",
|
||||
"uuid": "^3.3.2",
|
||||
"x256": "^0.0.2",
|
||||
"xterm": "^3.8.0",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -875,11 +875,6 @@
|
||||
"@nodelib/fs.scandir" "2.1.3"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@nxmix/tokenize-ansi@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@nxmix/tokenize-ansi/-/tokenize-ansi-3.0.0.tgz#9a7bdae1a0cf5317d5b9176038c026e374e62a58"
|
||||
integrity sha512-37QMpFIiQ6J31tavjMFCuWs3YIqXIDCuGvPiDVofFqvgXq6vM+8LqU4sqibsvb9JX/1SIeDp+SedOqpq2qc7TA==
|
||||
|
||||
"@samverschueren/stream-to-observable@^0.3.0":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
|
||||
@@ -10982,11 +10977,6 @@ ws@^6.2.1:
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
x256@^0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/x256/-/x256-0.0.2.tgz#c9af18876f7a175801d564fe70ad9e8317784934"
|
||||
integrity sha1-ya8Yh296F1gB1WT+cK2egxd4STQ=
|
||||
|
||||
xtend@^4.0.0, xtend@~4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
|
||||
Reference in New Issue
Block a user