Compare commits

..

14 Commits

Author SHA1 Message Date
snyk-bot
f30fc65dc7 fix: build/windows/Dockerfile to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE313-APKTOOLS-1533754
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1569446
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1569446
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1569448
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1569448
2021-09-07 03:06:58 +00:00
Dmitry Salakhov
e49e90f304 feat(kube): advanced apps management [EE-466] (#5446)
* feat(stack): backport changes to CE EE-1189

* feat(stack): front end backport changes to CE EE-1199 (#5455)

* feat(stack): front end backport changes to CE EE-1199

* fix k8s deploy logic

* fixed web editor confirmation message typo. EE-1501

* fix(stack): fixed issue auth detail not remembered EE-1502 (#5459)

* show status in buttons

* removed onChangeRef function.

* moved buttons in git form to its own component

* removed unused variable.

Co-authored-by: ArrisLee <arris_li@hotmail.com>

* moved formvalue to kube app component

* fix(stack): failed to pull and redeploy compose format k8s stack

* fixed form value

* fix(k8s): file content overridden when deployment failed with compose format EE-1548

* updated API response to get IsComposeFormat and show appropriate text.

* error message updates for different file type

* not display creation source for external application

* added confirmation modal to advanced app created by web editor

* stop showing confirmation modal when updating application

* disable rollback button when application type is not applicatiom form

* added analytics-on directive to pull and redeploy button

* fix(kube): don't valide resource control access for kube (#5568)

* added question marks to k8s app confirmation modal

* fix(k8s): Git authentication info not persisted

* removed unused function.

Co-authored-by: Hui <arris_li@hotmail.com>
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
Co-authored-by: Felix Han <felix.han@portainer.io>
2021-09-07 12:37:26 +12:00
itsconquest
f039292211 chore(project): replace stalebot with action [EE-1509] (#5515)
* chore(project): replace stalebot with action [EE-1509]

* add missing newline at EOF
2021-09-07 10:36:42 +12:00
Chaim Lev-Ari
3453735c8b feat(stacks): support standalone stacks on ARM (#5310) 2021-09-06 10:58:26 +03:00
LP B
582d370172 fix(k8s/namespace): missing header in namespace creation view (#5575) 2021-09-06 17:25:43 +12:00
LP B
6fea8373c6 feat(app/registries): add warning modal on registries deletion (#5396)
* feat(app/registries): add warning modal on registries deletion

feat(app/namespace): add confirmation modal on registry removal

feat(app/registry-access): add confirmation modal on namespace removal

fix(app/registry-access): change update to remove in confirmation modal

refactor(app/registries): generic message on registry access removal

* fix(app/registries): typo in warning messages
2021-09-06 17:25:02 +12:00
LP B
1b7296d5d1 fix(app/env-vars): make key regex non-greedy to match on first equal sign (#5545) 2021-09-06 17:23:51 +12:00
fhanportainer
f16fdd3ea7 fix(k8s): add tag ids to request payload for creating local k8s endpoint. EE-1454 (#5577)
* fix(k8s): add tag ids to request payload for creating local k8s endpoint.

* add https to k8s local environment url
2021-09-06 13:46:14 +12:00
Chaim Lev-Ari
4ffee27a4b feat(analytics): track existing features (#5448) [EE-1076] 2021-09-05 13:03:48 +03:00
LP B
b8e6c5ea91 fix(app/namespace): allow default-ns registries edit from namespace page (#5554) 2021-09-03 15:47:37 +02:00
Anthony Lapenna
70602cf7c8 feat(k8s): add the ability to deploy from a manifest URL (#5550) 2021-09-03 17:37:34 +12:00
zees-dev
1220ae7571 fix(kubectl/shell): zombie pods and websocket connection error bugfixes EE-1520 (#5562)
* - fixed zombie kubectl shell pod issue
- fixed bug with websocket connection error

* exec error if not websocket disconnect

* StartExecProcess updated to write error to channel
2021-09-03 13:11:11 +12:00
Anthony Lapenna
8d54b040f8 feat(kubernetes): replace advanced deployment action EE-1544 (#5534) 2021-09-02 23:30:55 +12:00
Anthony Lapenna
8d157c2c33 feat(k8s): display namespace status and terminating namespaces (#5551)
refactor(k8s): use function instead of filter
2021-09-02 23:30:27 +12:00
97 changed files with 3055 additions and 2178 deletions

54
.github/stale.yml vendored
View File

@@ -1,54 +0,0 @@
# Config for Stalebot, limited to only `issues`
only: issues
# Issues config
issues:
daysUntilStale: 60
daysUntilClose: 7
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Issues with these labels will never be considered stale
exemptLabels:
- kind/enhancement
- kind/question
- kind/style
- kind/workaround
- kind/refactor
- bug/need-confirmation
- bug/confirmed
- status/discuss
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking an issue as stale
staleLabel: status/stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been marked as stale as it has not had recent activity,
it will be closed if no further activity occurs in the next 7 days.
If you believe that it has been incorrectly labelled as stale,
leave a comment and the label will be removed.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
Since no further activity has appeared on this issue it will be closed.
If you believe that it has been incorrectly closed, leave a comment
mentioning `ametdoohan`, `balasu` or `keverv` and one of our staff will then review the issue.
Note - If it is an old bug report, make sure that it is reproduceable in the
latest version of Portainer as it may have already been fixed.

27
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue Config
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'status/stale'
exempt-all-issue-milestones: true # Do not stale issues in a milestone
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
# Pull Request Config
days-before-pr-stale: -1 # Do not stale pull request
days-before-pr-close: -1 # Do not close pull request

View File

@@ -163,5 +163,19 @@
"// @failure 500 \"Server error\"",
"// @router /{id} [get]"
]
},
"analytics": {
"prefix": "nlt",
"body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""],
"description": "analytics"
},
"analytics-if": {
"prefix": "nltf",
"body": ["analytics-if=\"$1\""],
"description": "analytics"
},
"analytics-metadata": {
"prefix": "nltm",
"body": "analytics-properties=\"{ metadata: { $1 } }\""
}
}

View File

@@ -28,7 +28,6 @@ import (
"github.com/portainer/portainer/api/kubernetes"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/libcompose"
"github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks"
@@ -86,18 +85,17 @@ func shutdownDatastore(shutdownCtx context.Context, datastore portainer.DataStor
datastore.Close()
}
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, dataStorePath, proxyManager)
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
if err != nil {
log.Printf("[INFO] [main,compose] [message: falling-back to libcompose] [error: %s]", err)
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
log.Fatalf("failed creating compose manager: %s", err)
}
return composeWrapper
}
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
func initSwarmStackManager(assetsPath string, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
@@ -446,14 +444,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %v", err)
}
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
dockerConfigPath := fileService.GetDockerConfigPath()
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)

View File

@@ -1,6 +1,7 @@
package exec
import (
"context"
"fmt"
"os"
"path"
@@ -8,7 +9,10 @@ import (
"strings"
"github.com/pkg/errors"
wrapper "github.com/portainer/docker-compose-wrapper"
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
@@ -16,33 +20,31 @@ import (
// ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct {
wrapper *wrapper.ComposeWrapper
configPath string
deployer libstack.Deployer
proxyManager *proxy.Manager
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(binaryPath string, configPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
wrap, err := wrapper.NewComposeWrapper(binaryPath)
deployer, err := compose.NewComposeDeployer(binaryPath, configPath)
if err != nil {
return nil, err
}
return &ComposeStackManager{
wrapper: wrap,
deployer: deployer,
proxyManager: proxyManager,
configPath: configPath,
}, nil
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (w *ComposeStackManager) ComposeSyntaxMaxVersion() string {
func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to featch endpoint proxy")
}
@@ -57,13 +59,13 @@ func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.End
}
filePaths := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
_, err = w.wrapper.Up(filePaths, stack.ProjectPath, url, stack.Name, envFilePath, w.configPath)
return manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
return errors.Wrap(err, "failed to deploy a stack")
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
@@ -73,8 +75,7 @@ func (w *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.E
filePaths := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
_, err = w.wrapper.Down(filePaths, stack.ProjectPath, url, stack.Name)
return err
return manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
@@ -83,17 +84,17 @@ func (w *ComposeStackManager) NormalizeStackName(name string) string {
return r.ReplaceAllString(strings.ToLower(name), "")
}
func (w *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return "", nil, nil
}
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
proxy, err := manager.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d", proxy.Port), proxy, nil
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
}
func createEnvFile(stack *portainer.Stack) (string, error) {

View File

@@ -1,6 +1,7 @@
package exec
import (
"context"
"fmt"
"log"
"os"
@@ -47,7 +48,9 @@ func Test_UpAndDown(t *testing.T) {
t.Fatalf("Failed creating manager: %s", err)
}
err = w.Up(stack, endpoint)
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
@@ -56,7 +59,7 @@ func Test_UpAndDown(t *testing.T) {
t.Fatal("container should exist")
}
err = w.Down(stack, endpoint)
err = w.Down(ctx, stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose down: %s", err)
}

View File

@@ -95,10 +95,7 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", token)
fmt.Printf("Namespace: %s\n", namespace)
if namespace != "" {
args = append(args, "--namespace", namespace)
}
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer

View File

@@ -19,7 +19,7 @@ import (
// SwarmStackManager represents a service for managing stacks.
type SwarmStackManager struct {
binaryPath string
dataPath string
configPath string
signatureService portainer.DigitalSignatureService
fileService portainer.FileService
reverseTunnelService portainer.ReverseTunnelService
@@ -27,16 +27,16 @@ type SwarmStackManager struct {
// NewSwarmStackManager initializes a new SwarmStackManager service.
// It also updates the configuration of the Docker CLI binary.
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
func NewSwarmStackManager(binaryPath, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
manager := &SwarmStackManager{
binaryPath: binaryPath,
dataPath: dataPath,
configPath: configPath,
signatureService: signatureService,
fileService: fileService,
reverseTunnelService: reverseTunnelService,
}
err := manager.updateDockerCLIConfiguration(dataPath)
err := manager.updateDockerCLIConfiguration(manager.configPath)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
@@ -57,7 +57,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
// Logout executes the docker logout command.
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@@ -65,7 +65,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
// Deploy executes the docker stack deploy command.
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
filePaths := stackutils.GetStackFilePaths(stack)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
@@ -85,7 +85,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
// Remove executes the docker stack rm command.
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@@ -109,7 +109,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
return nil
}
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string) {
// Assume Linux as a default
command := path.Join(binaryPath, "docker")
@@ -118,7 +118,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
}
args := make([]string, 0)
args = append(args, "--config", dataPath)
args = append(args, "--config", configPath)
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
@@ -145,8 +145,8 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
return command, args
}
func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string) error {
configFilePath := path.Join(dataPath, "config.json")
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
configFilePath := path.Join(configPath, "config.json")
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
if err != nil {
return err

View File

@@ -43,6 +43,8 @@ const (
BinaryStorePath = "bin"
// EdgeJobStorePath represents the subfolder where schedule files are stored.
EdgeJobStorePath = "edge_jobs"
// DockerConfigPath represents the subfolder where docker configuration is stored.
DockerConfigPath = "docker_config"
// ExtensionRegistryManagementStorePath represents the subfolder where files related to the
// registry management extension are stored.
ExtensionRegistryManagementStorePath = "extensions"
@@ -100,6 +102,11 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
return nil, err
}
err = service.createDirectoryInStore(DockerConfigPath)
if err != nil {
return nil, err
}
return service, nil
}
@@ -108,6 +115,11 @@ func (service *Service) GetBinaryFolder() string {
return path.Join(service.fileStorePath, BinaryStorePath)
}
// GetDockerConfigPath returns the full path to the docker config store on the filesystem
func (service *Service) GetDockerConfigPath() string {
return path.Join(service.fileStorePath, DockerConfigPath)
}
// RemoveDirectory removes a directory on the filesystem.
func (service *Service) RemoveDirectory(directoryPath string) error {
return os.RemoveAll(directoryPath)

View File

@@ -3,15 +3,21 @@ module github.com/portainer/portainer/api
go 1.16
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.4.16
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/boltdb/bolt v1.3.1
github.com/containerd/containerd v1.3.1 // indirect
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
github.com/docker/docker v0.0.0-00010101000000-000000000000
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
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
@@ -21,23 +27,27 @@ require (
github.com/gorilla/websocket v1.4.1
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.8
github.com/json-iterator/go v1.1.10
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/mattn/go-shellwords v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20210810234209-d01bc85eb481
github.com/portainer/libcompose v0.5.3
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gotest.tools v2.2.0+incompatible // indirect
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2

View File

@@ -1,8 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
@@ -11,40 +11,31 @@ 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/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
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=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/containerd v1.3.1 h1:LdbWxLhkAIxGO7h3mATHkyav06WuDs/yTWxIljJOTks=
github.com/containerd/containerd v1.3.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
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=
@@ -56,23 +47,16 @@ github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfD
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/cli v0.0.0-20190711175710-5b38d82aa076/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9 h1:Q6D6b2iRKhvtL3Wj9p0SyPOvUDJ1ht62mbiBoNJ3Aus=
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 h1:QeBh8wW8pIZKlXxlMOQ8hSCMdJA+2Z/bD/iDyCAS8XU=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
@@ -82,7 +66,6 @@ github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -102,20 +85,15 @@ github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbK
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=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
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/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=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -124,7 +102,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -142,8 +119,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -174,12 +149,10 @@ github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8bu
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/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/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
@@ -187,11 +160,9 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA=
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/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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -199,8 +170,6 @@ 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=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@@ -212,10 +181,9 @@ github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lN
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
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=
@@ -225,49 +193,29 @@ github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 h1:Aq8iG72akPb/kszE7ksZ5ldV+JYPYii/KZOxlpJF07s=
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c h1:iOMba/KmaXgSX5PFKu1u6s+DZXiq+EzPayawa76w6aA=
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
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/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/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=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20210810234209-d01bc85eb481 h1:5c8N9Gh21Ja/9EIpfyHFmQvTCKgOjnRhosmo0ZshkFk=
github.com/portainer/docker-compose-wrapper v0.0.0-20210810234209-d01bc85eb481/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548 h1:5I9j0e6f9KG/RV6YBKWyks8LSHheE+ltJgpMyyWYUoo=
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -285,17 +233,15 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/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=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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=
@@ -307,18 +253,15 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
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=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-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=
@@ -331,8 +274,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-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=
@@ -351,9 +292,7 @@ golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
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=
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=
@@ -363,9 +302,8 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
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=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
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=
@@ -394,7 +332,6 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=

View File

@@ -404,15 +404,5 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
}
}
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.registries, config.endpoint)
err = handler.ComposeStackManager.Up(config.stack, config.endpoint)
if err != nil {
return errors.Wrap(err, "failed to start up the stack")
}
return handler.SwarmStackManager.Logout(config.endpoint)
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries)
}

View File

@@ -1,25 +1,27 @@
package stacks
import (
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"strconv"
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
"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"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/client"
k "github.com/portainer/portainer/api/kubernetes"
)
const defaultReferenceName = "refs/heads/master"
type kubernetesStringDeploymentPayload struct {
ComposeFormat bool
Namespace string
@@ -37,6 +39,12 @@ type kubernetesGitDeploymentPayload struct {
FilePathInRepository string
}
type kubernetesManifestURLDeploymentPayload struct {
Namespace string
ComposeFormat bool
ManifestURL string
}
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
return errors.New("Invalid stack file content")
@@ -61,7 +69,14 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
return errors.New("Invalid file path in repository")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultReferenceName
payload.RepositoryReferenceName = defaultGitReferenceName
}
return nil
}
func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
return errors.New("Invalid manifest URL")
}
return nil
}
@@ -70,26 +85,39 @@ type createKubernetesStackResponse struct {
Output string `json:"Output"`
}
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload kubernetesStringDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
user, err := handler.DataStore.User().User(userID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Namespace: payload.Namespace,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
IsComposeFormat: payload.ComposeFormat,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
fileType := "Manifest"
if stack.IsComposeFormat {
fileType = "Compose"
}
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: errMsg, Err: err}
}
stack.ProjectPath = projectPath
@@ -102,6 +130,152 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
Owner: stack.CreatedBy,
Kind: "content",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
}
doCleanUp = false
resp := &createKubernetesStackResponse{
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload kubernetesGitDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
user, err := handler.DataStore.User().User(userID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: payload.FilePathInRepository,
GitConfig: &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.FilePathInRepository,
},
Namespace: payload.Namespace,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
IsComposeFormat: payload.ComposeFormat,
}
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
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
commitId, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
}
stack.GitConfig.ConfigHash = commitId
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
}
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
StackID: stackID,
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
}
doCleanUp = false
resp := &createKubernetesStackResponse{
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload kubernetesManifestURLDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
user, err := handler.DataStore.User().User(userID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
IsComposeFormat: payload.ComposeFormat,
}
var manifestContent []byte
manifestContent, err = client.Get(payload.ManifestURL, 30)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve manifest from URL", Err: err}
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, manifestContent)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
output, err := handler.deployKubernetesStack(r, endpoint, string(manifestContent), payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
StackID: stackID,
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "url",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
@@ -115,63 +289,10 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload kubernetesGitDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: payload.FilePathInRepository,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
}
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
StackID: stackID,
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
}
resp := &createKubernetesStackResponse{
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
func (handler *Handler) deployKubernetesStack(r *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string, appLabels k.KubeAppLabels) (string, error) {
func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string, appLabels k.KubeAppLabels) (string, error) {
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
@@ -189,11 +310,7 @@ func (handler *Handler) deployKubernetesStack(r *http.Request, endpoint *portain
return "", errors.Wrap(err, "failed to add application labels")
}
if !composeFormat && namespace == "default" {
namespace = ""
}
return handler.KubernetesDeployer.Deploy(r, endpoint, string(manifest), namespace)
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
}
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {

View File

@@ -1,13 +1,14 @@
package stacks
import (
"errors"
"fmt"
"net/http"
"path"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -391,7 +392,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
return errors.Wrap(err, "failed to validate user admin privileges")
}
settings := &config.endpoint.SecuritySettings
@@ -401,30 +402,15 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
path := path.Join(config.stack.ProjectPath, file)
stackContent, err := handler.FileService.GetFileContent(path)
if err != nil {
return err
return errors.WithMessage(err, "failed to get stack file content")
}
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return err
return errors.WithMessage(err, "swarm stack file content validation failed")
}
}
}
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.registries, config.endpoint)
err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
if err != nil {
return err
}
err = handler.SwarmStackManager.Logout(config.endpoint)
if err != nil {
return err
}
return nil
return handler.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune)
}

View File

@@ -110,7 +110,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
case portainer.DockerComposeStack:
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
case portainer.KubernetesStack:
return handler.createKubernetesStack(w, r, method, endpoint)
return handler.createKubernetesStack(w, r, method, endpoint, tokenData.ID)
}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
@@ -143,12 +143,14 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string, repository or file", Err: errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
switch method {
case "string":
return handler.createKubernetesStackFromFileContent(w, r, endpoint)
return handler.createKubernetesStackFromFileContent(w, r, endpoint, userID)
case "repository":
return handler.createKubernetesStackFromGitRepository(w, r, endpoint)
return handler.createKubernetesStackFromGitRepository(w, r, endpoint, userID)
case "url":
return handler.createKubernetesStackFromManifestURL(w, r, endpoint, userID)
}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string or repository", Err: errors.New(request.ErrInvalidQueryParameter)}
}

View File

@@ -1,6 +1,7 @@
package stacks
import (
"context"
"errors"
"net/http"
"strconv"
@@ -87,12 +88,14 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
}
}
@@ -174,5 +177,5 @@ func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.
return handler.SwarmStackManager.Remove(stack, endpoint)
}
return handler.ComposeStackManager.Down(stack, endpoint)
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
}

View File

@@ -66,17 +66,19 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
}
}

View File

@@ -1,9 +1,10 @@
package stacks
import (
"github.com/portainer/portainer/api/http/errors"
"net/http"
"github.com/portainer/portainer/api/http/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -60,21 +61,23 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
if resourceControl != nil {
stack.ResourceControl = resourceControl
if resourceControl != nil {
stack.ResourceControl = resourceControl
}
}
}

View File

@@ -78,22 +78,24 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1

View File

@@ -1,6 +1,7 @@
package stacks
import (
"context"
"errors"
"fmt"
"net/http"
@@ -68,17 +69,19 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
}
if stack.Status == portainer.StackStatusActive {
@@ -118,7 +121,7 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
switch stack.Type {
case portainer.DockerComposeStack:
return handler.ComposeStackManager.Up(stack, endpoint)
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint)
case portainer.DockerSwarmStack:
return handler.SwarmStackManager.Deploy(stack, true, endpoint)
}

View File

@@ -1,6 +1,7 @@
package stacks
import (
"context"
"errors"
"net/http"
@@ -57,17 +58,19 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
}
if stack.Status == portainer.StackStatusInactive {
@@ -102,7 +105,7 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
func (handler *Handler) stopStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
switch stack.Type {
case portainer.DockerComposeStack:
return handler.ComposeStackManager.Down(stack, endpoint)
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
case portainer.DockerSwarmStack:
return handler.SwarmStackManager.Remove(stack, endpoint)
}

View File

@@ -1,11 +1,12 @@
package stacks
import (
"errors"
"net/http"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -72,9 +73,9 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
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{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
@@ -82,7 +83,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
@@ -90,32 +91,36 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
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{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
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{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
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{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
//only check resource control when it is a DockerSwarmStack or a DockerComposeStack
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
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}
}
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}
}
}
updateError := handler.updateAndDeployStack(r, stack, endpoint)
@@ -123,9 +128,17 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
return updateError
}
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
}
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
stack.Status = portainer.StackStatusActive
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
@@ -139,15 +152,20 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
func (handler *Handler) updateAndDeployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.Type == portainer.DockerSwarmStack {
return handler.updateSwarmStack(r, stack, endpoint)
} else if stack.Type == portainer.DockerComposeStack {
return handler.updateComposeStack(r, stack, endpoint)
} else if stack.Type == portainer.KubernetesStack {
return handler.updateKubernetesStack(r, stack, endpoint)
} else {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unsupported stack", Err: errors.Errorf("unsupported stack type: %v", stack.Type)}
}
return handler.updateComposeStack(r, stack, endpoint)
}
func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload updateComposeStackPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stack.Env = payload.Env
@@ -155,7 +173,7 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
}
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
@@ -163,13 +181,9 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
return nil
@@ -179,7 +193,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
var payload updateSwarmStackPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stack.Env = payload.Env
@@ -187,7 +201,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
}
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune)
@@ -195,13 +209,9 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
return nil

View File

@@ -37,8 +37,8 @@ func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
}
// @id StackUpdateGit
// @summary Redeploy a stack
// @description Pull and redeploy a stack via Git
// @summary Update a stack's Git configs
// @description Update the Git settings in a stack, e.g., RepositoryReferenceName and AutoUpdate
// @description **Access policy**: restricted
// @tags stacks
// @security jwt
@@ -46,7 +46,7 @@ func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
// @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 updateStackGitPayload true "Git configs for pull and redeploy a stack"
// @param body body stackGitUpdatePayload true "Git configs for pull and redeploy a stack"
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
@@ -98,22 +98,24 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
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}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
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}
}
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}
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}
}
}
//stop the autoupdate job if there is any

View File

@@ -2,11 +2,14 @@ package stacks
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"path/filepath"
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -16,6 +19,7 @@ import (
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
k "github.com/portainer/portainer/api/kubernetes"
)
type stackGitRedployPayload struct {
@@ -30,11 +34,26 @@ func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
return nil
}
// PUT request on /api/stacks/:id/git?endpointId=<endpointId>
// @id StackGitRedeploy
// @summary Redeploy a stack
// @description Pull and redeploy a stack via Git
// @description **Access policy**: restricted
// @tags stacks
// @security jwt
// @accept json
// @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 stackGitRedployPayload true "Git configs for pull and redeploy a stack"
// @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/redeploy [put]
func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
@@ -75,22 +94,26 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
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}
//only check resource control when it is a DockerSwarmStack or a DockerComposeStack
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
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}
}
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}
}
}
var payload stackGitRedployPayload
@@ -140,9 +163,23 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
return httpErr
}
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable get latest commit id", Err: errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID)}
}
stack.GitConfig.ConfigHash = newHash
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
}
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
stack.Status = portainer.StackStatusActive
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 &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: errors.Wrap(err, "failed to update the stack")}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
@@ -154,37 +191,48 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
}
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.Type == portainer.DockerSwarmStack {
switch stack.Type {
case portainer.DockerSwarmStack:
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if httpErr != nil {
return httpErr
}
err := handler.deploySwarmStack(config)
if err != nil {
if err := handler.deploySwarmStack(config); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
case portainer.DockerComposeStack:
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
return nil
if err := handler.deployComposeStack(config); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
case portainer.KubernetesStack:
if stack.Namespace == "" {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
}
content, err := ioutil.ReadFile(filepath.Join(stack.ProjectPath, stack.GitConfig.ConfigFilePath))
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to read deployment.yml manifest file", Err: errors.Wrap(err, "failed to read manifest file")}
}
_, err = handler.deployKubernetesStack(r, endpoint, string(content), stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
StackID: int(stack.ID),
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to redeploy Kubernetes stack", Err: errors.WithMessage(err, "failed to deploy kube application")}
}
default:
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unsupported stack", Err: errors.Errorf("unsupported stack type: %v", stack.Type)}
}
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
err := handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
return nil
}

View File

@@ -0,0 +1,97 @@
package stacks
import (
"fmt"
"net/http"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types"
k "github.com/portainer/portainer/api/kubernetes"
)
type kubernetesFileStackUpdatePayload struct {
StackFileContent string
}
type kubernetesGitStackUpdatePayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
}
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
return errors.New("Invalid stack file content")
}
return nil
}
func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
return nil
}
func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.GitConfig != nil {
var payload kubernetesGitStackUpdatePayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
password = stack.GitConfig.Authentication.Password
}
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: password,
}
} else {
stack.GitConfig.Authentication = nil
}
return nil
}
var payload kubernetesFileStackUpdatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
_, err = handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
StackID: int(stack.ID),
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "content",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack via file content", Err: err}
}
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
fileType := "Manifest"
if stack.IsComposeFormat {
fileType = "Compose"
}
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: errMsg, Err: err}
}
return nil
}

View File

@@ -135,21 +135,25 @@ func (handler *Handler) hijackPodExecStartOperation(
stdoutReader, stdoutWriter := io.Pipe()
defer stdoutWriter.Close()
// errorChan is used to propagate errors from the go routines to the caller.
errorChan := make(chan error, 1)
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
err = cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
}
// StartExecProcess is a blocking operation which streams IO to/from pod;
// this must execute in asynchronously, since the websocketConn could return errors (e.g. client disconnects) before
// the blocking operation is completed.
go cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter, errorChan)
err = <-errorChan
// websocket client successfully disconnected
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
log.Printf("websocket error: %s \n", err.Error())
return nil
}
return nil
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
}
func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) {

View File

@@ -60,7 +60,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
return nil, err
}
return proxyServer, err
return proxyServer, nil
}
func (proxy *ProxyServer) start() error {

View File

@@ -4,7 +4,7 @@ import (
"errors"
"io"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
@@ -15,10 +15,12 @@ import (
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
// to the stdout parameter.
// This function only works against a local endpoint using an in-cluster config with the user's SA token.
func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
// This is a blocking operation.
func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error) {
config, err := rest.InClusterConfig()
if err != nil {
return err
errChan <- err
return
}
if !useAdminToken {
@@ -44,7 +46,8 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
return err
errChan <- err
return
}
err = exec.Stream(remotecommand.StreamOptions{
@@ -54,9 +57,7 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
})
if err != nil {
if _, ok := err.(utilexec.ExitError); !ok {
return errors.New("unable to start exec process")
errChan <- errors.New("unable to start exec process")
}
}
return nil
}

View File

@@ -1,148 +0,0 @@
package libcompose
import (
"context"
"fmt"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/portainer/libcompose/config"
"github.com/portainer/libcompose/docker"
"github.com/portainer/libcompose/docker/client"
"github.com/portainer/libcompose/docker/ctx"
"github.com/portainer/libcompose/lookup"
"github.com/portainer/libcompose/project"
"github.com/portainer/libcompose/project/options"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/stackutils"
)
const (
dockerClientVersion = "1.24"
composeSyntaxMaxVersion = "2"
)
// ComposeStackManager represents a service for managing compose stacks.
type ComposeStackManager struct {
dataPath string
reverseTunnelService portainer.ReverseTunnelService
}
// NewComposeStackManager initializes a new ComposeStackManager service.
func NewComposeStackManager(dataPath string, reverseTunnelService portainer.ReverseTunnelService) *ComposeStackManager {
return &ComposeStackManager{
dataPath: dataPath,
reverseTunnelService: reverseTunnelService,
}
}
func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (client.Factory, error) {
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
}
clientOpts := client.Options{
Host: endpointURL,
APIVersion: dockerClientVersion,
}
if endpoint.TLSConfig.TLS {
clientOpts.TLS = endpoint.TLSConfig.TLS
clientOpts.TLSVerify = !endpoint.TLSConfig.TLSSkipVerify
clientOpts.TLSCAFile = endpoint.TLSConfig.TLSCACertPath
clientOpts.TLSCertFile = endpoint.TLSConfig.TLSCertPath
clientOpts.TLSKeyFile = endpoint.TLSConfig.TLSKeyPath
}
return client.NewDefaultFactory(clientOpts)
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return composeSyntaxMaxVersion
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
// this is coming from libcompose
// https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
// Up will deploy a compose stack (equivalent of docker-compose up)
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
clientFactory, err := manager.createClient(endpoint)
if err != nil {
return err
}
env := make(map[string]string)
for _, envvar := range stack.Env {
env[envvar.Name] = envvar.Value
}
filePaths := stackutils.GetStackFilePaths(stack)
proj, err := docker.NewProject(&ctx.Context{
ConfigDir: manager.dataPath,
Context: project.Context{
ComposeFiles: filePaths,
EnvironmentLookup: &lookup.ComposableEnvLookup{
Lookups: []config.EnvironmentLookup{
&lookup.EnvfileLookup{
Path: filepath.Join(stack.ProjectPath, ".env"),
},
&lookup.MapLookup{
Vars: env,
},
},
},
ProjectName: stack.Name,
},
ClientFactory: clientFactory,
}, nil)
if err != nil {
return err
}
return proj.Up(context.Background(), options.Up{})
}
// Down will shutdown a compose stack (equivalent of docker-compose down)
func (manager *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
clientFactory, err := manager.createClient(endpoint)
if err != nil {
return err
}
var composeFiles []string
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
composeFiles = append(composeFiles, path.Join(stack.ProjectPath, file))
}
proj, err := docker.NewProject(&ctx.Context{
Context: project.Context{
ComposeFiles: composeFiles,
ProjectName: stack.Name,
},
ClientFactory: clientFactory,
}, nil)
if err != nil {
return err
}
return proj.Down(context.Background(), options.Down{RemoveVolume: false, RemoveOrphans: true})
}
func stackFilePaths(stack *portainer.Stack) []string {
var filePaths []string
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
filePaths = append(filePaths, path.Join(stack.ProjectPath, file))
}
return filePaths
}

View File

@@ -759,6 +759,10 @@ type (
AutoUpdate *StackAutoUpdate `json:"AutoUpdate"`
// The git config of this stack
GitConfig *gittypes.RepoConfig
// Kubernetes namespace if stack is a kube application
Namespace string `example:"default"`
// IsComposeFormat indicates if the Kubernetes stack is created from a Docker Compose file
IsComposeFormat bool `example:"false"`
}
//StackAutoUpdate represents the git auto sync config for stack deployment
@@ -1050,8 +1054,8 @@ type (
ComposeStackManager interface {
ComposeSyntaxMaxVersion() string
NormalizeStackName(name string) string
Up(stack *Stack, endpoint *Endpoint) error
Down(stack *Stack, endpoint *Endpoint) error
Up(ctx context.Context, stack *Stack, endpoint *Endpoint) error
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
}
// CryptoService represents a service for encrypting/hashing data
@@ -1175,6 +1179,7 @@ type (
// FileService represents a service for managing files
FileService interface {
GetDockerConfigPath() string
GetFileContent(filePath string) ([]byte, error)
Rename(oldPath, newPath string) error
RemoveDirectory(directoryPath string) error
@@ -1228,7 +1233,7 @@ type (
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
GetServiceAccountBearerToken(userID int) (string, error)
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error)
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
GetNodesLimits() (K8sNodesLimits, error)
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)

View File

@@ -1,6 +1,7 @@
package stacks
import (
"context"
"sync"
portainer "github.com/portainer/portainer/api"
@@ -42,5 +43,5 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
d.swarmStackManager.Login(registries, endpoint)
defer d.swarmStackManager.Logout(endpoint)
return d.composeStackManager.Up(stack, endpoint)
return d.composeStackManager.Up(context.TODO(), stack, endpoint)
}

View File

@@ -1,4 +1,5 @@
import angular from 'angular';
import _ from 'lodash-es';
const basePath = 'http://portainer-ce.app';
@@ -131,7 +132,8 @@ function config($analyticsProvider, $windowProvider) {
let metadataString = '';
if (metadata) {
metadataString = JSON.stringify(metadata).toLowerCase();
const kebabCasedMetadata = Object.fromEntries(Object.entries(metadata).map(([key, value]) => [_.kebabCase(key), value]));
metadataString = JSON.stringify(kebabCasedMetadata).toLowerCase();
}
push([

View File

@@ -199,6 +199,10 @@
ng-click="$ctrl.createStack()"
button-spinner="$ctrl.state.actionInProgress"
data-cy="edgeStackCreate-createStackButton"
analytics-on
analytics-event="edge-stack-creation"
analytics-category="edge"
analytics-properties="$ctrl.buildAnalyticsProperties()"
>
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>

View File

@@ -43,6 +43,30 @@ export class CreateEdgeStackViewController {
this.onChangeFormValues = this.onChangeFormValues.bind(this);
}
buildAnalyticsProperties() {
const format = 'compose';
const metadata = { type: methodLabel(this.state.Method), format };
if (metadata.type === 'template') {
metadata.templateName = this.selectedTemplate.title;
}
return { metadata };
function methodLabel(method) {
switch (method) {
case 'editor':
return 'web-editor';
case 'repository':
return 'git';
case 'upload':
return 'file-upload';
case 'template':
return 'template';
}
}
}
async uiCanExit() {
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();

View File

@@ -73,7 +73,10 @@
<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.applications.new" data-cy="k8sApp-addApplicationButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application with form
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sApp-deployFromManifestButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
</button>
</div>
<div class="searchBar">

View File

@@ -66,8 +66,11 @@
>
<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.configurations.new" data-cy="k8sConfig-addConfigButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigWithFormButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration with form
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sConfig-deployFromManifestButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
</button>
</div>
<div class="searchBar">

View File

@@ -60,8 +60,11 @@
>
<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" data-cy="k8sNamespace-addNamespaceButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceWithFormButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace with form
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sNamespace-deployFromManifestButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
</button>
</div>
<div class="searchBar">
@@ -92,6 +95,13 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Namespace.Status')">
Status
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Quota')">
Quota
@@ -124,6 +134,9 @@
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace.Name })">{{ item.Namespace.Name }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
</td>
<td>
<span class="label label-{{ $ctrl.namespaceStatusColor(item.Namespace.Status) }}">{{ item.Namespace.Status }}</span>
</td>
<td> <i class="fa {{ item.Quota ? 'fa-toggle-on' : 'fa-toggle-off' }}" aria-hidden="true" style="margin-right: 2px;"></i> {{ item.Quota ? 'Yes' : 'No' }} </td>
<td>{{ item.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
<td ng-if="$ctrl.isAdmin">

View File

@@ -38,6 +38,17 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin);
};
this.namespaceStatusColor = function (status) {
switch (status.toLowerCase()) {
case 'active':
return 'success';
case 'terminating':
return 'danger';
default:
return 'primary';
}
};
/**
* Do not allow system namespaces to be selected
*/

View File

@@ -54,6 +54,9 @@
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<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.deploy" data-cy="k8sVolume-deployFromManifestButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>

View File

@@ -1,4 +1,13 @@
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.connectConsole()" ng-disabled="$ctrl.state.shell.connected" data-cy="k8sSidebar-shellButton">
<button
type="button"
class="btn btn-xs btn-primary"
ng-click="$ctrl.connectConsole()"
ng-disabled="$ctrl.state.shell.connected"
data-cy="k8sSidebar-shellButton"
analytics-on
analytics-category="kubernetes"
analytics-event="kubernetes-kubectl-shell"
>
<i class="fa fa-terminal space-right"></i> kubectl shell
</button>

View File

@@ -15,6 +15,7 @@ import {
KubernetesPortainerApplicationOwnerLabel,
KubernetesPortainerApplicationStackNameLabel,
KubernetesPortainerApplicationStackIdLabel,
KubernetesPortainerApplicationKindLabel,
} from 'Kubernetes/models/application/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
@@ -60,6 +61,7 @@ class KubernetesApplicationConverter {
const { labels } = data.metadata;
res.StackId = labels[KubernetesPortainerApplicationStackIdLabel] ? parseInt(labels[KubernetesPortainerApplicationStackIdLabel], 10) : null;
res.StackName = labels[KubernetesPortainerApplicationStackNameLabel] || '';
res.ApplicationKind = labels[KubernetesPortainerApplicationKindLabel] || '';
res.ApplicationOwner = labels[KubernetesPortainerApplicationOwnerLabel] || '';
res.ApplicationName = labels[KubernetesPortainerApplicationNameLabel] || res.Name;
}

View File

@@ -40,8 +40,11 @@ export const KubernetesApplicationQuotaDefaults = {
};
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack';
export const KubernetesPortainerApplicationStackIdLabel = 'io.portainer.kubernetes.application.stackid';
export const KubernetesPortainerApplicationKindLabel = 'io.portainer.kubernetes.application.kind';
export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name';
export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner';

View File

@@ -7,6 +7,8 @@ const _KubernetesApplication = Object.freeze({
Id: '',
Name: '',
StackName: '',
StackId: '',
ApplicationKind: '',
ApplicationOwner: '',
ApplicationName: '',
ResourcePool: '',
@@ -91,3 +93,9 @@ export class KubernetesApplicationPort {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPort)));
}
}
export const KubernetesDeploymentTypes = Object.freeze({
GIT: 'git',
CONTENT: 'content',
APPLICATION_FORM: 'application form',
});

View File

@@ -7,9 +7,11 @@ export const KubernetesDeployBuildMethods = Object.freeze({
GIT: 1,
WEB_EDITOR: 2,
CUSTOM_TEMPLATE: 3,
URL: 4,
});
export const KubernetesDeployRequestMethods = Object.freeze({
REPOSITORY: 'repository',
STRING: 'string',
URL: 'url',
});

View File

@@ -2,9 +2,10 @@ import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
export default class KubernetesRegistryAccessController {
/* @ngInject */
constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService) {
constructor($async, $state, ModalService, EndpointService, Notifications, KubernetesResourcePoolService) {
this.$async = $async;
this.$state = $state;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.EndpointService = EndpointService;
@@ -26,8 +27,15 @@ export default class KubernetesRegistryAccessController {
handleRemove(namespaces) {
const removeNamespaces = namespaces.map(({ value }) => value);
const nsToUpdate = this.savedResourcePools.map(({ value }) => value).filter((value) => !removeNamespaces.includes(value));
return this.updateNamespaces(this.savedResourcePools.map(({ value }) => value).filter((value) => !removeNamespaces.includes(value)));
const displayedMessage =
'This registry might be used by one or more applications inside this environment. Removing the registry access could lead to a service interruption for these applications.<br/><br/>Do you wish to continue?';
this.ModalService.confirmDeletion(displayedMessage, (confirmed) => {
if (confirmed) {
return this.updateNamespaces(nsToUpdate);
}
});
}
updateNamespaces(namespaces) {

View File

@@ -41,14 +41,11 @@ class KubernetesNamespaceService {
const data = await this.KubernetesNamespaces().get().$promise;
const promises = _.map(data.items, (item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise);
const namespaces = await $allSettled(promises);
const visibleNamespaces = _.map(namespaces.fulfilled, (item) => {
if (item.status.phase !== 'Terminating') {
return KubernetesNamespaceConverter.apiToNamespace(item);
}
const allNamespaces = _.map(namespaces.fulfilled, (item) => {
return KubernetesNamespaceConverter.apiToNamespace(item);
});
const res = _.without(visibleNamespaces, undefined);
updateNamespaces(res);
return res;
updateNamespaces(allNamespaces);
return allNamespaces;
} catch (err) {
throw new PortainerError('Unable to retrieve namespaces', err);
}

View File

@@ -1,14 +0,0 @@
<information-panel title-text="Advanced deployment">
<span class="small">
<p class="text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Advanced deployment allows you to deploy any Kubernetes manifest inside your cluster.
</p>
<p>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sApp-advancedDeployButton">
<i class="fa fa-file-code space-right" aria-hidden="true"></i>
Advanced deployment
</button>
</p>
</span>
</information-panel>

View File

@@ -5,8 +5,6 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
<div class="row">
<div class="col-sm-12" data-cy="k8sApp-appList">
<rd-widget>

View File

@@ -1,5 +1,3 @@
require('../../templates/advancedDeploymentPanel.html');
import angular from 'angular';
import _ from 'lodash-es';
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import {
KubernetesApplicationQuotaDefaults,
KubernetesApplicationTypes,
KubernetesApplicationPlacementTypes,
KubernetesDeploymentTypes,
} from 'Kubernetes/models/application/models';
import {
KubernetesApplicationConfigurationFormValue,
@@ -50,6 +51,7 @@ class KubernetesCreateApplicationController {
KubernetesPersistentVolumeClaimService,
KubernetesVolumeService,
RegistryService,
StackService,
KubernetesNodesLimitsService
) {
this.$async = $async;
@@ -66,6 +68,7 @@ class KubernetesCreateApplicationController {
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.RegistryService = RegistryService;
this.StackService = StackService;
this.KubernetesNodesLimitsService = KubernetesNodesLimitsService;
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
@@ -75,8 +78,11 @@ class KubernetesCreateApplicationController {
this.ApplicationTypes = KubernetesApplicationTypes;
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
this.ServiceTypes = KubernetesServiceTypes;
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
this.state = {
appType: this.KubernetesDeploymentTypes.APPLICATION_FORM,
updateWebEditorInProgress: false,
actionInProgress: false,
useLoadBalancer: false,
useServerMetrics: false,
@@ -133,9 +139,51 @@ class KubernetesCreateApplicationController {
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
this.setPullImageValidity = this.setPullImageValidity.bind(this);
this.onChangeFileContent = this.onChangeFileContent.bind(this);
}
/* #endregion */
onChangeFileContent(value) {
if (this.stackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== value.replace(/(\r\n|\n|\r)/gm, '')) {
this.state.isEditorDirty = true;
this.stackFileContent = value;
}
}
async updateApplicationViaWebEditor() {
return this.$async(async () => {
try {
const confirmed = await this.ModalService.confirmAsync({
title: 'Are you sure?',
message: 'Any changes to this application will be overriden and may cause a service interruption. Do you wish to continue?',
buttons: {
confirm: {
label: 'Update',
className: 'btn-warning',
},
},
});
if (!confirmed) {
return;
}
this.state.updateWebEditorInProgress = true;
await this.StackService.updateKubeStack({ EndpointId: this.endpoint.Id, Id: this.application.StackId }, this.stackFileContent, null);
this.state.isEditorDirty = false;
await this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Failed redeploying application');
} finally {
this.state.updateWebEditorInProgress = false;
}
});
}
async uiCanExit() {
if (this.stackFileContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
setPullImageValidity(validity) {
this.state.pullImageValidity = validity;
}
@@ -1029,6 +1077,17 @@ class KubernetesCreateApplicationController {
this.nodesLabels,
this.ingresses
);
if (this.application.ApplicationKind) {
this.state.appType = this.KubernetesDeploymentTypes[this.application.ApplicationKind.toUpperCase()];
if (this.application.StackId) {
this.stack = await this.StackService.stack(this.application.StackId);
if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.CONTENT) {
this.stackFileContent = await this.StackService.getStackFile(this.application.StackId);
}
}
}
this.formValues.OriginalIngresses = this.ingresses;
this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel);
this.savedFormValues = angular.copy(this.formValues);

View File

@@ -67,7 +67,10 @@
<td>Creation</td>
<td>
<span ng-if="ctrl.application.ApplicationOwner" style="margin-right: 5px;"> <i class="fas fa-user"></i> {{ ctrl.application.ApplicationOwner }} </span>
<span> <i class="fas fa-clock"></i> {{ ctrl.application.CreationDate | getisodate }} </span>
<span> <i class="fas fa-clock"></i> {{ ctrl.application.CreationDate | getisodate }}</span>
<span ng-if="ctrl.application.ApplicationOwner">
<i class="fa fa-file-code space-left space-right" aria-hidden="true"></i> Deployed from {{ ctrl.state.appType }}</span
>
</td>
</tr>
<tr>
@@ -210,7 +213,7 @@
class="btn btn-sm btn-primary"
style="margin-left: 0;"
ng-click="ctrl.rollbackApplication()"
ng-disabled="ctrl.application.Revisions.length < 2"
ng-disabled="ctrl.application.Revisions.length < 2 || ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
>
<i class="fas fa-history space-right" aria-hidden="true"></i>Rollback to previous configuration
</button>

View File

@@ -1,7 +1,12 @@
import angular from 'angular';
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import {
KubernetesApplicationDataAccessPolicies,
KubernetesApplicationDeploymentTypes,
KubernetesApplicationTypes,
KubernetesDeploymentTypes,
} from 'Kubernetes/models/application/models';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
@@ -127,6 +132,7 @@ class KubernetesApplicationController {
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
this.EndpointProvider = EndpointProvider;
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
this.KubernetesServiceTypes = KubernetesServiceTypes;
@@ -137,6 +143,7 @@ class KubernetesApplicationController {
this.getApplicationAsync = this.getApplicationAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
this.getEventsAsync = this.getEventsAsync.bind(this);
this.updateApplicationKindText = this.updateApplicationKindText.bind(this);
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
this.redeployApplicationAsync = this.redeployApplicationAsync.bind(this);
this.rollbackApplicationAsync = this.rollbackApplicationAsync.bind(this);
@@ -262,6 +269,14 @@ class KubernetesApplicationController {
return this.$async(this.updateApplicationAsync);
}
updateApplicationKindText() {
if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.GIT) {
this.state.appType = `git repository`;
} else if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.CONTENT) {
this.state.appType = `web editor`;
}
}
/**
* EVENTS
*/
@@ -343,6 +358,7 @@ class KubernetesApplicationController {
namespace: this.$transition$.params().namespace,
name: this.$transition$.params().name,
},
appType: this.KubernetesDeploymentTypes.APPLICATION_FORM,
eventWarningCount: 0,
placementWarning: false,
expandedNote: false,
@@ -359,6 +375,7 @@ class KubernetesApplicationController {
await this.getApplication();
await this.getEvents();
this.updateApplicationKindText();
this.state.viewReady = true;
}

View File

@@ -5,8 +5,6 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
<div class="row">
<div class="col-sm-12">
<kubernetes-configurations-datatable

View File

@@ -1,5 +1,3 @@
require('../../templates/advancedDeploymentPanel.html');
import angular from 'angular';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';

View File

@@ -299,6 +299,11 @@
ng-click="ctrl.configure()"
ng-disabled="ctrl.state.actionInProgress || !kubernetesClusterSetupForm.$valid || !ctrl.hasValidStorageConfiguration()"
button-spinner="ctrl.state.actionInProgress"
analytics-on
analytics-if="ctrl.restrictDefaultToggledOn()"
analytics-category="kubernetes"
analytics-event="kubernetes-configure"
analytics-properties="{ metadata: { restrictAccessToDefaultNamespace: ctrl.formValues.RestrictDefaultNamespace } }"
>
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>

View File

@@ -237,6 +237,10 @@ class KubernetesConfigureController {
}
/* #endregion */
restrictDefaultToggledOn() {
return this.formValues.RestrictDefaultNamespace && !this.oldFormValues.RestrictDefaultNamespace;
}
/* #region ON INIT */
async onInit() {
this.state = {
@@ -287,6 +291,8 @@ class KubernetesConfigureController {
ic.NeedsDeletion = false;
return ic;
});
this.oldFormValues = Object.assign({}, this.formValues);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration');
} finally {

View File

@@ -83,7 +83,7 @@
placeholder="# Define or paste the content of your manifest file here"
>
<editor-description>
<span class="col-sm-12" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE">
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that
@@ -95,7 +95,7 @@
</p>
</span>
<span
class="col-sm-12"
class="col-sm-12 text-muted small"
ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES && ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR"
>
<p>
@@ -111,6 +111,33 @@
</web-editor-form>
<!-- !editor -->
<!-- url -->
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.URL">
<div class="col-sm-12 form-section-title">
URL
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Indicate the URL to the manifest.
</span>
</div>
<div class="form-group">
<label for="manifest_url" class="col-sm-1 control-label text-left">URL</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
ng-model="ctrl.formValues.ManifestURL"
id="manifest_url"
placeholder="https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/controllers/nginx-deployment.yaml"
data-cy="k8sAppDeploy-urlFileUrl"
/>
</div>
</div>
</div>
<!-- !url -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
@@ -124,6 +151,10 @@
ng-click="ctrl.deploy()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppDeploy-deployButton"
analytics-on
analytics-category="kubernetes"
analytics-event="kubernetes-application-advanced-deployment"
analytics-properties="ctrl.buildAnalyticsProperties()"
>
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>

View File

@@ -1,15 +1,17 @@
import angular from 'angular';
import _ from 'lodash-es';
import stripAnsi from 'strip-ansi';
import PortainerError from 'Portainer/error';
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy';
import { buildOption } from '@/portainer/components/box-selector';
class KubernetesDeployController {
/* @ngInject */
constructor($async, $state, $window, CustomTemplateService, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
constructor($async, $state, $window, Authentication, CustomTemplateService, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
this.$async = $async;
this.$state = $state;
this.$window = $window;
this.Authentication = Authentication;
this.CustomTemplateService = CustomTemplateService;
this.ModalService = ModalService;
this.Notifications = Notifications;
@@ -25,6 +27,7 @@ class KubernetesDeployController {
this.methodOptions = [
buildOption('method_repo', 'fab fa-github', 'Git Repository', 'Use a git repository', KubernetesDeployBuildMethods.GIT),
buildOption('method_editor', 'fa fa-edit', 'Web editor', 'Use our Web editor', KubernetesDeployBuildMethods.WEB_EDITOR),
buildOption('method_url', 'fa fa-globe', 'URL', 'Specify a URL to a file', KubernetesDeployBuildMethods.URL),
buildOption('method_template', 'fa fa-rocket', 'Custom Template', 'Use a custom template', KubernetesDeployBuildMethods.CUSTOM_TEMPLATE),
];
@@ -50,6 +53,47 @@ class KubernetesDeployController {
this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.onRepoUrlChange = this.onRepoUrlChange.bind(this);
this.onRepoRefChange = this.onRepoRefChange.bind(this);
this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this);
}
buildAnalyticsProperties() {
const metadata = {
type: buildLabel(this.state.BuildMethod),
format: formatLabel(this.state.DeployType),
role: roleLabel(this.Authentication.isAdmin()),
};
if (this.state.BuildMethod === KubernetesDeployBuildMethods.GIT) {
metadata.auth = this.formValues.RepositoryAuthentication;
}
return { metadata };
function roleLabel(isAdmin) {
if (isAdmin) {
return 'admin';
}
return 'standard';
}
function buildLabel(buildMethod) {
switch (buildMethod) {
case KubernetesDeployBuildMethods.GIT:
return 'git';
case KubernetesDeployBuildMethods.WEB_EDITOR:
return 'web-editor';
}
}
function formatLabel(format) {
switch (format) {
case KubernetesDeployManifestTypes.COMPOSE:
return 'compose';
case KubernetesDeployManifestTypes.KUBERNETES:
return 'manifest';
}
}
}
disableDeploy() {
@@ -57,10 +101,13 @@ class KubernetesDeployController {
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
(!this.formValues.RepositoryURL ||
!this.formValues.FilePathInRepository ||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword)));
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent);
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) &&
_.isEmpty(this.formValues.Namespace);
const isWebEditorInvalid =
this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace);
const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL);
return isGitFormInvalid || isWebEditorInvalid || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress;
return isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress;
}
onChangeFormValues(values) {
@@ -111,16 +158,25 @@ class KubernetesDeployController {
this.state.actionInProgress = true;
try {
let method = KubernetesDeployRequestMethods.STRING;
let method;
let composeFormat = this.state.DeployType === this.ManifestDeployTypes.COMPOSE;
switch (this.state.BuildMethod) {
case KubernetesDeployBuildMethods.GIT:
case this.BuildMethods.GIT:
method = KubernetesDeployRequestMethods.REPOSITORY;
break;
case this.BuildMethods.WEB_EDITOR:
method = KubernetesDeployRequestMethods.STRING;
break;
case KubernetesDeployBuildMethods.CUSTOM_TEMPLATE:
method = KubernetesDeployRequestMethods.STRING;
composeFormat = false;
break;
case this.BuildMethods.URL:
method = KubernetesDeployRequestMethods.URL;
break;
default:
throw new PortainerError('Unable to determine build method');
}
const payload = {
@@ -137,8 +193,10 @@ class KubernetesDeployController {
payload.RepositoryPassword = this.formValues.RepositoryPassword;
}
payload.FilePathInRepository = this.formValues.FilePathInRepository;
} else {
} else if (method === KubernetesDeployRequestMethods.STRING) {
payload.StackFileContent = this.formValues.EditorContent;
} else {
payload.ManifestURL = this.formValues.ManifestURL;
}
await this.StackService.kubernetesDeploy(this.endpointId, method, payload);

View File

@@ -1,4 +1,4 @@
<kubernetes-view-header title="Create a namespace" state="kubernetes.resourcePools.new" view-ready="ctrl.state.viewReady">
<kubernetes-view-header title="Create a namespace" state="kubernetes.resourcePools.new" view-ready="$ctrl.state.viewReady">
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt; Create a namespace
</kubernetes-view-header>

View File

@@ -317,16 +317,16 @@
Registries
</div>
<div class="form-group" ng-if="!ctrl.isAdmin">
<div class="form-group" ng-if="!ctrl.isAdmin || ctrl.isSystem">
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
Selected registries
</label>
<div class="col-sm-9 col-lg-4">
{{ ctrl.selectedRegistries }}
{{ ctrl.selectedRegistries ? ctrl.selectedRegistries : 'None' }}
</div>
</div>
<div ng-if="ctrl.isAdmin">
<div ng-if="ctrl.isAdmin && !ctrl.isSystem">
<div class="form-group">
<div class="col-sm-12 small text-muted">
<p>
@@ -405,14 +405,14 @@
<!-- !summary -->
<!-- actions -->
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="col-sm-12 form-section-title">
<div ng-if="ctrl.isAdmin" class="col-sm-12 form-section-title">
Actions
</div>
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="form-group">
<div ng-if="ctrl.isAdmin" class="form-group">
<div class="col-sm-12">
<button
type="button"
ng-if="ctrl.isEditable"
ng-if="!ctrl.isSystem"
class="btn btn-primary btn-sm"
ng-disabled="!resourcePoolEditForm.$valid || ctrl.isUpdateButtonDisabled()"
ng-click="ctrl.updateResourcePool()"
@@ -421,7 +421,13 @@
<span ng-hide="ctrl.state.actionInProgress" data-cy="k8sNamespaceEdit-updateNamespaceButton">Update namespace</span>
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
</button>
<button type="button" class="btn btn-primary btn-sm" ng-click="ctrl.markUnmarkAsSystem()" button-spinner="ctrl.state.actionInProgress">
<button
ng-if="!ctrl.isDefaultNamespace"
type="button"
class="btn btn-primary btn-sm"
ng-click="ctrl.markUnmarkAsSystem()"
button-spinner="ctrl.state.actionInProgress"
>
<span ng-if="ctrl.isSystem">Unmark as system</span>
<span ng-if="!ctrl.isSystem">Mark as system</span>
</button>

View File

@@ -185,20 +185,26 @@ class KubernetesResourcePoolController {
}
updateResourcePool() {
const willBeDeleted = _.filter(this.formValues.IngressClasses, { WasSelected: true, Selected: false });
const ingressesToDelete = _.filter(this.formValues.IngressClasses, { WasSelected: true, Selected: false });
const registriesToDelete = _.filter(this.registries, { WasChecked: true, Checked: false });
const warnings = {
quota: this.hasResourceQuotaBeenReduced(),
ingress: willBeDeleted.length !== 0,
ingress: ingressesToDelete.length !== 0,
registries: registriesToDelete.length !== 0,
};
if (warnings.quota || warnings.ingress) {
if (warnings.quota || warnings.ingress || warnings.registries) {
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.',
ingress: 'Deactivating ingresses may cause applications to be unaccessible. All ingress configurations from affected applications will be removed.',
registries:
'Some registries you removed might be used by one or more applications inside this environment. Removing the registries access could lead to a service interruption for these applications.',
};
const displayedMessage = `${warnings.quota ? messages.quota : ''}${warnings.quota && warnings.ingress ? '<br/><br/>' : ''}
${warnings.ingress ? messages.ingress : ''}<br/><br/>Do you wish to continue?`;
const displayedMessage = `${warnings.quota ? messages.quota + '<br/><br/>' : ''}
${warnings.ingress ? messages.ingress + '<br/><br/>' : ''}
${warnings.registries ? messages.registries + '<br/><br/>' : ''}
Do you wish to continue?`;
this.ModalService.confirmUpdate(displayedMessage, (confirmed) => {
if (confirmed) {
return this.$async(this.updateResourcePoolAsync, this.savedFormValues, this.formValues);
@@ -322,10 +328,11 @@ class KubernetesResourcePoolController {
this.registries.forEach((reg) => {
if (reg.RegistryAccesses && reg.RegistryAccesses[this.endpoint.Id] && reg.RegistryAccesses[this.endpoint.Id].Namespaces.includes(namespace)) {
reg.Checked = true;
reg.WasChecked = true;
this.formValues.Registries.push(reg);
}
});
this.selectedRegistries = this.formValues.Registries.map((r) => r.Name).join(', ');
return;
}

View File

@@ -5,8 +5,6 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
<div class="row">
<div class="col-sm-12">
<rd-widget>

View File

@@ -1,5 +1,3 @@
require('../../templates/advancedDeploymentPanel.html');
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import angular from 'angular';

View File

@@ -8,8 +8,8 @@ class CustomTemplateSelectorController {
}
async handleChangeTemplate(templateId) {
this.selectedTemplate = this.templates.find((t) => t.id === templateId);
this.onChange(templateId);
this.selectedTemplate = this.templates.find((t) => t.Id === templateId);
this.onChange(templateId, this.selectedTemplate);
}
$onChanges({ value }) {

View File

@@ -9,7 +9,6 @@ export const webEditorForm = {
placeholder: '@',
yml: '<',
value: '<',
onChange: '<',
},

View File

@@ -6,6 +6,7 @@ export const gitFormAuthFieldset = {
bindings: {
model: '<',
onChange: '<',
showAuthExplanation: '<',
isEdit: '<',
},
};

View File

@@ -0,0 +1,15 @@
<div class="form-group" ng-class="$ctrl.className">
<div class="col-sm-12">
<p>
This stack was deployed from the git repository <code>{{ $ctrl.url }}</code>
.
</p>
<p>
Update
<code
>{{ $ctrl.configFilePath }}<span ng-if="$ctrl.additionalFiles.length > 0">,{{ $ctrl.additionalFiles.join(',') }}</span></code
>
in git and pull from here to update the stack.
</p>
</div>
</div>

View File

@@ -0,0 +1,9 @@
export const gitFormInfoPanel = {
templateUrl: './git-form-info-panel.html',
bindings: {
url: '<',
configFilePath: '<',
additionalFiles: '<',
className: '@',
},
};

View File

@@ -8,6 +8,7 @@ import { gitFormAutoUpdateFieldset } from './git-form-auto-update-fieldset';
import { gitFormComposePathField } from './git-form-compose-path-field';
import { gitFormRefField } from './git-form-ref-field';
import { gitFormUrlField } from './git-form-url-field';
import { gitFormInfoPanel } from './git-form-info-panel';
export default angular
.module('portainer.app.components.forms.git', [])
@@ -15,6 +16,7 @@ export default angular
.component('gitFormRefField', gitFormRefField)
.component('gitForm', gitForm)
.component('gitFormUrlField', gitFormUrlField)
.component('gitFormInfoPanel', gitFormInfoPanel)
.component('gitFormAdditionalFilesPanel', gitFormAdditionalFilesPanel)
.component('gitFormAdditionalFileItem', gitFormAdditionalFileItem)
.component('gitFormAutoUpdateFieldset', gitFormAutoUpdateFieldset)

View File

@@ -0,0 +1,96 @@
class KubernetesAppGitFormController {
/* @ngInject */
constructor($async, $state, StackService, ModalService, Notifications) {
this.$async = $async;
this.$state = $state;
this.StackService = StackService;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.state = {
saveGitSettingsInProgress: false,
redeployInProgress: false,
showConfig: true,
isEdit: false,
};
this.formValues = {
RefName: '',
RepositoryAuthentication: false,
RepositoryUsername: '',
RepositoryPassword: '',
};
this.onChange = this.onChange.bind(this);
this.onChangeRef = this.onChangeRef.bind(this);
}
onChangeRef(value) {
this.onChange({ RefName: value });
}
onChange(values) {
this.formValues = {
...this.formValues,
...values,
};
}
async pullAndRedeployApplication() {
return this.$async(async () => {
try {
const confirmed = await this.ModalService.confirmAsync({
title: 'Are you sure?',
message: 'Any changes to this application will be overriden by the definition in git and may cause a service interruption. Do you wish to continue?',
buttons: {
confirm: {
label: 'Update',
className: 'btn-warning',
},
},
});
if (!confirmed) {
return;
}
this.state.redeployInProgress = true;
await this.StackService.updateKubeGit(this.stack.Id, this.stack.EndpointId, this.namespace, this.formValues);
this.Notifications.success('Pulled and redeployed stack successfully');
await this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Failed redeploying application');
} finally {
this.state.redeployInProgress = false;
}
});
}
async saveGitSettings() {
return this.$async(async () => {
try {
this.state.saveGitSettingsInProgress = true;
await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, null, this.formValues);
this.Notifications.success('Save stack settings successfully');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to save application settings');
} finally {
this.state.saveGitSettingsInProgress = false;
}
});
}
isSubmitButtonDisabled() {
return this.state.saveGitSettingsInProgress || this.state.redeployInProgress;
}
$onInit() {
console.log(this);
this.formValues.RefName = this.stack.GitConfig.ReferenceName;
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
this.formValues.RepositoryAuthentication = true;
this.state.isEdit = true;
}
}
}
export default KubernetesAppGitFormController;

View File

@@ -0,0 +1,59 @@
<form name="$ctrl.redeployGitForm">
<div class="col-sm-12 form-section-title">
Redeploy from git repository
</div>
<div class="form-group text-muted">
<div class="col-sm-12">
<p>
Pull the latest manifest from git and redeploy the application.
</p>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-click="$ctrl.state.showConfig = !$ctrl.state.showConfig">
<i ng-class="['fa space-right', { 'fa-minus': $ctrl.state.showConfig, 'fa-plus': !$ctrl.state.showConfig }]" aria-hidden="true"></i>
{{ $ctrl.state.showConfig ? 'Hide' : 'Advanced' }} configuration
</a>
</p>
</div>
</div>
<git-form-ref-field ng-if="$ctrl.state.showConfig" value="$ctrl.formValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
<git-form-auth-fieldset
ng-if="$ctrl.state.showConfig"
model="$ctrl.formValues"
is-edit="$ctrl.state.isEdit"
on-change="($ctrl.onChange)"
show-auth-explanation="true"
></git-form-auth-fieldset>
<div class="col-sm-12 form-section-title">
Actions
</div>
<!-- #Git buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.pullAndRedeployApplication()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0;"
button-spinner="ctrl.state.redeployInProgress"
analytics-on
analytics-category="kubernetes"
analytics-event="kubernetes-application-edit-git-pull"
>
<span ng-show="!$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and update application </span>
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
</button>
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.saveGitSettings()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0;"
button-spinner="$ctrl.state.saveGitSettingsInProgress"
>
<span ng-show="!$ctrl.state.saveGitSettingsInProgress"> Save settings </span>
<span ng-show="$ctrl.state.saveGitSettingsInProgress">In progress...</span>
</button>
</form>

View File

@@ -0,0 +1,13 @@
import angular from 'angular';
import controller from './kubernetes-app-git-form.controller';
const kubernetesAppGitForm = {
templateUrl: './kubernetes-app-git-form.html',
controller,
bindings: {
namespace: '<',
stack: '<',
},
};
angular.module('portainer.app').component('kubernetesAppGitForm', kubernetesAppGitForm);

View File

@@ -71,7 +71,16 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid" button-spinner="$ctrl.actionInProgress">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"
analytics-event="portainer-registry-creation"
analytics-properties="{ metadata: { type: 'azure' } }"
>
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>

View File

@@ -93,7 +93,16 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid" button-spinner="$ctrl.actionInProgress">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"
analytics-event="portainer-registry-creation"
analytics-properties="{ metadata: { type: 'custom' }}"
>
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>

View File

@@ -54,7 +54,16 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid" button-spinner="$ctrl.actionInProgress">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"
analytics-event="portainer-registry-creation"
analytics-properties="{ metadata: { type: 'dockerhub' } }"
>
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>

View File

@@ -133,6 +133,10 @@
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"
analytics-event="portainer-registry-creation"
analytics-properties="{ metadata: { type: 'gitlab' } }"
>
<span ng-hide="$ctrl.actionInProgress">Create registries</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>

View File

@@ -100,7 +100,16 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormProGet.$valid" button-spinner="$ctrl.actionInProgress">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormProGet.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"
analytics-event="portainer-registry-creation"
analytics-properties="{ metadata: { type: 'proget' } }"
>
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>

View File

@@ -67,7 +67,16 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormQuay.$valid" button-spinner="$ctrl.actionInProgress">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !registryFormQuay.$valid"
button-spinner="$ctrl.actionInProgress"
analytics-on
analytics-category="portainer"
analytics-event="portainer-registry-creation"
analytics-properties="{ metadata: { type: 'quay' } }"
>
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>

View File

@@ -25,7 +25,7 @@ class StackRedeployGitFormController {
RepositoryUsername: '',
RepositoryPassword: '',
Env: [],
// auto upadte
// auto update
AutoUpdate: {
RepositoryAutomaticUpdates: false,
RepositoryMechanism: 'Interval',
@@ -40,6 +40,25 @@ class StackRedeployGitFormController {
this.onChangeEnvVar = this.onChangeEnvVar.bind(this);
}
buildAnalyticsProperties() {
const metadata = {};
if (this.formValues.RepositoryAutomaticUpdates) {
metadata.automaticUpdates = autoSyncLabel(this.formValues.RepositoryMechanism);
}
return { metadata };
function autoSyncLabel(type) {
switch (type) {
case 'Interval':
return 'polling';
case 'Webhook':
return 'webhook';
}
return 'off';
}
}
onChange(values) {
this.formValues = {
...this.formValues,
@@ -100,10 +119,17 @@ class StackRedeployGitFormController {
return this.$async(async () => {
try {
this.state.inProgress = true;
await this.StackService.updateGitStackSettings(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), this.formValues);
const stack = await this.StackService.updateGitStackSettings(
this.stack.Id,
this.stack.EndpointId,
this.FormHelper.removeInvalidEnvVars(this.formValues.Env),
this.formValues
);
this.savedFormValues = angular.copy(this.formValues);
this.state.hasUnsavedChanges = false;
this.Notifications.success('Save stack settings successfully');
this.stack = stack;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to save stack settings');
} finally {
@@ -116,6 +142,12 @@ class StackRedeployGitFormController {
return this.state.inProgress || this.state.redeployInProgress;
}
isAutoUpdateChanged() {
const wasEnabled = !!(this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook));
const isEnabled = this.formValues.AutoUpdate.RepositoryAutomaticUpdates;
return isEnabled !== wasEnabled;
}
$onInit() {
this.formValues.RefName = this.model.ReferenceName;
this.formValues.Env = this.stack.Env;

View File

@@ -2,21 +2,12 @@
<div class="col-sm-12 form-section-title">
Redeploy from git repository
</div>
<div class="text-muted small form-group">
<div class="col-sm-12">
<p>
This stack was deployed from the git repository <code>{{ $ctrl.model.URL }}</code>
.
</p>
<p>
Update
<code
>{{ $ctrl.model.ConfigFilePath }}<span ng-if="$ctrl.stack.AdditionalFiles.length > 0">,{{ $ctrl.stack.AdditionalFiles.join(',') }}</span></code
>
in git and pull from here to update the stack.
</p>
</div>
</div>
<git-form-info-panel
class-name="text-muted small"
url="$ctrl.model.URL"
config-file-path="$ctrl.model.ConfigFilePath"
additional-files="$ctrl.stack.AdditionalFiles"
></git-form-info-panel>
<git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChangeAutoUpdate)"></git-form-auto-update-fieldset>
<div class="form-group">
@@ -50,8 +41,9 @@
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0;"
button-spinner="$ctrl.state.redeployInProgress"
style="margin-top: 7px; margin-left: 0;"
button-spinner="$ctrl.state.inProgress"
analytics-on
analytics-event="docker-stack-pull-redeploy"
analytics-category="docker"
>
<span ng-hide="$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and redeploy </span>
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
@@ -63,6 +55,10 @@
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0;"
button-spinner="$ctrl.state.inProgress"
analytics-on
analytics-event="docker-stack-update-git-settings"
analytics-category="docker"
analytics-properties="$ctrl.buildAnalyticsProperties()"
>
<span ng-hide="$ctrl.state.inProgress"> Save settings </span>
<span ng-show="$ctrl.state.inProgress">In progress...</span>

View File

@@ -1,6 +1,6 @@
import _ from 'lodash-es';
export const KEY_REGEX = /(.+)/.source;
export const KEY_REGEX = /(.+?)/.source;
export const VALUE_REGEX = /(.*)?/.source;
const KEY_VALUE_REGEX = new RegExp(`^(${KEY_REGEX})\\s*=(${VALUE_REGEX})$`);

View File

@@ -7,6 +7,7 @@ export function StackViewModel(data) {
this.EndpointId = data.EndpointId;
this.SwarmId = data.SwarmId;
this.Env = data.Env ? data.Env : [];
this.IsComposeFormat = data.IsComposeFormat;
if (data.ResourceControl && data.ResourceControl.Id !== 0) {
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
}

View File

@@ -137,10 +137,10 @@ angular.module('portainer.app').factory('EndpointService', [
return deferred.promise;
};
service.createLocalKubernetesEndpoint = function (name = 'local') {
service.createLocalKubernetesEndpoint = function (name = 'local', tagIds = []) {
var deferred = $q.defer();
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, [], true, true, true)
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, tagIds, true, true, true)
.then(function success(response) {
deferred.resolve(response.data);
})

View File

@@ -15,6 +15,7 @@ angular.module('portainer.app').factory('StackService', [
'use strict';
var service = {
updateGit,
updateKubeGit,
};
service.stack = function (id) {
@@ -268,6 +269,25 @@ angular.module('portainer.app').factory('StackService', [
return Stack.update({ endpointId: stack.EndpointId }, { id: stack.Id, StackFileContent: stackFile, Env: env, Prune: prune }).$promise;
};
service.updateKubeStack = function (stack, stackFile, gitConfig) {
let payload = {};
if (stackFile) {
payload = {
StackFileContent: stackFile,
};
} else {
payload = {
RepositoryReferenceName: gitConfig.RefName,
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
RepositoryUsername: gitConfig.RepositoryUsername,
RepositoryPassword: gitConfig.RepositoryPassword,
};
}
return Stack.update({ id: stack.Id, endpointId: stack.EndpointId }, payload).$promise;
};
service.createComposeStackFromFileUpload = function (name, stackFile, env, endpointId) {
return FileUploadService.createComposeStack(name, stackFile, env, endpointId);
};
@@ -417,6 +437,19 @@ angular.module('portainer.app').factory('StackService', [
).$promise;
}
function updateKubeGit(id, endpointId, namespace, gitConfig) {
return Stack.updateGit(
{ endpointId, id },
{
Namespace: namespace,
RepositoryReferenceName: gitConfig.RefName,
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
RepositoryUsername: gitConfig.RepositoryUsername,
RepositoryPassword: gitConfig.RepositoryPassword,
}
).$promise;
}
service.updateGitStackSettings = function (id, endpointId, env, gitConfig) {
// prepare auto update
const autoUpdate = {};

View File

@@ -51,6 +51,10 @@ class SslCertificateController {
});
}
wasHTTPsChanged() {
return this.originalValues.forceHTTPS !== this.formValues.forceHTTPS;
}
async $onInit() {
return this.$async(async () => {
try {

View File

@@ -80,6 +80,11 @@
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.isFormChanged()"
ng-click="$ctrl.save()"
button-spinner="$ctrl.state.actionInProgress"
analytics-on
analytics-if="$ctrl.wasHTTPsChanged()"
analytics-category="portainer"
analytics-event="portainer-settings-edit"
analytics-properties="{ metadata: { forceHTTPS: $ctrl.formValues.forceHTTPS } }"
>
<span ng-hide="$ctrl.state.actionInProgress || $ctrl.state.reloadingPage">Apply Changes</span>
<span ng-show="$ctrl.state.actionInProgress">Saving in progress...</span>

View File

@@ -5,6 +5,7 @@ angular
.module('portainer.app')
.controller('CreateEndpointController', function CreateEndpointController(
$async,
$analytics,
$q,
$scope,
$state,
@@ -153,8 +154,9 @@ angular
$scope.addKubernetesEndpoint = function () {
var name = $scope.formValues.Name;
var tagIds = $scope.formValues.TagIds;
$scope.state.actionInProgress = true;
EndpointService.createLocalKubernetesEndpoint(name)
EndpointService.createLocalKubernetesEndpoint(name, tagIds)
.then(function success(result) {
Notifications.success('Endpoint created', name);
$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: result.Id });
@@ -167,16 +169,32 @@ angular
});
};
$scope.addAgentEndpoint = function () {
var name = $scope.formValues.Name;
// var URL = $filter('stripprotocol')($scope.formValues.URL);
var URL = $scope.formValues.URL;
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
var groupId = $scope.formValues.GroupId;
var tagIds = $scope.formValues.TagIds;
$scope.addAgentEndpoint = addAgentEndpoint;
async function addAgentEndpoint() {
return $async(async () => {
const name = $scope.formValues.Name;
const URL = $scope.formValues.URL;
const publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
const groupId = $scope.formValues.GroupId;
const tagIds = $scope.formValues.TagIds;
addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null);
};
const endpoint = await addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null);
$analytics.eventTrack('portainer-endpoint-creation', { category: 'portainer', metadata: { type: 'agent', platform: platformLabel(endpoint.Type) } });
});
function platformLabel(type) {
switch (type) {
case PortainerEndpointTypes.DockerEnvironment:
case PortainerEndpointTypes.AgentOnDockerEnvironment:
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
return 'docker';
case PortainerEndpointTypes.KubernetesLocalEnvironment:
case PortainerEndpointTypes.AgentOnKubernetesEnvironment:
case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment:
return 'kubernetes';
}
}
}
$scope.addEdgeAgentEndpoint = function () {
var name = $scope.formValues.Name;
@@ -213,24 +231,26 @@ angular
});
}
function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) {
$scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(
name,
creationType,
URL,
PublicURL,
groupId,
tagIds,
TLS,
TLSSkipVerify,
TLSSkipClientVerify,
TLSCAFile,
TLSCertFile,
TLSKeyFile,
CheckinInterval
)
.then(function success(endpoint) {
async function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) {
return $async(async () => {
$scope.state.actionInProgress = true;
try {
const endpoint = await EndpointService.createRemoteEndpoint(
name,
creationType,
URL,
PublicURL,
groupId,
tagIds,
TLS,
TLSSkipVerify,
TLSSkipClientVerify,
TLSCAFile,
TLSCertFile,
TLSKeyFile,
CheckinInterval
);
Notifications.success('Endpoint created', name);
switch (endpoint.Type) {
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
@@ -244,13 +264,14 @@ angular
$state.go('portainer.endpoints', {}, { reload: true });
break;
}
})
.catch(function error(err) {
return endpoint;
} catch (err) {
Notifications.error('Failure', err, 'Unable to create endpoint');
})
.finally(function final() {
} finally {
$scope.state.actionInProgress = false;
});
}
});
}
function initView() {

View File

@@ -482,6 +482,10 @@
ng-click="addDockerEndpoint()"
button-spinner="state.actionInProgress"
data-cy="endpointCreate-createDockerEndpoint"
analytics-on
analytics-category="portainer"
analytics-event="portainer-endpoint-creation"
analytics-properties="{ metadata: { type: 'docker-api' } }"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
@@ -506,6 +510,10 @@
ng-click="addEdgeAgentEndpoint()"
button-spinner="state.actionInProgress"
data-cy="endpointCreate-createEdgeAgentEndpoint"
analytics-on
analytics-category="portainer"
analytics-event="portainer-endpoint-creation"
analytics-properties="{ metadata: { type: 'edge-agent' } }"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
@@ -517,6 +525,10 @@
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
ng-click="addKubernetesEndpoint()"
button-spinner="state.actionInProgress"
analytics-on
analytics-category="portainer"
analytics-event="portainer-endpoint-creation"
analytics-properties="{ metadata: { type: 'kubernetes-api' } }"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
@@ -529,6 +541,10 @@
ng-click="addAzureEndpoint()"
button-spinner="state.actionInProgress"
data-cy="endpointCreate-createAzureEndpoint"
analytics-on
analytics-category="portainer"
analytics-event="portainer-endpoint-creation"
analytics-properties="{ metadata: { type: 'azure-api' } }"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>

View File

@@ -23,7 +23,16 @@
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
</p>
<p>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress" ng-click="onDeassociateEndpoint()" button-spinner="state.actionInProgress">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress"
ng-click="onDeassociateEndpoint()"
button-spinner="state.actionInProgress"
analytics-on
analytics-event="edge-endpoint-deassociate"
analytics-category="edge"
>
<span ng-hide="state.actionInProgress">De-associate</span>
</button>
</p>

View File

@@ -168,10 +168,14 @@ function EndpointController(
payload.URL = 'tcp://' + endpoint.URL;
}
if (endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment) {
if (endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment) {
payload.URL = endpoint.URL;
}
if (endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment) {
payload.URL = 'https://' + endpoint.URL;
}
$scope.state.actionInProgress = true;
EndpointService.updateEndpoint(endpoint.Id, payload).then(
function success() {

View File

@@ -21,7 +21,11 @@ angular.module('portainer.app').controller('RegistriesController', [
};
$scope.removeAction = function (selectedItems) {
ModalService.confirmDeletion('Do you want to remove the selected registries?', function onConfirm(confirmed) {
const regAttrMsg = selectedItems.length > 1 ? 'hese' : 'his';
const registriesMsg = selectedItems.length > 1 ? 'registries' : 'registry';
const msg = `T${regAttrMsg} ${registriesMsg} might be used by applications inside one or more endpoints. Removing the ${registriesMsg} could lead to a service interruption for the applications using t${regAttrMsg} ${registriesMsg}. Do you want to remove the selected ${registriesMsg}?`;
ModalService.confirmDeletion(msg, function onConfirm(confirmed) {
if (!confirmed) {
return;
}

View File

@@ -26,6 +26,7 @@ angular
clipboard
) {
$scope.onChangeTemplateId = onChangeTemplateId;
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
$scope.formValues = {
Name: '',
@@ -33,7 +34,7 @@ angular
StackFile: null,
RepositoryURL: '',
RepositoryReferenceName: '',
RepositoryAuthentication: false,
RepositoryAuthentication: true,
RepositoryUsername: '',
RepositoryPassword: '',
Env: [],
@@ -54,6 +55,8 @@ angular
editorYamlValidationError: '',
uploadYamlValidationError: '',
isEditorDirty: false,
selectedTemplate: null,
selectedTemplateId: null,
};
$window.onbeforeunload = () => {
@@ -76,6 +79,47 @@ angular
$scope.formValues.AdditionalFiles.splice(index, 1);
};
function buildAnalyticsProperties() {
const metadata = { type: methodLabel($scope.state.Method) };
if ($scope.state.Method === 'repository') {
metadata.automaticUpdates = 'off';
if ($scope.formValues.RepositoryAutomaticUpdates) {
metadata.automaticUpdates = autoSyncLabel($scope.formValues.RepositoryMechanism);
}
metadata.auth = $scope.formValues.RepositoryAuthentication;
}
if ($scope.state.Method === 'template') {
metadata.templateName = $scope.state.selectedTemplate.Title;
}
return { metadata };
function methodLabel(method) {
switch (method) {
case 'editor':
return 'web-editor';
case 'repository':
return 'git';
case 'upload':
return 'file-upload';
case 'template':
return 'custom-template';
}
}
function autoSyncLabel(type) {
switch (type) {
case 'Interval':
return 'polling';
case 'Webhook':
return 'webhook';
}
return 'off';
}
}
function validateForm(accessControlData, isAdmin) {
$scope.state.formValidationError = '';
var error = '';
@@ -238,10 +282,11 @@ angular
}
};
function onChangeTemplateId(templateId) {
function onChangeTemplateId(templateId, template) {
return $async(async () => {
try {
$scope.state.templateId = templateId;
$scope.state.selectedTemplateId = templateId;
$scope.state.selectedTemplate = template;
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
$scope.onChangeFileContent(fileContent);

View File

@@ -120,11 +120,11 @@
new-template-path="docker.templates.custom.new"
stack-type="state.StackType"
on-change="(onChangeTemplateId)"
value="state.templateId"
value="state.selectedTemplateId"
></custom-template-selector>
<web-editor-form
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.templateId)"
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.selectedTemplateId)"
identifier="stack-creation-editor"
value="formValues.StackFileContent"
on-change="(onChangeFileContent)"
@@ -164,6 +164,10 @@
|| !formValues.Name"
ng-click="deployStack()"
button-spinner="state.actionInProgress"
analytics-on
analytics-category="docker"
analytics-event="docker-stack-create"
analytics-properties="buildAnalyticsProperties()"
>
<span ng-hide="state.actionInProgress">Deploy the stack</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>

View File

@@ -1,18 +1,57 @@
#!/usr/bin/env bash
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
PLATFORM=$1
ARCH=$2
DOCKER_COMPOSE_VERSION=$3
if [ "${PLATFORM}" == 'linux' ] && [ "${ARCH}" == 'amd64' ]; then
wget -O "dist/docker-compose" "https://github.com/portainer/docker-compose-linux-amd64-static-binary/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose"
chmod +x "dist/docker-compose"
elif [ "${PLATFORM}" == 'mac' ]; then
wget -O "dist/docker-compose" "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-Darwin-x86_64"
chmod +x "dist/docker-compose"
elif [ "${PLATFORM}" == 'win' ]; then
wget -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-Windows-x86_64.exe"
chmod +x "dist/docker-compose.exe"
function download_binary() {
local PLATFORM=$1
local ARCH=$2
local BINARY_VERSION=$3
if [ "${PLATFORM}" == 'linux' ] && [ "${ARCH}" == 'amd64' ]; then
wget -O "dist/docker-compose" "https://github.com/portainer/docker-compose-linux-amd64-static-binary/releases/download/${BINARY_VERSION}/docker-compose"
chmod +x "dist/docker-compose"
return
fi
if [ "${PLATFORM}" == 'mac' ]; then
wget -O "dist/docker-compose" "https://github.com/docker/compose/releases/download/${BINARY_VERSION}/docker-compose-Darwin-x86_64"
chmod +x "dist/docker-compose"
return
fi
if [ "${PLATFORM}" == 'win' ]; then
wget -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/${BINARY_VERSION}/docker-compose-Windows-x86_64.exe"
chmod +x "dist/docker-compose.exe"
return
fi
}
function download_plugin() {
local PLATFORM=$1
local ARCH=$2
local PLUGIN_VERSION=$3
if [ "${PLATFORM}" == 'mac' ]; then
PLATFORM="darwin"
fi
FILENAME="docker-compose-${PLATFORM}-${ARCH}"
TARGET_FILENAME="docker-compose.plugin"
if [[ "$PLATFORM" == "windows" ]]; then
FILENAME="$FILENAME.exe"
TARGET_FILENAME="$TARGET_FILENAME.exe"
fi
wget -O "dist/$TARGET_FILENAME" "https://github.com/docker/compose-cli/releases/download/v$PLUGIN_VERSION/$FILENAME"
chmod +x "dist/$TARGET_FILENAME"
}
if [ "${PLATFORM}" == 'linux' ] && [ "${ARCH}" != 'amd64' ]; then
download_plugin "$PLATFORM" "$ARCH" "$DOCKER_COMPOSE_VERSION"
fi
exit 0
download_binary "$PLATFORM" "$ARCH" "$DOCKER_COMPOSE_VERSION"

View File

@@ -1,6 +1,6 @@
ARG OSVERSION
FROM --platform=linux/amd64 gcr.io/k8s-staging-e2e-test-images/windows-servercore-cache:1.0-linux-amd64-${OSVERSION} as core
FROM --platform=linux/amd64 alpine:3.13.0 as downloader
FROM --platform=linux/amd64 alpine:3.13.6 as downloader
ENV GIT_VERSION 2.30.0
ENV GIT_PATCH_VERSION 2

View File

@@ -22,6 +22,7 @@ module.exports = function (grunt) {
dockerWindowsVersion: '19-03-12',
dockerLinuxComposeVersion: '1.27.4',
dockerWindowsComposeVersion: '1.28.0',
dockerComposePluginVersion: '2.0.0-beta.6',
komposeVersion: 'v1.22.0',
kubectlVersion: 'v1.18.0',
},
@@ -175,7 +176,9 @@ function shell_build_binary_azuredevops(p, a) {
function shell_run_container() {
return [
'docker rm -f portainer',
'docker run -d -p 8000:8000 -p 9000:9000 -v ' + portainer_root + '/dist:/app -v ' +
'docker run -d -p 8000:8000 -p 9000:9000 -p 9443:9443 -v ' +
portainer_root +
'/dist:/app -v ' +
portainer_data +
':/data -v /var/run/docker.sock:/var/run/docker.sock:z -v /var/run/docker.sock:/var/run/alternative.sock:z -v /tmp:/tmp --name portainer portainer/base /app/portainer',
].join(';');
@@ -212,13 +215,24 @@ function shell_download_docker_compose_binary(p, a) {
var ia = as[a] || a;
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsComposeVersion %>' : '<%= binaries.dockerLinuxComposeVersion %>';
return [
'if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ]; then',
'echo "Docker Compose binary exists";',
'else',
'build/download_docker_compose_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
'fi',
].join(' ');
// plugin
if (p === 'linux' && a !== 'amd64') {
if (a === 'arm64') {
ia = 'arm64';
}
if (a === 'arm') {
ia = 'armv7';
}
binaryVersion = '<%= binaries.dockerComposePluginVersion %>';
}
return `
if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ] || [ -f dist/docker-compose.plugin ] || [ -f dist/docker-compose.plugin.exe ]; then
echo "Docker Compose binary exists";
else
build/download_docker_compose_binary.sh ${ip} ${ia} ${binaryVersion};
fi`;
}
function shell_download_kompose_binary(p, a) {