From f16fdd3ea7e2dfeeaae45f5e8bb0a0226318a1ba Mon Sep 17 00:00:00 2001 From: fhanportainer <79428273+fhanportainer@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:46:14 +1200 Subject: [PATCH 1/8] 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 --- app/portainer/services/api/endpointService.js | 4 ++-- .../views/endpoints/create/createEndpointController.js | 3 ++- app/portainer/views/endpoints/edit/endpointController.js | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 33dd04e29..852d398f9 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -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); }) diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index f8d983456..dde612ab5 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -154,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 }); diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 5e297df04..2318a9509 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -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() { From 1b7296d5d1a979b0750d8741319c415589aaa927 Mon Sep 17 00:00:00 2001 From: LP B Date: Mon, 6 Sep 2021 07:23:51 +0200 Subject: [PATCH 2/8] fix(app/env-vars): make key regex non-greedy to match on first equal sign (#5545) --- app/portainer/helpers/env-vars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/portainer/helpers/env-vars.js b/app/portainer/helpers/env-vars.js index c55c448ea..f64f52b5e 100644 --- a/app/portainer/helpers/env-vars.js +++ b/app/portainer/helpers/env-vars.js @@ -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})$`); From 6fea8373c654bec24ad246e2c3b01e4df29b15f3 Mon Sep 17 00:00:00 2001 From: LP B Date: Mon, 6 Sep 2021 07:25:02 +0200 Subject: [PATCH 3/8] 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 --- .../kube-registry-access-view.controller.js | 12 ++++++++++-- .../edit/resourcePoolController.js | 17 ++++++++++++----- .../views/registries/registriesController.js | 6 +++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js index 9367f5aef..97db67ae8 100644 --- a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js +++ b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js @@ -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.

Do you wish to continue?'; + this.ModalService.confirmDeletion(displayedMessage, (confirmed) => { + if (confirmed) { + return this.updateNamespaces(nsToUpdate); + } + }); } updateNamespaces(namespaces) { diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index 1fe6e844d..f611a5b9d 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -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 ? '

' : ''} - ${warnings.ingress ? messages.ingress : ''}

Do you wish to continue?`; + const displayedMessage = `${warnings.quota ? messages.quota + '

' : ''} + ${warnings.ingress ? messages.ingress + '

' : ''} + ${warnings.registries ? messages.registries + '

' : ''} + Do you wish to continue?`; this.ModalService.confirmUpdate(displayedMessage, (confirmed) => { if (confirmed) { return this.$async(this.updateResourcePoolAsync, this.savedFormValues, this.formValues); @@ -322,6 +328,7 @@ 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); } }); diff --git a/app/portainer/views/registries/registriesController.js b/app/portainer/views/registries/registriesController.js index 83e3cbafc..568516de7 100644 --- a/app/portainer/views/registries/registriesController.js +++ b/app/portainer/views/registries/registriesController.js @@ -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; } From 582d3701720a094d6c7f6bab5e6ebfd9dec65087 Mon Sep 17 00:00:00 2001 From: LP B Date: Mon, 6 Sep 2021 07:25:43 +0200 Subject: [PATCH 4/8] fix(k8s/namespace): missing header in namespace creation view (#5575) --- .../views/resource-pools/create/createResourcePool.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html index 63712f155..9a624b2f1 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePool.html +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -1,4 +1,4 @@ - + Namespaces > Create a namespace From 3453735c8bd272ef074b6c66a5b85e9a692b2b39 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 6 Sep 2021 10:58:26 +0300 Subject: [PATCH 5/8] feat(stacks): support standalone stacks on ARM (#5310) --- api/cmd/portainer/main.go | 25 +-- api/exec/compose_stack.go | 35 +++-- api/exec/compose_stack_integration_test.go | 7 +- api/exec/swarm_stack.go | 24 +-- api/filesystem/filesystem.go | 12 ++ api/go.mod | 20 ++- api/go.sum | 109 +++---------- .../handler/stacks/create_compose_stack.go | 3 +- api/http/handler/stacks/stack_delete.go | 3 +- api/http/handler/stacks/stack_start.go | 3 +- api/http/handler/stacks/stack_stop.go | 3 +- api/http/proxy/factory/docker_compose.go | 2 +- api/libcompose/compose_stack.go | 148 ------------------ api/portainer.go | 5 +- api/stacks/deployer.go | 3 +- build/download_docker_compose_binary.sh | 61 ++++++-- gruntfile.js | 26 ++- 17 files changed, 181 insertions(+), 308 deletions(-) delete mode 100644 api/libcompose/compose_stack.go diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 32074c7e5..a6070ad25 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -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) diff --git a/api/exec/compose_stack.go b/api/exec/compose_stack.go index 36283c6d9..94df99a65 100644 --- a/api/exec/compose_stack.go +++ b/api/exec/compose_stack.go @@ -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) { diff --git a/api/exec/compose_stack_integration_test.go b/api/exec/compose_stack_integration_test.go index f0b279659..27209d57d 100644 --- a/api/exec/compose_stack_integration_test.go +++ b/api/exec/compose_stack_integration_test.go @@ -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) } diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index eecb1047a..6256e92e3 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -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 diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index 416798ad7..9063c22f6 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -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) diff --git a/api/go.mod b/api/go.mod index 8273e9271..9d36555bd 100644 --- a/api/go.mod +++ b/api/go.mod @@ -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 diff --git a/api/go.sum b/api/go.sum index 47f69d704..6aafc2567 100644 --- a/api/go.sum +++ b/api/go.sum @@ -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= diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 4ca76967f..bd27a2eb4 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -1,6 +1,7 @@ package stacks import ( + "context" "fmt" "net/http" "path" @@ -409,7 +410,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) handler.SwarmStackManager.Login(config.registries, config.endpoint) - err = handler.ComposeStackManager.Up(config.stack, config.endpoint) + err = handler.ComposeStackManager.Up(context.TODO(), config.stack, config.endpoint) if err != nil { return errors.Wrap(err, "failed to start up the stack") } diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index f7ce69bf1..98c3e57ff 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -1,6 +1,7 @@ package stacks import ( + "context" "errors" "net/http" "strconv" @@ -174,5 +175,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) } diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go index ff3f5f268..e2c9fbfba 100644 --- a/api/http/handler/stacks/stack_start.go +++ b/api/http/handler/stacks/stack_start.go @@ -1,6 +1,7 @@ package stacks import ( + "context" "errors" "fmt" "net/http" @@ -118,7 +119,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) } diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go index 1e24d9607..fcab18929 100644 --- a/api/http/handler/stacks/stack_stop.go +++ b/api/http/handler/stacks/stack_stop.go @@ -1,6 +1,7 @@ package stacks import ( + "context" "errors" "net/http" @@ -102,7 +103,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) } diff --git a/api/http/proxy/factory/docker_compose.go b/api/http/proxy/factory/docker_compose.go index 9438117e8..2d1ac4966 100644 --- a/api/http/proxy/factory/docker_compose.go +++ b/api/http/proxy/factory/docker_compose.go @@ -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 { diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go deleted file mode 100644 index f2dfff230..000000000 --- a/api/libcompose/compose_stack.go +++ /dev/null @@ -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 -} diff --git a/api/portainer.go b/api/portainer.go index 74784e992..48aab26da 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1050,8 +1050,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 +1175,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 diff --git a/api/stacks/deployer.go b/api/stacks/deployer.go index d38c50cbc..c594e48ec 100644 --- a/api/stacks/deployer.go +++ b/api/stacks/deployer.go @@ -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) } diff --git a/build/download_docker_compose_binary.sh b/build/download_docker_compose_binary.sh index 131de5ac4..63ea5b005 100755 --- a/build/download_docker_compose_binary.sh +++ b/build/download_docker_compose_binary.sh @@ -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" \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index b0a1e2906..84efc034d 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -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', }, @@ -214,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) { From f03929221142ec612c53ede9709d5e3beebd8a6c Mon Sep 17 00:00:00 2001 From: itsconquest Date: Tue, 7 Sep 2021 10:36:42 +1200 Subject: [PATCH 6/8] chore(project): replace stalebot with action [EE-1509] (#5515) * chore(project): replace stalebot with action [EE-1509] * add missing newline at EOF --- .github/stale.yml | 54 ------------------------------------- .github/workflows/stale.yml | 27 +++++++++++++++++++ 2 files changed, 27 insertions(+), 54 deletions(-) delete mode 100644 .github/stale.yml create mode 100644 .github/workflows/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index fce5d3700..000000000 --- a/.github/stale.yml +++ /dev/null @@ -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. diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..cf978446c --- /dev/null +++ b/.github/workflows/stale.yml @@ -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 From e49e90f304ba259ce42084ea87f3f3ae5a435606 Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Tue, 7 Sep 2021 12:37:26 +1200 Subject: [PATCH 7/8] 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 * 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 Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com> Co-authored-by: Felix Han --- .../handler/stacks/create_compose_stack.go | 13 +- .../handler/stacks/create_kubernetes_stack.go | 122 +- api/http/handler/stacks/create_swarm_stack.go | 26 +- api/http/handler/stacks/stack_create.go | 10 +- api/http/handler/stacks/stack_delete.go | 14 +- api/http/handler/stacks/stack_file.go | 22 +- api/http/handler/stacks/stack_inspect.go | 31 +- api/http/handler/stacks/stack_migrate.go | 30 +- api/http/handler/stacks/stack_start.go | 22 +- api/http/handler/stacks/stack_stop.go | 22 +- api/http/handler/stacks/stack_update.go | 80 +- api/http/handler/stacks/stack_update_git.go | 36 +- .../stacks/stack_update_git_redeploy.go | 118 +- .../handler/stacks/update_kubernetes_stack.go | 97 + api/portainer.go | 4 + .../resourcePoolsDatatable.html | 2 +- .../resourcePoolsDatatableController.js | 2 +- .../volumes-datatable/volumesDatatable.html | 2 +- app/kubernetes/converters/application.js | 2 + .../models/application/models/constants.js | 3 + .../models/application/models/index.js | 8 + app/kubernetes/models/deploy.js | 4 +- .../create/createApplication.html | 2874 +++++++++-------- .../create/createApplicationController.js | 59 + .../views/applications/edit/application.html | 7 +- .../edit/applicationController.js | 19 +- app/kubernetes/views/deploy/deploy.html | 4 +- .../form-components/web-editor-form/index.js | 1 - .../git-form/git-form-auth-fieldset/index.js | 1 + .../git-form-info-panel.html | 15 + .../git-form/git-form-info-panel/index.js | 9 + .../components/forms/git-form/index.js | 2 + .../kubernetes-app-git-form.controller.js | 96 + .../kubernetes-app-git-form.html | 59 + .../kubernetes-app-git-form.js | 13 + .../stack-redeploy-git-form.controller.js | 5 - .../stack-redeploy-git-form.html | 21 +- app/portainer/models/stack.js | 1 + app/portainer/services/api/stackService.js | 33 + .../stacks/create/createStackController.js | 2 +- 40 files changed, 2234 insertions(+), 1657 deletions(-) create mode 100644 api/http/handler/stacks/update_kubernetes_stack.go create mode 100644 app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html create mode 100644 app/portainer/components/forms/git-form/git-form-info-panel/index.js create mode 100644 app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js create mode 100644 app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html create mode 100644 app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index bd27a2eb4..df78ca965 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -1,7 +1,6 @@ package stacks import ( - "context" "fmt" "net/http" "path" @@ -405,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(context.TODO(), 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) } diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index f6129d81d..29ddfe280 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -1,26 +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" - k "github.com/portainer/portainer/api/kubernetes" + 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 @@ -39,9 +40,9 @@ type kubernetesGitDeploymentPayload struct { } type kubernetesManifestURLDeploymentPayload struct { - Namespace string - ComposeFormat bool - ManifestURL string + Namespace string + ComposeFormat bool + ManifestURL string } func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error { @@ -68,7 +69,7 @@ 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 } @@ -84,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 @@ -116,6 +130,7 @@ 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} } @@ -125,6 +140,8 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err} } + doCleanUp = false + resp := &createKubernetesStackResponse{ Output: output, } @@ -134,20 +151,40 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit return response.JSON(w, resp) } -func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError { +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, - Status: portainer.StackStatusActive, - CreationDate: time.Now().Unix(), + 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))) @@ -156,6 +193,12 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr 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} @@ -167,6 +210,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr Owner: stack.CreatedBy, Kind: "git", }) + if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err} } @@ -176,6 +220,8 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err} } + doCleanUp = false + resp := &createKubernetesStackResponse{ Output: output, } @@ -185,27 +231,33 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr return response.JSON(w, resp) } - -func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError { +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(), + 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) + manifestContent, err = client.Get(payload.ManifestURL, 30) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve manifest from URL", err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve manifest from URL", Err: err} } stackFolder := strconv.Itoa(int(stack.ID)) @@ -240,7 +292,7 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit 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() @@ -258,7 +310,7 @@ func (handler *Handler) deployKubernetesStack(r *http.Request, endpoint *portain return "", errors.Wrap(err, "failed to add application labels") } - 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) { diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index e76338384..b2252b9af 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -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) } diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 2ffb455e8..fe5a0526f 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -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,14 +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) + 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)} } diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 98c3e57ff..424255b89 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -88,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} + } } } diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index f21083bb1..35d38d378 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -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} + } } } diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index d0797700e..05c1ab102 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -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 + } } } diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index 2323a50ab..19377f418 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -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 diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go index e2c9fbfba..4b036a287 100644 --- a/api/http/handler/stacks/stack_start.go +++ b/api/http/handler/stacks/stack_start.go @@ -69,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 { diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go index fcab18929..8b435c560 100644 --- a/api/http/handler/stacks/stack_stop.go +++ b/api/http/handler/stacks/stack_stop.go @@ -58,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 { diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 24f85f817..99aeab570 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -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 diff --git a/api/http/handler/stacks/stack_update_git.go b/api/http/handler/stacks/stack_update_git.go index 48b7d2af4..7e1e7883c 100644 --- a/api/http/handler/stacks/stack_update_git.go +++ b/api/http/handler/stacks/stack_update_git.go @@ -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 diff --git a/api/http/handler/stacks/stack_update_git_redeploy.go b/api/http/handler/stacks/stack_update_git_redeploy.go index c338757e0..445ba9604 100644 --- a/api/http/handler/stacks/stack_update_git_redeploy.go +++ b/api/http/handler/stacks/stack_update_git_redeploy.go @@ -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= +// @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 } diff --git a/api/http/handler/stacks/update_kubernetes_stack.go b/api/http/handler/stacks/update_kubernetes_stack.go new file mode 100644 index 000000000..114552b68 --- /dev/null +++ b/api/http/handler/stacks/update_kubernetes_stack.go @@ -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 +} diff --git a/api/portainer.go b/api/portainer.go index 48aab26da..87903d4b9 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -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 diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html index 880ad737f..0b0d95060 100644 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html @@ -101,7 +101,7 @@ - + Quota diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js index a669e3bc0..46519a99d 100644 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js @@ -38,7 +38,7 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin); }; - this.namespaceStatusColor = function(status) { + this.namespaceStatusColor = function (status) { switch (status.toLowerCase()) { case 'active': return 'success'; diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html index 56958fc34..1f67c26b5 100644 --- a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html @@ -56,7 +56,7 @@ + -
- Application -
- -
- -
- + + + + + + + + + +

+ + Portainer uses Kompose to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not + all the Compose format options are supported by Kompose at the moment. +

+

+ You can get more information about Compose file format in the + official documentation. +

+ + +

+ + This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...). +

+

+ You can get more information about Kubernetes file format in the + official documentation. +

+
+ + + +
+
+ Application
-
-
-
-
-

This field is required.

-

This field must consist of lower case alphanumeric characters or '-', start with an alphabetic - character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

+
+ +
+ +
+
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters or '-', start with an alphabetic + character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

+
+

An application with the same name already exists inside the selected namespace.

-

An application with the same name already exists inside the selected namespace.

-
- + - + -
-
- -
-
- -
- Stack -
- -
-
- - Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use the - application name. -
-
- -
- -
- -
-
- - -
- Environment -
- -
-
- - - add environment variable - -
- -
-
-
-
-
- name - -
-
- -
- value - -
- -
- - -
-
-
-
-
- -

Environment variable name is required.

-

This field must consist of alphabetic characters, digits, '_', '-', or '.', and must not - start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.

-
-

This environment variable is already defined.

-
-
-
-
-
-
-
-
- - -
- Configurations -
- -
-
- - - add configuration - -
-
- - Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key via - the override button. -
-
- - -
- -
- -
-
- - - -
- -
-
-
- The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: - - {{ key }}{{ $last ? '' : ', ' }} - -
-
- - - -
-
-
-
-
- configuration key - -
- -
-
- path on disk - -
-
- -
- - -
-
- -
-
-
-
-
- -

Path is required.

-
-

This path is already used.

-
-
-
-
-
-
- -
- - - -
- Persisting data -
- -
-
- - No storage option is available to persist data, contact your administrator to enable a storage option. -
-
- -
-
- - - add persisted folder - -
- -
-
-
- path in container - -
- -
- - - - -
- -
- requested size - - - - -
- -
- storage - - -
- -
- volume - -
- -
-
- - -
-
-
- -
-
-
- -

Path is required.

-
-

This path is already defined.

-
-
- -
- -
-
- -

Size is required.

-

This value must be greater than zero.

-
-
-
- -

Volume is required.

-
-

This volume is already used.

-
-
- -
-
-
-
- - - -
- +
+
+ Stack +
+
- Specify how the data will be used across instances. + + Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use + the application name.
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
- - -
- Resource reservations -
- -
-
- - Resource reservations are applied per instance of the application. -
-
- -
-
- - A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums are - inherited from the namespace quota. -
-
- -
-
- - This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the - namespace. -
-
- - -
- -
- -
-
- -
-
-

- Maximum memory usage (MB) -

-
-
-
-
-
-

Value must be between {{ ctrl.state.sliders.memory.min }} and - {{ ctrl.state.sliders.memory.max }} -

-
-
-
- - -
- -
- -
-
-

- Maximum CPU usage -

-
-
- -
-
- - These reservations would exceed the resources currently available in the cluster. -
-
- - - -
- Deployment -
- -
-
- Select how you want to deploy your application inside the cluster. -
-
- - -
-
-
- - -
-
- - -
-
+
+ +
-
-
- + - -
-
- - -
-
-
-
- -

Instance count is required.

-

Instance count must be greater than 0.

-
-
-
- - -
-
- - This application will reserve the following resources: - {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and - {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. -
-
- -
-
- - This application would exceed available resources. Please review resource reservations or the instance count. -
-
- -
-
- - The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. -
-
- - - -
- Auto-scaling -
- -
-
- - -
-
- -
-
-

- This feature is currently disabled and must be enabled by an administrator user. -

-

- Server metrics features must be enabled in the - endpoint configuration view. -

-
-
- -
- - - - - - - - - - - - - -
Minimum instancesMaximum instances - Target CPU usage (%) - - -
-
- -
-
-
- -

Minimum instances is required.

-

Minimum instances must be greater than 0.

-

Minimum instances must be smaller than maximum instances.

-
-
-
-
-
- -
-
-
- -

Maximum instances is required.

-

Maximum instances must be greater than minimum instances.

-
-
-
-
-
- -
-
-
- -

Target CPU usage is required.

-

Target CPU usage must be greater than 0.

-

Target CPU usage must be smaller than 100.

-
-
-
-
- -
-
- - This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy. -
-
-
- - -
- Placement preferences and constraints + Environment
- - +
- - - add rule + + + add environment variable
-
- - Deploy this application on nodes that respect ALL of the following placement rules. Placement rules are based on node labels. -
-
-
-
- -
-
- -
- -
- - -
-
-
-
-
-

- This label is already defined. -

+
+
+
+
+ name + +
+ +
+ value + +
+ +
+ + +
+
+
+
+
+ +

Environment variable name is required.

+

This field must consist of alphabetic characters, digits, '_', '-', or '.', and must + not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.

+
+

This environment variable is already defined.

+
+
+
+
-
+ + +
+ Configurations +
+ +
+
+ + + add configuration + +
+
+ + Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key via + the override button. +
+
+ + +
+ +
+ +
+
+ + + +
+ +
+
+
+ The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: + + {{ key }}{{ $last ? '' : ', ' }} + +
+
+ + + +
+
+
+
+
+ configuration key + +
+ +
+
+ path on disk + +
+
+ +
+ + +
+
+ +
+
+
+
+
+ +

Path is required.

+
+

This path is already used.

+
+
+
+
+
+
+ +
+ + + +
+ Persisting data +
+ +
+
+ + No storage option is available to persist data, contact your administrator to enable a storage option. +
+
+ +
+
+ + + add persisted folder + +
+ +
+
+
+ path in container + +
+ +
+ + + + +
+ +
+ requested size + + + + +
+ +
+ storage + + +
+ +
+ volume + +
+ +
+
+ + +
+
+
+ +
+
+
+ +

Path is required.

+
+

This path is already defined.

+
+
+ +
+ +
+
+ +

Size is required.

+

This value must be greater than zero.

+
+
+
+ +

Volume is required.

+
+

This volume is already used.

+
+
+ +
+
+
+
+ + + +
- +
- Specify the policy associated to the placement rules. + Specify how the data will be used across instances.
- -
+ +
-
- -
- +
-
-
- Publishing the application -
- -
-
- Select how you want to publish your application. +
+ Resource reservations
-
- - -
-
-
- - - -
- -
- - - -
-
- - - -
-
- - - + +
+
+ + Resource reservations are applied per instance of the application.
-
- - -
-
- - - publish a new port - +
+
+ + A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums are + inherited from the namespace quota. +
+
+
+ + This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the + namespace. +
+
+ + +
+ +
+ +
+
+ +
+
+

+ Maximum memory usage (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.state.sliders.memory.min }} and + {{ ctrl.state.sliders.memory.max }} +

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ +
+
+ + These reservations would exceed the resources currently available in the cluster. +
+
+ + + +
+ Deployment +
+ +
+
+ Select how you want to deploy your application inside the cluster. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+
+
+
+ +

Instance count is required.

+

Instance count must be greater than 0.

+
+
+
+ +
- - When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a port - number inside the default range 30000-32767. -
-
- At least one published port must be defined. +
+ + This application will reserve the following resources: + {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and + {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. +
-
- -
-
- container port - -
+
+
+ + This application would exceed available resources. Please review resource reservations or the instance count. +
+
-
- node port - -
+
+
+ + The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. +
+
+ -
- load balancer port - -
+ +
+ Auto-scaling +
-
- ingress - -
+
+
+ + +
+
-
- hostname - -
+
+
+

+ This feature is currently disabled and must be enabled by an administrator user. +

+

+ Server metrics features must be enabled in the + endpoint configuration view. +

+
+
-
- route - -
+
+ + + + + + + + + + + + + +
Minimum instancesMaximum instances + Target CPU usage (%) + + +
+
+ +
+
+
+ +

Minimum instances is required.

+

Minimum instances must be greater than 0.

+

Minimum instances must be smaller than maximum instances.

+
+
+
+
+
+ +
+
+
+ +

Maximum instances is required.

+

Maximum instances must be greater than minimum instances.

+
+
+
+
+
+ +
+
+
+ +

Target CPU usage is required.

+

Target CPU usage must be greater than 0.

+

Target CPU usage must be smaller than 100.

+
+
+
+
-
-
- - -
- - +
+
+ + This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy.
+
+ + +
+
+ Placement preferences and constraints +
+ + +
+
+ + + add rule + +
+ +
+ + Deploy this application on nodes that respect ALL of the following placement rules. Placement rules are based on node labels. +
+ +
+
+
+ +
+
+ +
+ +
+ + +
+
+
+
+
+

+ This label is already defined. +

+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ Specify the policy associated to the placement rules. +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+ Publishing the application +
+ +
+
+ Select how you want to publish your application. +
+
+ + +
+
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + publish a new port + +
-
-
+ + When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a + port number inside the default range 30000-32767. +
+
+ At least one published port must be defined. +
+ +
+ +
+ container port + +
+ +
-
-

Container port number is required.

-

Container port number must be inside the range 1-65535.

-

Container port number must be inside the range 1-65535.

-
-

- This port is already used. -

+ node port +
-
-
-
-

Node port number must be inside the range 30000-32767.

-

Node port number must be inside the range 30000-32767.

-
-

- This port is already used. -

+ load balancer port +
-
-
-
-
-

Ingress selection is required.

-
-
-
-
-
-

Route is required.

-

This field must consist of alphanumeric characters or the special characters: '-', '_' or - '/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').

ingress + +
+ +
+ hostname + +
+ +
+ route + +
+ +
+
+ +
-

- This route is already used. -

+ +
+ -
-
-
-

Load balancer port number is required.

-

Load balancer port number must be inside the range 1-65535.

-

Load balancer port number must be inside the range 1-65535.

+ +
+
+
+
+

Container port number is required.

+

Container port number must be inside the range 1-65535.

+

Container port number must be inside the range 1-65535.

+
+

+ This port is already used. +

-

- - This port is already used. -

+ +
+
+
+

Node port number must be inside the range 30000-32767.

+

Node port number must be inside the range 30000-32767.

+
+

+ This port is already used. +

+
+
+ +
+
+
+

Ingress selection is required.

+
+
+
+
+
+
+

Route is required.

+

This field must consist of alphanumeric characters or the special characters: '-', '_' or + '/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').

+
+

+ This route is already used. +

+
+
+ +
+
+
+

Load balancer port number is required.

+

Load balancer port number must be inside the range 1-65535.

+

Load balancer port number must be inside the range 1-65535.

+
+

+ + This port is already used. +

+
+
+ +
- -
+
-
+ + + +
- - - - -
+
Actions
-
+ +
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 31ca80f16..064486f04 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -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); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 54a641ca8..5ea71a2e1 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -67,7 +67,10 @@ Creation {{ ctrl.application.ApplicationOwner }} - {{ ctrl.application.CreationDate | getisodate }} + {{ ctrl.application.CreationDate | getisodate }} + + Deployed from {{ ctrl.state.appType }} @@ -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" > Rollback to previous configuration diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index 710571c53..935de40aa 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -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; } diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html index e0ed3d9e9..bf892cfd6 100644 --- a/app/kubernetes/views/deploy/deploy.html +++ b/app/kubernetes/views/deploy/deploy.html @@ -111,7 +111,7 @@ - +
@@ -135,7 +135,7 @@ />
-
+
diff --git a/app/portainer/components/form-components/web-editor-form/index.js b/app/portainer/components/form-components/web-editor-form/index.js index bc1c80a07..ab3c06417 100644 --- a/app/portainer/components/form-components/web-editor-form/index.js +++ b/app/portainer/components/form-components/web-editor-form/index.js @@ -9,7 +9,6 @@ export const webEditorForm = { placeholder: '@', yml: '<', value: '<', - onChange: '<', }, diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js b/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js index 3000869c3..a5fe96be2 100644 --- a/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js @@ -6,6 +6,7 @@ export const gitFormAuthFieldset = { bindings: { model: '<', onChange: '<', + showAuthExplanation: '<', isEdit: '<', }, }; diff --git a/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html b/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html new file mode 100644 index 000000000..ae8c95252 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html @@ -0,0 +1,15 @@ +
+
+

+ This stack was deployed from the git repository {{ $ctrl.url }} + . +

+

+ Update + {{ $ctrl.configFilePath }},{{ $ctrl.additionalFiles.join(',') }} + in git and pull from here to update the stack. +

+
+
diff --git a/app/portainer/components/forms/git-form/git-form-info-panel/index.js b/app/portainer/components/forms/git-form/git-form-info-panel/index.js new file mode 100644 index 000000000..c2529969b --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-info-panel/index.js @@ -0,0 +1,9 @@ +export const gitFormInfoPanel = { + templateUrl: './git-form-info-panel.html', + bindings: { + url: '<', + configFilePath: '<', + additionalFiles: '<', + className: '@', + }, +}; diff --git a/app/portainer/components/forms/git-form/index.js b/app/portainer/components/forms/git-form/index.js index 60eff71e6..e73966464 100644 --- a/app/portainer/components/forms/git-form/index.js +++ b/app/portainer/components/forms/git-form/index.js @@ -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) diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js new file mode 100644 index 000000000..68ab3d1bc --- /dev/null +++ b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js @@ -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; diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html new file mode 100644 index 000000000..88f019f26 --- /dev/null +++ b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html @@ -0,0 +1,59 @@ + +
+ Redeploy from git repository +
+
+
+

+ Pull the latest manifest from git and redeploy the application. +

+
+
+ + + + + +
+ Actions +
+ + + + diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js new file mode 100644 index 000000000..7a21a6384 --- /dev/null +++ b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js @@ -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); diff --git a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js index 6641f25f0..5012b7241 100644 --- a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js @@ -38,7 +38,6 @@ class StackRedeployGitFormController { this.onChangeRef = this.onChangeRef.bind(this); this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this); this.onChangeEnvVar = this.onChangeEnvVar.bind(this); - this.handleEnvVarChange = this.handleEnvVarChange.bind(this); } buildAnalyticsProperties() { @@ -143,10 +142,6 @@ class StackRedeployGitFormController { return this.state.inProgress || this.state.redeployInProgress; } - handleEnvVarChange(value) { - this.formValues.Env = value; - } - isAutoUpdateChanged() { const wasEnabled = !!(this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook)); const isEnabled = this.formValues.AutoUpdate.RepositoryAutomaticUpdates; diff --git a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html index 5d9025438..bb9c6585e 100644 --- a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html @@ -2,21 +2,12 @@
Redeploy from git repository
-
-
-

- This stack was deployed from the git repository {{ $ctrl.model.URL }} - . -

-

- Update - {{ $ctrl.model.ConfigFilePath }},{{ $ctrl.stack.AdditionalFiles.join(',') }} - in git and pull from here to update the stack. -

-
-
+
diff --git a/app/portainer/models/stack.js b/app/portainer/models/stack.js index 41c32f70f..118174d0a 100644 --- a/app/portainer/models/stack.js +++ b/app/portainer/models/stack.js @@ -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); } diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index 86ad1905a..da3a1a545 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -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 = {}; diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js index d01646ef8..93c5ca05d 100644 --- a/app/portainer/views/stacks/create/createStackController.js +++ b/app/portainer/views/stacks/create/createStackController.js @@ -34,7 +34,7 @@ angular StackFile: null, RepositoryURL: '', RepositoryReferenceName: '', - RepositoryAuthentication: false, + RepositoryAuthentication: true, RepositoryUsername: '', RepositoryPassword: '', Env: [], From 8d8f21368d0366b26d18d8befe50bb6709a2350b Mon Sep 17 00:00:00 2001 From: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Date: Wed, 8 Sep 2021 11:06:18 +1200 Subject: [PATCH 8/8] feat(frontend): dark and high contrast theme supported EE-909 (#5353) * feat dark theme & high contrast theme supported --- api/http/handler/users/user_update.go | 5 + api/portainer.go | 2 + app/assets/css/app.css | 134 ++-- app/assets/css/index.js | 2 + app/assets/css/rdash.css | 21 +- app/assets/css/theme.css | 576 ++++++++++++++++++ app/assets/css/vendor-override.css | 399 ++++++++++++ .../containerNetworksDatatable.html | 6 +- .../networks-datatable/networksDatatable.html | 2 +- .../serviceTasksDatatable.html | 6 +- .../applicationsPortsDatatable.html | 12 +- .../applicationsStacksDatatable.html | 10 +- .../placements-datatable/template.html | 28 +- .../ingresses-datatable/template.html | 10 +- app/kubernetes/views/summary/summary.html | 2 +- .../volumes-storages-datatable/template.html | 10 +- .../components/box-selector/box-selector.css | 13 +- .../customTemplatesList.html | 2 +- .../components/datatables/datatable.css | 19 +- app/portainer/components/sidebar/sidebar.css | 20 +- .../template-list/templateList.html | 2 +- .../theme/theme-settings.controller.js | 70 +++ .../components/theme/theme-settings.html | 19 + .../components/theme/theme-settings.js | 7 + app/portainer/models/user.js | 1 + app/portainer/rest/user.js | 1 + app/portainer/services/api/userService.js | 4 + app/portainer/services/authentication.js | 12 +- app/portainer/services/stateManager.js | 5 + app/portainer/services/themeManager.js | 23 + app/portainer/views/account/account.html | 1 + .../views/account/accountController.js | 29 +- .../views/logout/logoutController.js | 5 +- app/portainer/views/main/mainController.js | 1 + 34 files changed, 1352 insertions(+), 107 deletions(-) create mode 100644 app/assets/css/theme.css create mode 100644 app/assets/css/vendor-override.css create mode 100644 app/portainer/components/theme/theme-settings.controller.js create mode 100644 app/portainer/components/theme/theme-settings.html create mode 100644 app/portainer/components/theme/theme-settings.js create mode 100644 app/portainer/services/themeManager.js diff --git a/api/http/handler/users/user_update.go b/api/http/handler/users/user_update.go index fdd7e1a65..02e337f3b 100644 --- a/api/http/handler/users/user_update.go +++ b/api/http/handler/users/user_update.go @@ -17,6 +17,7 @@ import ( type userUpdatePayload struct { Username string `validate:"required" example:"bob"` Password string `validate:"required" example:"cg9Wgky3"` + UserTheme string `example:"dark"` // User role (1 for administrator account and 2 for regular account) Role int `validate:"required" enums:"1,2" example:"2"` } @@ -104,6 +105,10 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http user.Role = portainer.UserRole(payload.Role) } + if payload.UserTheme != "" { + user.UserTheme = payload.UserTheme + } + err = handler.DataStore.User().UpdateUser(user.ID, user) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user changes inside the database", err} diff --git a/api/portainer.go b/api/portainer.go index 87903d4b9..96af4f17e 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1003,6 +1003,8 @@ type ( ID UserID `json:"Id" example:"1"` Username string `json:"Username" example:"bob"` Password string `json:"Password,omitempty" example:"passwd"` + // User Theme + UserTheme string `example:"dark"` // User role (1 for administrator account and 2 for regular account) Role UserRole `json:"Role" example:"1"` diff --git a/app/assets/css/app.css b/app/assets/css/app.css index d88bfc110..74b1788b3 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -59,10 +59,10 @@ body, } .form-section-title { - border-bottom: 1px solid #777; + border-bottom: 1px solid var(--border-form-section-title-color); margin-top: 5px; margin-bottom: 15px; - color: #777; + color: var(--text-form-section-title-color); padding-left: 0; } @@ -108,12 +108,33 @@ a[ng-click] { pointer-events: none; } +.datatable-highlighted { + background-color: var(--bg-item-highlighted-color); +} + +.datatable-unhighlighted { + background-color: var(--bg-item-highlighted-null-color); +} + +.service-datatable { + background-color: var(--bg-item-highlighted-color); + padding: 2px; +} + +.service-datatable thead { + background-color: var(--bg-service-datatable-thead) !important; +} + +.service-datatable tbody { + background-color: var(--bg-service-datatable-tbody); +} + .tooltip.portainer-tooltip .tooltip-inner { font-family: Montserrat; - background-color: #ffffff; + background-color: var(--bg-tooltip-color); padding: 0.833em 1em; - color: #333333; - border: 1px solid #d4d4d5; + color: var(--text-tooltip-color); + border: 1px solid var(--border-tooltip-color); border-radius: 0.14285714rem; box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); } @@ -145,7 +166,7 @@ a[ng-click] { .fa.blue-icon, .fab.blue-icon { - color: #337ab7; + color: var(--blue-2); } .text-warning { @@ -207,25 +228,25 @@ a[ng-click] { padding: 0.7rem; margin-bottom: 0.7rem; cursor: pointer; - border: 1px solid #cccccc; + border: 1px solid var(--border-blocklist-color); border-radius: 2px; - box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5); + box-shadow: var(--shadow-box-color); } .blocklist-item--disabled { cursor: auto; - background-color: #ececec; + background-color: var(--grey-12); } .blocklist-item--selected { - border: 2px solid #bbbbbb; - background-color: #ececec; - color: #2d3e63; + background-color: var(--bg-blocklist-item-selected-color); + border: 2px solid var(--border-blocklist-item-selected-color); + color: var(--text-blocklist-item-selected-color); } .blocklist-item:hover { - background-color: #ececec; - color: #2d3e63; + background-color: var(--bg-blocklist-hover-color); + color: var(--text-blocklist-hover-color); } .blocklist-item-box { @@ -360,7 +381,7 @@ a[ng-click] { .panel-body { padding-top: 30px; - background-color: #ffffff; + background-color: var(--white-color) fff; } .pagination-controls { @@ -443,8 +464,8 @@ a[ng-click] { display: inline-block; padding: 0px 6px; margin-left: 10px; - color: #555555; - background-color: #fff; + color: var(--text-small-select-color); + background-color: var(--bg-small-select-color); background-image: none; border-radius: 4px; font-size: 14px; @@ -462,11 +483,11 @@ a[ng-click] { } .visualizer_container .node { - border: 1px dashed #337ab7; + border: 1px dashed var(--blue-2); background-color: rgb(51, 122, 183); background-color: rgba(51, 122, 183, 0.1); border-radius: 4px; - box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5); + box-shadow: 0 3px 10px -2px var(--grey-50); padding: 15px; margin: 5px; } @@ -476,7 +497,7 @@ a[ng-click] { flex-direction: column; justify-content: center; text-align: center; - border-bottom: 1px solid #777; + border-bottom: 1px solid var(--grey-26); padding-bottom: 10px; } @@ -486,7 +507,7 @@ a[ng-click] { } .visualizer_container .node .node_info .node_labels { - border-top: 1px solid #777; + border-top: 1px solid var(--grey-26); padding-top: 10px; margin-top: 10px; } @@ -503,9 +524,9 @@ a[ng-click] { } .visualizer_container .node .tasks .task { - border: 1px solid #333333; + border: 1px solid var(--grey-6); border-radius: 2px; - box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5); + box-shadow: 0 3px 10px -2px var(--grey-50); padding: 10px; margin: 5px; font-size: 10px; @@ -547,9 +568,9 @@ a[ng-click] { .log_viewer { height: 100%; overflow-y: scroll; - color: black; + color: var(--text-log-viewer-color); font-size: 0.85em; - background-color: white; + background-color: var(--bg-log-viewer-color); } .log_viewer.wrap_lines { @@ -568,7 +589,7 @@ a[ng-click] { } .log_viewer .line_selected { - background-color: #c5cae9; + background-color: var(--bg-log-line-selected-color); } .row.header .meta .page { @@ -578,7 +599,7 @@ a[ng-click] { .tag { padding: 2px 6px; color: white; - background-color: #337ab7; + background-color: var(--blue-2); border: 1px solid #2e6da4; border-radius: 4px; } @@ -588,7 +609,7 @@ a[ng-click] { } .line-separator { - border-bottom: 1px solid #777; + border-bottom: 1px solid var(--grey-26); width: 50%; margin: 20px auto 10px auto; } @@ -613,7 +634,7 @@ a[ng-click] { } .striked::after { - border-bottom: 0.2em solid #777777; + border-bottom: 0.2em solid var(--grey-26); content: ''; left: 0; margin-top: calc(0.2em / 2 * -1); @@ -625,7 +646,7 @@ a[ng-click] { .striketext:before, .striketext:after { - background-color: #777777; + background-color: var(--grey-26); content: ''; display: inline-block; height: 1px; @@ -657,20 +678,51 @@ a[ng-click] { /*angular-multi-select override*/ .multiSelect > button { min-height: 30px !important; - background-color: unset; - background-image: unset; + background-image: var(--bg-image-multiselect-button); + border-color: var(--border-multiselect); + color: var(--text-multiselect); + background-color: var(--bg-multiselect-color); +} + +.multiSelect > button:hover { + background-image: var(--bg-image-multiselect-hover); +} + +.multiSelect .checkboxLayer { + border-color: var(--border-multiselect-checkboxlayer); +} + +.multiSelect .checkBoxContainer { + background-color: var(--bg-multiselect-checkboxcontainer); +} + +.multiSelect .multiSelectItem { + color: var(--text-multiselect-item); +} + +.multiSelect .helperContainer { + background-color: var(--bg-multiselect-helpercontainer); +} + +.multiSelect .multiSelectFocus { + background-image: var(--bg-image-multiselect); } .multiSelect .multiSelectItem:not(.multiSelectGroup).selected { - background-image: linear-gradient(#337ab7, #337ab7); - color: #fff; + background-image: var(--bg-image-multiselect); + color: var(--white-color); border: none; } .multiSelect .multiSelectItem:hover, .multiSelect .multiSelectGroup:hover { - background-image: linear-gradient(#337ab7, #337ab7) !important; - color: #fff !important; + border-color: var(--grey-3); +} + +.multiSelect .multiSelectItem:hover, +.multiSelect .multiSelectGroup:hover { + background-image: var(--bg-image-multiselect) !important; + color: var(--white-color) !important; } .multiSelect .tickMark, @@ -696,7 +748,7 @@ a[ng-click] { #loading-bar .bar { position: relative; height: 3px; - background: #738bc0; + background: var(--blue-3); } /*!angular-loading-bar override*/ @@ -708,11 +760,11 @@ a[ng-click] { /* json-tree override */ json-tree { font-size: 13px; - color: #30426a; + color: var(--blue-5); } json-tree .key { - color: #738bc0; + color: var(--blue-3); padding-right: 5px; } @@ -725,7 +777,7 @@ json-tree .branch-preview { /* uib-progressbar override */ .progress-bar { - color: #4e4e4e; + color: var(--text-progress-bar-color); } /* !uib-progressbar override */ @@ -756,7 +808,7 @@ json-tree .branch-preview { } .sk-fold-cube:before { - background-color: #337ab7; + background-color: var(--blue-2); } /* !spinkit override */ diff --git a/app/assets/css/index.js b/app/assets/css/index.js index c035bf022..b812316ab 100644 --- a/app/assets/css/index.js +++ b/app/assets/css/index.js @@ -1,2 +1,4 @@ import './rdash.css'; import './app.css'; +import './theme.css'; +import './vendor-override.css'; diff --git a/app/assets/css/rdash.css b/app/assets/css/rdash.css index a9dc4ff48..e885c51f3 100644 --- a/app/assets/css/rdash.css +++ b/app/assets/css/rdash.css @@ -52,7 +52,8 @@ * Header */ .row.header { - background: #fff; + height: 60px; + background: var(--bg-row-header-color); margin-bottom: 15px; } .row.header > div:last-child { @@ -202,9 +203,9 @@ html { overflow-y: scroll; } body { - background: #f3f3f3; + background: var(--bg-body-color); font-family: 'Montserrat'; - color: #333333 !important; + color: var(--text-body-color) !important; } .row { margin-left: 0 !important; @@ -236,7 +237,7 @@ body { background: #23ae89 !important; } .blue { - background: #2361ae !important; + background: var(--blue-color) !important; } .orange { background: #d3a938 !important; @@ -258,20 +259,20 @@ div.input-mask { -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); - background: #ffffff; + background: var(--bg-widget-color); border: 1px solid transparent; border-radius: 2px; - border-color: #e9e9e9; + border-color: var(--border-widget-color); } .widget .widget-header .pagination, .widget .widget-footer .pagination { margin: 0; } .widget .widget-header { - color: #767676; - background-color: #f6f6f6; + color: var(--text-widget-header-color); + background-color: var(--bg-widget-header-color); padding: 10px 15px; - border-bottom: 1px solid #e9e9e9; + border-bottom: 1px solid var(--border-widget-color); line-height: 30px; } .widget .widget-header i { @@ -281,7 +282,7 @@ div.input-mask { padding: 20px; } .widget .widget-body table thead { - background: #fafafa; + background: var(--bg-widget-table-color); } .widget .widget-body table thead * { font-size: 14px !important; diff --git a/app/assets/css/theme.css b/app/assets/css/theme.css new file mode 100644 index 000000000..b186f81c6 --- /dev/null +++ b/app/assets/css/theme.css @@ -0,0 +1,576 @@ +/* Color Variable */ +html { + --black-color: #000; + --white-color: #fff; + + --grey-1: #212121; + --grey-2: #181818; + --grey-3: #383838; + --grey-4: #585858; + --grey-5: #323c48; + --grey-6: #333333; + --grey-7: #767676; + --grey-8: #aaa; + --grey-9: #f3f3f3; + --grey-10: #f6f6f6; + --grey-11: #eeeeee; + --grey-12: #ececec; + --grey-13: #fafafa; + --grey-14: #f5f5f5; + --grey-15: #f9f2f4; + --grey-16: #eee; + --grey-17: #f7f7f7; + --grey-18: #c5cae9; + --grey-19: #ddd; + --grey-20: #dae3f3; + --grey-21: #d5e8f3; + --grey-22: #c3c3e4; + --grey-23: #e7f6ff; + --grey-24: #f1f9fd; + --grey-25: #555555; + --grey-26: #777777; + --grey-27: #4e4e4e; + --grey-28: #262626; + --grey-29: #555; + --grey-30: #444; + --grey-31: #868686; + --grey-32: #65798e; + --grey-34: #314252; + --grey-35: #546477; + --grey-36: #55637d; + --grey-37: #2d3e63; + --grey-38: #434343; + --grey-39: #194973; + --grey-40: #cfddfc; + --grey-41: #b4b4b4; + --grey-42: #d2d1d1; + --grey-43: #e9e9e9; + --grey-44: #ccc; + --grey-45: #e5e5e5; + --grey-46: #bbbbbb; + --grey-47: #d4d4d5; + --grey-48: #c6c6c6; + --grey-49: rgba(0, 0, 0, 0.54); + --grey-50: rgba(161, 170, 166, 0.5); + --grey-51: rgba(0, 0, 0, 0.15); + --grey-52: rgba(255, 255, 255, 0.3); + --grey-53: rgba(255, 255, 255, 0.6); + --grey-54: rgb(54, 54, 54); + --grey-55: rgba(255, 255, 255, 0.8); + --grey-56: #b2bfdc; + --grey-57: #999; + --grey-58: #ebf4f8; + --grey-59: #e6e6e6; + --grey-60: #cacaca; + + --blue-1: #219; + --blue-2: #337ab7; + --blue-3: #738bc0; + --blue-4: #23527c; + --blue-5: #30426a; + --blue-6: #577bc9; + --blue-7: #6b9aff; + --blue-8: #90ccff; + --blue-9: #3ea6ff; + --blue-10: #61b6ff; + --blue-11: #3ea5ff; + --blue-12: #41a6ff; + --blue-13: #2361ae; + --blue-14: #357ebd; + + --red-1: #a94442; + --red-2: #c7254e; + --red-3: #a11; + --red-4: #d9534f; + --red-5: #ff2727; + --red-6: #ff00e0; + --red-7: #f00; + + --green-1: #164; + --green-2: #1ec863; +} + +:root { + --bg-card-color: var(--grey-10); + --bg-main-color: var(--white-color); + --bg-body-color: var(--grey-9); + --bg-checkbox-border-color: var(--grey-49); + --bg-sidebar-color: var(--grey-37); + --bg-sidebar-header-color: var(--grey-37); + --bg-widget-color: var(--white-color); + --bg-widget-header-color: var(--grey-10); + --bg-widget-table-color: var(--grey-13); + --bg-header-color: var(--white-color); + --bg-hover-table-color: var(--grey-14); + --bg-switch-box-color: var(--white-color); + --bg-input-group-addon-color: var(--grey-11); + --bg-btn-default-color: var(--white-color); + --bg-blocklist-hover-color: var(--grey-12); + --bg-boxselector-color: var(--white-color); + --bg-table-color: var(--white-color); + --bg-md-checkbox-color: var(--grey-12); + --bg-form-control-disabled-color: var(--grey-11); + --bg-modal-content-color: var(--white-color); + --bg-code-color: var(--grey-15); + --bg-navtabs-color: var(--white-color); + --bg-navtabs-hover-color: var(--grey-16); + --bg-table-selected-color: var(--grey-14); + --bg-codemirror-gutters-color: var(--grey-17); + --bg-dropdown-menu-color: var(--white-color); + --bg-log-viewer-color: var(--white-color); + --bg-log-line-selected-color: var(--grey-18); + --bg-pre-color: var(--grey-14); + --bg-blocklist-item-selected-color: var(--grey-12); + --bg-progress-color: var(--grey-14); + --bg-pagination-color: var(--white-color); + --bg-pagination-span-color: var(--white-color); + --bg-pagination-hover-color: var(--grey-11); + --bg-ui-select-hover-color: var(--grey-14); + --bg-motd-body-color: var(--grey-20); + --bg-item-highlighted-color: var(--grey-21); + --bg-item-highlighted-null-color: var(--grey-14); + --bg-row-header-color: var(--white-color); + --bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17)); + --bg-multiselect-checkbox-color: var(--white-color); + --bg-sidebar-wrapper-color: var(--blue-5); + --bg-panel-body-color: var(--white-color); + --bg-codemirror-color: var(--white-color); + --bg-codemirror-selected-color: var(--grey-22); + --bg-multiselect-color: var(--white-color); + --bg-daterangepicker-color: var(--white-color); + --bg-calendar-color: var(--white-color); + --bg-calendar-table-color: var(--white-color); + --bg-daterangepicker-end-date: var(--white-color); + --bg-daterangepicker-hover: var(--grey-16); + --bg-daterangepicker-in-range: var(--grey-58); + --bg-daterangepicker-active: var(--blue-14); + --bg-tooltip-color: var(--white-color); + --bg-input-autofill-color: var(--white-color); + --bg-btn-default-hover-color: var(--grey-59); + --bg-btn-focus: var(--grey-59); + --bg-boxselector-disabled-color: var(--white-color); + --bg-small-select-color: var(--white-color); + + --text-main-color: var(--grey-7); + --text-body-color: var(--grey-6); + --text-sidebar-title-color: var(--blue-3); + --text-widget-header-color: var(--grey-7); + --text-form-control-color: var(--grey-25); + --text-muted-color: var(--grey-26); + --text-link-color: var(--blue-2); + --text-link-hover-color: var(--blue-4); + --text-input-group-addon-color: var(--grey-25); + --text-btn-default-color: var(--grey-6); + --text-blocklist-hover-color: var(--grey-37); + --text-dashboard-item-color: var(--grey-32); + --text-danger-color: var(--red-1); + --text-code-color: var(--red-2); + --text-navtabs-color: var(--grey-25); + --text-form-section-title-color: var(--grey-26); + --text-cm-default-color: var(--blue-1); + --text-cm-meta-color: var(--black-color); + --text-cm-string-color: var(--red-3); + --text-cm-number-color: var(--green-1); + --text-codemirror-color: var(--black-color); + --text-dropdown-menu-color: var(--grey-6); + --text-log-viewer-color: var(--black-color); + --text-json-tree-color: var(--blue-3); + --text-json-tree-leaf-color: var(--blue-5); + --text-json-tree-branch-preview-color: var(--blue-5); + --text-pre-color: var(--grey-6); + --text-blocklist-item-selected-color: var(--grey-37); + --text-progress-bar-color: var(--grey-27); + --text-pagination-color: var(--grey-26); + --text-pagination-span-color: var(--blue-2); + --text-pagination-span-hover-color: var(--blue-4); + --text-ui-select-color: var(--grey-6); + --text-ui-select-hover-color: var(--grey-28); + --text-summary-color: var(--black-color); + --text-multiselect-button-color: var(--grey-29); + --text-multiselect-item-color: var(--grey-30); + --text-sidebar-list-color: var(--grey-56); + --text-rzslider-color: var(--grey-36); + --text-rzslider-limit-color: var(--grey-36); + --text-daterangepicker-end-date: var(--grey-57); + --text-daterangepicker-in-range: var(--black-color); + --text-daterangepicker-active: var(--white-color); + --text-tooltip-color: var(--grey-6); + --text-input-autofill-color: var(--black-color); + --text-button-hover-color: var(--grey-6); + --text-small-select-color: var(--grey-25); + + --border-color: var(--grey-42); + --border-widget-color: var(--grey-43); + --border-sidebar-color: var(--white-color); + --border-form-control-color: var(--grey-44); + --border-table-color: var(--grey-19); + --border-table-top-color: var(--grey-19); + --border-datatable-top-color: var(--grey-10); + --border-blocklist-color: var(--grey-44) ccc; + --border-input-group-addon-color: var(--grey-44); + --border-btn-default-color: var(--grey-44); + --border-boxselector-color: var(--grey-6); + --border-md-checkbox-color: var(--grey-19); + --border-modal-header-color: var(--grey-45); + --border-navtabs-color: var(--grey-19); + --border-form-section-title-color: var(--grey-26); + --border-codemirror-cursor-color: var(--black-color); + --border-codemirror-gutters-color: var(--grey-19); + --border-pre-color: var(--grey-43); + --border-blocklist-item-selected-color: var(--grey-46); + --border-pagination-color: var(--grey-19); + --border-pagination-span-color: var(--grey-19); + --border-pagination-hover-color: var(--grey-19); + --border-multiselect-button-color: var(--grey-48); + --border-searchbar-color: var(--grey-10); + --border-panel-color: var(--white-color); + --border-daterangepicker-color: var(--grey-19); + --border-calendar-table: var(--white-color); + --border-daterangepicker: var(--grey-19); + --border-pre-next-month: var(--black-color); + --border-daterangepicker-after: var(--white-color); + --border-tooltip-color: var(--grey-47); + --border-modal: 0px; + + --hover-sidebar-color: var(--grey-37); + --shadow-box-color: 0 3px 10px -2px var(--grey-50); + --shadow-boxselector-color: 0 3px 10px -2px var(--grey-50); + --blue-color: var(--blue-13); + --button-close-color: var(--black-color); + --button-opacity: 0.2; + --button-opacity-hover: 0.5; + --bg-boxselector-wrapper-color: var(--grey-6); + + --bg-image-multiselect: linear-gradient(var(--blue-2), var(--blue-2)); + --bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17)); + --bg-image-multiselect-hover: linear-gradient(var(--white-color), var(--grey-43)); + --border-multiselect: var(--grey-48); + --border-multiselect-checkboxlayer: var(--grey-51); + --text-multiselect: var(--grey-29); + --text-multiselect-selectitem: var(--white-color); + --bg-multiselect-checkboxcontainer: var(--white-color); + --text-multiselect-item: var(--grey-30); + --bg-multiselect-helpercontainer: var(--white-color); + --text-input-textarea: var(--white-color); + --bg-service-datatable-thead: var(--grey-23); + --bg-service-datatable-tbody: var(--grey-24); +} + +:root[theme='dark'] { + --bg-card-color: var(--grey-1); + --bg-main-color: var(--grey-2); + --bg-body-color: var(--grey-2); + --bg-checkbox-border-color: var(--grey-8); + --bg-sidebar-color: var(--grey-3); + --bg-widget-color: var(--grey-1); + --bg-widget-header-color: var(--grey-1); + --bg-widget-table-color: var(--grey-1); + --bg-header-color: var(--grey-2); + --bg-hover-table-color: var(--grey-3); + --bg-switch-box-color: var(--grey-53); + --bg-input-group-addon-color: var(--grey-3); + --bg-btn-default-color: var(--grey-3); + --bg-blocklist-hover-color: var(--grey-3); + --bg-boxselector-color: var(--grey-54); + --bg-table-color: var(--grey-1); + --bg-md-checkbox-color: var(--grey-31); + --bg-form-control-disabled-color: var(--grey-3); + --bg-modal-content-color: var(--grey-1); + --bg-code-color: var(--red-4); + --bg-navtabs-color: var(--grey-3); + --bg-navtabs-hover-color: var(--grey-3); + --bg-table-selected-color: var(--grey-3); + --bg-codemirror-color: var(--grey-2); + --bg-codemirror-gutters-color: var(--grey-2); + --bg-dropdown-menu-color: var(--grey-1); + --bg-log-viewer-color: var(--grey-2); + --bg-log-line-selected-color: var(--grey-3); + --bg-pre-color: var(--grey-2); + --bg-blocklist-item-selected-color: var(--grey-3); + --bg-progress-color: var(--grey-3); + --bg-pagination-color: var(--grey-3); + --bg-pagination-span-color: var(--grey-3); + --bg-pagination-hover-color: var(--grey-4); + --bg-ui-select-hover-color: var(--grey-3); + --bg-motd-body-color: var(--grey-1); + --bg-item-highlighted-color: var(--grey-2); + --bg-item-highlighted-null-color: var(--grey-2); + --bg-row-header-color: var(--grey-2); + --bg-multiselect-button-color: var(--grey-3); + --bg-image-multiselect-button: none !important; + --bg-multiselect-checkbox-color: var(--grey-3); + --bg-sidebar-wrapper-color: var(--grey-1); + --bg-panel-body-color: var(--grey-1); + --bg-boxselector-wrapper-disabled-color: var(--grey-39); + --bg-codemirror-selected-color: var(--grey-3); + --bg-sidebar-header-color: var(--grey-1); + --bg-multiselect-color: var(--grey-1); + --bg-daterangepicker-color: var(--grey-3); + --bg-calendar-color: var(--grey-3); + --bg-calendar-table-color: var(--grey-3); + --bg-daterangepicker-end-date: var(--grey-4); + --bg-daterangepicker-hover: var(--grey-4); + --bg-daterangepicker-in-range: var(--grey-2); + --bg-daterangepicker-active: var(--blue-14); + --bg-tooltip-color: var(--grey-3); + --bg-input-autofill-color: var(--grey-2); + --bg-btn-default-hover-color: var(--grey-3); + --bg-btn-focus: var(--grey-3); + --bg-boxselector-disabled-color: var(--grey-54); + --bg-small-select-color: var(--grey-2); + + --text-main-color: var(--white-color); + --text-body-color: var(--white-color); + --text-sidebar-title-color: var(--grey-8); + --text-widget-header-color: var(--white-color); + --text-form-control-color: var(--grey-8); + --text-muted-color: var(--grey-8); + --text-link-color: var(--blue-9); + --text-link-hover-color: var(--blue-2); + --text-input-group-addon-color: var(--grey-8); + --text-btn-default-color: var(--grey-8); + --text-blocklist-hover-color: var(--white-color); + --text-dashboard-item-color: var(--blue-2); + --text-danger-color: var(--red-4); + --text-code-color: var(--white-color); + --text-navtabs-color: var(--white-color); + --text-form-section-title-color: var(--grey-8); + --text-cm-default-color: var(--blue-10); + --text-cm-meta-color: var(--white-color); + --text-cm-string-color: var(--red-5); + --text-cm-number-color: var(--green-2); + --text-codemirror-color: var(--white-color); + --text-dropdown-menu-color: var(--white-color); + --text-log-viewer-color: var(--white-color); + --text-json-tree-color: var(--grey-40); + --text-json-tree-leaf-color: var(--blue-6); + --text-json-tree-branch-preview-color: var(--blue-7); + --text-pre-color: var(--white-color); + --text-blocklist-item-selected-color: var(--white-color); + --text-progress-bar-color: var(--white-color); + --text-pagination-color: var(--white-color); + --text-pagination-span-color: var(--white-color); + --text-pagination-span-hover-color: var(--white-color); + --text-ui-select-color: var(--white-color); + --text-ui-select-hover-color: var(--white-color); + --text-summary-color: var(--white-color); + --text-multiselect-button-color: var(--white-color); + --text-multiselect-item-color: var(--white-color); + --text-sidebar-list-color: var(--white-color); + --text-boxselector-wrapper-color: var(--white-color); + --text-daterangepicker-end-date: var(--grey-7); + --text-daterangepicker-in-range: var(--white-color); + --text-daterangepicker-active: var(--white-color); + --text-tooltip-color: var(--white-color); + --text-btn-default-color: var(--white-color); + --text-input-autofill-color: var(--grey-8); + --text-button-hover-color: var(--white-color); + --text-small-select-color: var(--grey-7); + + --border-color: var(--grey-3); + --border-widget-color: var(--grey-1); + --border-sidebar-color: var(--blue-9); + --border-form-control-color: var(--grey-54); + --border-table-color: var(--grey-3); + --border-table-top-color: var(--grey-3); + --border-datatable-top-color: var(--grey-3); + --border-blocklist-color: var(--grey-3); + --border-input-group-addon-color: var(--grey-38); + --border-btn-default-color: var(--grey-38); + --border-boxselector-color: var(--grey-1); + --border-md-checkbox-color: var(--grey-41); + --border-modal-header-color: var(--grey-1); + --border-navtabs-color: var(--grey-38); + --border-form-section-title-color: var(--grey-8); + --border-codemirror-cursor-color: var(--white-color); + --border-codemirror-gutters-color: var(--grey-26); + --border-pre-color: var(--grey-3); + --border-blocklist-item-selected-color: var(--grey-38); + --border-pagination-color: var(--grey-3); + --border-pagination-span-color: var(--grey-3); + --border-pagination-hover-color: var(--grey-3); + --border-pagination-hover-color: var(--grey-3); + --border-multiselect-button-color: var(--grey-3); + --border-searchbar-color: var(--grey-1); + --border-panel-color: var(--grey-2); + --border-daterangepicker-color: var(--grey-3); + --border-calendar-table: var(--grey-3); + --border-daterangepicker: var(--grey-4); + --border-pre-next-month: var(--white-color); + --border-daterangepicker-after: var(--grey-3); + --border-tooltip-color: var(--grey-3); + --border-modal: 0px; + + --hover-sidebar-color: var(--grey-3); + --blue-color: var(--blue-2); + --button-close-color: var(--white-color); + --button-opacity: 0.6; + --button-opacity-hover: 0.3; + --shadow-box-color: none; + --shadow-boxselector-color: none; + + --bg-image-multiselect: linear-gradient(var(--grey-38), var(--grey-38)); + --bg-image-multiselect-button: linear-gradient(var(--grey-1), var(--grey-1)); + --bg-image-multiselect-hover: linear-gradient(var(--grey-3), var(--grey-3)); + --border-multiselect: var(--grey-3); + --border-multiselect-checkboxlayer: var(--grey-3); + --text-multiselect: var(--white-color); + --bg-multiselect-checkboxcontainer: var(--grey-3); + --text-multiselect-item: var(--white-color); + --bg-multiselect-helpercontainer: var(--grey-1); + --text-input-textarea: var(--grey-1); + --bg-service-datatable-thead: var(--grey-1); + --bg-service-datatable-tbody: var(--grey-1); +} + +:root[theme='highcontrast'] { + --bg-card-color: var(--black-color); + --bg-main-color: var(--black-color); + --bg-body-color: var(--black-color); + --bg-checkbox-border-color: var(--grey-8); + --bg-sidebar-color: var(--black-color); + --bg-widget-color: var(--black-color); + --bg-widget-header-color: var(--black-color); + --bg-widget-table-color: var(--black-color); + --bg-header-color: var(--black-color); + --bg-hover-table-color: var(--grey-3); + --bg-switch-box-color: var(--grey-53); + --bg-panel-body-color: var(--black-color); + --bg-boxselector-wrapper-disabled-color: var(--grey-39); + --bg-dropdown-menu-color: var(--black-color); + --bg-codemirror-selected-color: var(--grey-3); + --bg-row-header-color: var(--black-color); + --bg-sidebar-wrapper-color: var(--black-color); + --bg-motd-body-color: var(--black-color); + --bg-blocklist-hover-color: var(--black-color); + --bg-blocklist-item-selected-color: var(--black-color); + --bg-input-group-addon-color: var(--grey-1); + --bg-table-color: var(--black-color); + --bg-codemirror-gutters-color: var(--black-color); + --bg-codemirror-color: var(--black-color); + --bg-codemirror-selected-color: var(--grey-3); + --bg-log-viewer-color: var(--black-color); + --bg-log-line-selected-color: var(--grey-3); + --bg-sidebar-header-color: var(--black-color); + --bg-modal-content-color: var(--black-color); + --bg-form-control-disabled-color: var(--grey-1); + --bg-input-sm-color: var(--black-color); + --bg-item-highlighted-color: var(--black-color); + --bg-service-datatable-thead: var(--black-color); + --bg-service-datatable-tbody: var(--black-color); + --bg-pagination-color: var(--grey-3); + --bg-pagination-span-color: var(--grey-3); + --bg-multiselect-color: var(--grey-1); + --bg-daterangepicker-color: var(--black-color); + --bg-calendar-color: var(--black-color); + --bg-calendar-table-color: var(--black-color); + --bg-daterangepicker-end-date: var(--grey-3); + --bg-daterangepicker-hover: var(--grey-3); + --bg-daterangepicker-in-range: var(--grey-2); + --bg-daterangepicker-active: var(--blue-14); + --bg-tooltip-color: var(--black-color); + --bg-table-selected-color: var(--grey-3); + --bg-pre-color: var(--grey-2); + --bg-navtabs-hover-color: var(--grey-3); + --bg-btn-default-color: var(--black-color); + --bg-code-color: var(--red-4); + --bg-navtabs-color: var(--black-color); + --bg-input-autofill-color: var(--black-color); + --bg-code-color: var(--grey-2); + --bg-navtabs-color: var(--grey-2); + --bg-navtabs-hover-color: var(--grey-3); + --bg-btn-default-hover-color: var(--grey-3); + --bg-btn-default-color: var(--black-color); + --bg-btn-focus: var(--black-color); + --bg-boxselector-color: var(--black-color); + --bg-boxselector-disabled-color: var(--black-color); + --bg-small-select-color: var(--black-color); + + --text-main-color: var(--white-color); + --text-body-color: var(--white-color); + --text-sidebar-title-color: var(--grey-8); + --text-widget-header-color: var(--white-color); + --text-link-color: var(--blue-9); + --text-link-hover-color: var(--blue-9); + --text-danger-color: var(--red-7); + --text-code-color: var(--red-7); + --text-form-control-color: var(--white-color); + --text-blocklist-hover-color: var(--blue-11); + --text-boxselector-wrapper-color: var(--white-color); + --text-dashboard-item-color: var(--blue-12); + --text-form-section-title-color: var(--white-color); + --text-muted-color: var(--white-color); + --text-tooltip-color: var(--white-color); + --text-blocklist-item-selected-color: var(--blue-9); + --text-input-group-addon-color: var(--white-color); + --text-codemirror-color: var(--white-color); + --text-log-viewer-color: var(--white-color); + --text-summary-color: var(--white-color); + --text-rzslider-color: var(--white-color); + --text-rzslider-limit-color: var(--white-color); + --text-pagination-color: var(--white-color); + --text-daterangepicker-end-date: var(--grey-7); + --text-daterangepicker-in-range: var(--white-color); + --text-daterangepicker-active: var(--white-color); + --text-sidebar-list-color: var(--white-color); + --text-ui-select-color: var(--white-color); + --text-btn-default-color: var(--white-color); + --text-json-tree-color: var(--white-color); + --text-json-tree-leaf-color: var(--white-color); + --text-json-tree-branch-preview-color: var(--white-color); + --text-pre-color: var(--white-color); + --text-navtabs-color: var(--white-color); + --text-input-autofill-color: var(--white-color); + --text-navtabs-color: var(--white-color); + --text-button-hover-color: var(--white-color); + --text-btn-default-color: var(--white-color); + --text-small-select-color: var(--white-color); + + --border-color: var(--grey-55); + --border-widget-color: var(--white-color); + --border-sidebar-color: var(--blue-9); + --border-form-control-color: var(--grey-54); + --border-table-color: var(--grey-55); + --border-table-top-color: var(--grey-55); + --border-datatable-top-color: var(--grey-55); + --border-sidebar-high-contrast: 1px solid var(--blue-9); + --border-code-high-contrast: 1px solid var(--white-color); + --border-boxselector-wrapper: 3px solid var(--blue-2); + --border-boxselector-wrapper-hover: 3px solid var(--blue-8); + --border-panel-color: var(--white-color); + --border-input-group-addon-color: var(--grey-54); + --border-modal-header-color: var(--grey-3); + --border-input-sm-color: var(--white-color); + --border-pagination-color: var(--grey-3); + --border-pagination-span-color: var(--grey-3); + --border-daterangepicker-color: var(--white-color); + --border-calendar-table: var(--black-color); + --border-daterangepicker: var(--black-color); + --border-pre-next-month: var(--white-color); + --border-daterangepicker-after: var(--black-color); + --border-tooltip-color: var(--white-color); + --border-pre-color: var(--grey-3); + --border-codemirror-cursor-color: var(--white-color); + --border-modal: 1px solid var(--white-color); + + --hover-sidebar-color: var(--blue-9); + --hover-sidebar-color: var(--black-color); + --shadow-box-color: none; + --shadow-boxselector-color: none; + + --bg-image-multiselect: linear-gradient(var(--black-color), var(--black-color)); + --bg-image-multiselect-button: linear-gradient(var(--grey-1), var(--grey-1)); + --bg-image-multiselect-hover: linear-gradient(var(--grey-3), var(--grey-3)); + --border-multiselect: var(--black-color); + --border-multiselect-checkboxlayer: var(--grey-3); + --text-multiselect: var(--white-color); + --bg-multiselect-checkboxcontainer: var(--grey-3); + --text-multiselect-item: var(--white-color); + --bg-multiselect-helpercontainer: var(--grey-1); + --text-input-textarea: var(--black-color); + --bg-item-highlighted-null-color: var(--grey-2); + --text-cm-default-color: var(--blue-9); + --text-cm-meta-color: var(--white-color); + --text-cm-string-color: var(--red-7); + --text-progress-bar-color: var(--black-color); +} diff --git a/app/assets/css/vendor-override.css b/app/assets/css/vendor-override.css new file mode 100644 index 000000000..21572a7dd --- /dev/null +++ b/app/assets/css/vendor-override.css @@ -0,0 +1,399 @@ +/* Overide Vendor CSS */ +.form-control { + background-color: var(--bg-main-color) !important; + border: 1px solid var(--border-form-control-color); + color: var(--text-form-control-color); +} + +.text-muted { + color: var(--text-muted-color); +} + +.table > thead > tr > th { + border-bottom: 2px solid var(--border-table-color); +} + +.table-hover > tbody > tr:hover { + background-color: var(--bg-hover-table-color); +} + +.switch i, +.bootbox-form .checkbox i { + background: var(--bg-switch-box-color); +} + +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + border-top: 1px solid var(--border-table-top-color); +} + +a { + color: var(--text-link-color); +} + +a:hover, +a:focus { + color: var(--text-link-hover-color); +} + +.input-group-addon { + color: var(--text-input-group-addon-color); + background-color: var(--bg-input-group-addon-color); + border: 1px solid var(--border-input-group-addon-color); +} + +.btn-default { + color: var(--text-btn-default-color); + background-color: var(--bg-btn-default-color); + border-color: var(--border-btn-default-color); +} + +.text-danger { + color: var(--text-danger-color); +} + +.table .table { + background-color: var(--bg-table-color); +} + +.table-bordered { + border-color: var(--border-table-top-color); +} + +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border-color: var(--border-table-top-color); +} + +.md-checkbox input[type='checkbox']:disabled + label:before { + background: var(--bg-md-checkbox-color) !important; + border-color: var(--border-md-checkbox-color) !important; +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: var(--bg-form-control-disabled-color) !important; +} + +.modal.in .modal-dialog { + border: var(--border-modal); +} + +.modal-content { + background-color: var(--bg-modal-content-color); +} + +.modal-header { + border-bottom: 1px solid var(--border-modal-header-color); +} + +.modal-footer { + border-top: 1px solid var(--border-modal-header-color); +} + +.close { + color: var(--button-close-color); + opacity: var(--button-opacity); +} + +.close:hover, +.close:focus { + color: var(--button-close-color); + opacity: var(--button-opacity-hover); +} + +code { + color: var(--text-code-color); + background-color: var(--bg-code-color); +} + +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: var(--text-navtabs-color); + background-color: var(--bg-navtabs-color); + border: 1px solid var(--border-navtabs-color); +} + +.nav-tabs { + border-bottom: 1px solid var(--border-navtabs-color); +} + +.nav-tabs > li > a:hover { + border-color: var(--border-navtabs-color); +} + +.nav > li > a:hover, +.nav > li > a:focus { + background-color: var(--bg-navtabs-hover-color); +} + +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: var(--bg-table-selected-color); +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: var(--bg-table-selected-color); +} + +.CodeMirror-gutters { + background: var(--bg-codemirror-gutters-color); + border-right: 1px solid var(--border-codemirror-gutters-color); +} + +.CodeMirror { + background: var(--bg-codemirror-color); + color: var(--text-codemirror-color); +} + +.CodeMirror-selected { + background: var(--bg-codemirror-selected-color) !important; +} + +.CodeMirror-cursor { + border-left: 1px solid var(--border-codemirror-cursor-color); +} + +.cm-s-default .cm-atom { + color: var(--text-cm-default-color); +} + +.cm-s-default .cm-meta { + color: var(--text-cm-meta-color); +} + +.cm-s-default .cm-string { + color: var(--text-cm-string-color); +} + +.cm-s-default .cm-number { + color: var(--text-cm-number-color); +} + +.dropdown-menu { + background: var(--bg-dropdown-menu-color); +} + +.dropdown-menu > li > a { + color: var(--text-dropdown-menu-color); +} + +pre { + border: 1px solid var(--border-pre-color); + background-color: var(--bg-pre-color); + color: var(--text-pre-color); +} +json-tree .key { + color: var(--text-json-tree-color); +} + +json-tree .leaf-value { + color: var(--text-json-tree-leaf-color); +} + +json-tree .branch-preview { + color: var(--text-json-tree-branch-preview-color); +} + +.progress { + background-color: var(--bg-progress-color); +} + +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: var(--text-pagination-color); + background-color: var(--bg-pagination-color); + border-color: var(--border-pagination-color); +} + +.pagination > li > a, +.pagination > li > span { + background-color: var(--bg-pagination-span-color); + border-color: var(--border-pagination-span-color); + color: var(--text-pagination-span-color); +} + +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + background-color: var(--bg-pagination-hover-color); + border-color: var(--border-pagination-hover-color); +} + +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: var(--text-pagination-span-hover-color); +} + +.ui-select-bootstrap .ui-select-choices-row > span { + color: var(--text-ui-select-color); +} + +.ui-select-bootstrap .ui-select-choices-row > span:hover, +.ui-select-bootstrap .ui-select-choices-row > span:focus { + background-color: var(--bg-ui-select-hover-color); + color: var(--text-ui-select-hover-color); +} + +.motd-body { + background-color: var(--bg-motd-body-color) !important; +} + +.panel-body { + background-color: var(--bg-panel-body-color) !important; +} + +.panel { + border: 1px solid var(--border-panel-color); +} + +.theme-information .col-sm-12 { + padding-left: 0px; + padding-right: 0px; + margin-top: 15px; +} + +.theme-panel { + margin-top: 15px; +} + +.summary { + color: var(--text-summary-color); + font-weight: 700; +} + +.input-sm { + background-color: var(--bg-input-sm-color); + border: 1px solid var(--border-input-sm-color); +} + +.rzslider .rz-bubble { + color: var(--text-rzslider-color); +} + +.rzslider .rz-bubble.rz-limit { + color: var(--text-rzslider-limit-color); +} +input, +button, +select, +textarea { + background: var(--text-input-textarea); +} + +.daterangepicker { + background-color: var(--bg-daterangepicker-color); + border: 1px solid var(--border-daterangepicker-color); +} + +.daterangepicker .drp-calendar.left { + background: var(--bg-calendar-color); +} + +.daterangepicker .drp-calendar.left .calendar-table { + background: var(--bg-calendar-table-color); +} + +.daterangepicker .drp-calendar.right { + background: var(--bg-calendar-color); +} + +.daterangepicker .drp-calendar.right .calendar-table { + background: var(--bg-calendar-table-color); +} + +.daterangepicker .calendar-table { + border: 1px solid var(--border-calendar-table); +} + +.daterangepicker td.off, +.daterangepicker td.off.in-range, +.daterangepicker td.off.start-date, +.daterangepicker td.off.end-date { + background-color: var(--bg-daterangepicker-end-date); + color: var(--text-daterangepicker-end-date); +} + +.daterangepicker td.available:hover, +.daterangepicker th.available:hover { + background-color: var(--bg-daterangepicker-hover); +} + +.daterangepicker td.in-range { + background-color: var(--bg-daterangepicker-in-range); + color: var(--text-daterangepicker-in-range); +} + +.daterangepicker td.active, +.daterangepicker td.active:hover { + background-color: var(--bg-daterangepicker-active); + color: var(--text-daterangepicker-active); +} + +.daterangepicker .drp-buttons { + border-top: 1px solid var(--border-daterangepicker); +} + +.daterangepicker .calendar-table .next span, +.daterangepicker .calendar-table .prev span { + border-color: var(--border-pre-next-month); +} + +.daterangepicker:after { + border-bottom: 6px solid var(--border-daterangepicker-after); +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px var(--bg-input-autofill-color) inset !important; + box-shadow: 0 0 0 30px var(--bg-input-autofill-color) inset !important; +} + +input:-webkit-autofill { + -webkit-text-fill-color: var(--text-input-autofill-color) !important; +} + +.btn:hover { + color: var(--text-button-hover-color); +} + +.btn-default:hover { + background-color: var(--bg-btn-default-hover-color); +} + +.btn-primary:hover { + color: var(--white-color) !important; +} +/* Overide Vendor CSS */ diff --git a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html index 77ce34153..f1a81acaf 100644 --- a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html +++ b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html @@ -73,7 +73,11 @@ - + {{ value.GlobalIPv6Address }} diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.html b/app/docker/components/datatables/networks-datatable/networksDatatable.html index bf54ca837..22aeb605f 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.html +++ b/app/docker/components/datatables/networks-datatable/networksDatatable.html @@ -171,7 +171,7 @@ allow-checkbox="true" > - + Loading... diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html index 285f2263e..521e324a9 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html @@ -1,6 +1,6 @@ -
+
- + - + diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html index 814ff9ebf..ec593a0a0 100644 --- a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html @@ -105,8 +105,7 @@ @@ -177,7 +176,7 @@ - + @@ -190,7 +189,12 @@ - + diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html index d27aa586a..324a75644 100644 --- a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html @@ -109,8 +109,7 @@ @@ -141,7 +140,12 @@ Logs - + @@ -92,14 +91,23 @@ - + - + @@ -110,7 +118,7 @@ ng-if="$ctrl.isAdmin" ng-show="item.Expanded" ng-repeat="label in item.UnmatchedNodeSelectorLabels" - ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }" + ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }" > - + @@ -139,7 +151,7 @@ ng-if="$ctrl.isAdmin" ng-show="item.Expanded" ng-repeat="aff in item.UnmatchedNodeAffinities" - ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }" + ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }" > @@ -84,7 +83,12 @@ >{{ item.Name }} - + @@ -95,7 +94,12 @@ - +
@@ -54,7 +54,7 @@
- -{{ port.TargetPort }}/{{ port.Protocol }} -
- -
{{ app.Name }} diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/template.html b/app/kubernetes/views/applications/edit/components/placements-datatable/template.html index 50bd58670..9622f2225 100644 --- a/app/kubernetes/views/applications/edit/components/placements-datatable/template.html +++ b/app/kubernetes/views/applications/edit/components/placements-datatable/template.html @@ -75,8 +75,7 @@
This application is missing a toleration for the taint {{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.Effect }}
Placement constraint not respected for that node. This application can only be scheduled on a node where the label {{ label.key }} is set to {{ label.value }} @@ -121,7 +129,7 @@
Placement label not respected for that node. @@ -129,7 +137,11 @@
This application can only be scheduled on nodes respecting one of the following labels combination: diff --git a/app/kubernetes/views/resource-pools/edit/components/ingresses-datatable/template.html b/app/kubernetes/views/resource-pools/edit/components/ingresses-datatable/template.html index ef78e3734..27a359130 100644 --- a/app/kubernetes/views/resource-pools/edit/components/ingresses-datatable/template.html +++ b/app/kubernetes/views/resource-pools/edit/components/ingresses-datatable/template.html @@ -73,8 +73,7 @@
{{ path.Host ? path.Host : path.IP }}{{ path.Path }} diff --git a/app/kubernetes/views/summary/summary.html b/app/kubernetes/views/summary/summary.html index 8ab3e4f16..cb62834f9 100644 --- a/app/kubernetes/views/summary/summary.html +++ b/app/kubernetes/views/summary/summary.html @@ -22,7 +22,7 @@
  • {{ summary.action }} {{ $ctrl.getArticle(summary.kind, summary.action) }} - {{ summary.kind }} named {{ summary.name }} + {{ summary.kind }} named {{ summary.name }} of type {{ summary.type }} diff --git a/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html b/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html index b0130ab5c..b912d3f41 100644 --- a/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html +++ b/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html @@ -82,8 +82,7 @@
  • {{ item.Name }} {{ item.Size }}
    diff --git a/app/portainer/components/box-selector/box-selector.css b/app/portainer/components/box-selector/box-selector.css index b359cb4df..97091a4fa 100644 --- a/app/portainer/components/box-selector/box-selector.css +++ b/app/portainer/components/box-selector/box-selector.css @@ -27,22 +27,27 @@ .boxselector_wrapper input[type='radio']:not(:disabled) ~ label { cursor: pointer; + background-color: var(--bg-boxselector-wrapper-disabled-color); +} + +.boxselector_wrapper input[type='radio']:not(:disabled):hover ~ label:hover { + cursor: pointer; } .boxselector_wrapper label { font-weight: normal; font-size: 12px; display: block; - background: white; - border: 1px solid #333333; + background: var(--bg-boxselector-color); + border: 1px solid var(--border-boxselector-color); border-radius: 2px; padding: 10px 10px 0 10px; text-align: center; - box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5); + box-shadow: var(--shadow-boxselector-color); position: relative; } .boxselector_wrapper label.boxselector_disabled { - background: #cacaca; + background: var(--bg-boxselector-disabled-color) !important; border-color: #787878; color: #787878; cursor: not-allowed; diff --git a/app/portainer/components/custom-templates-list/customTemplatesList.html b/app/portainer/components/custom-templates-list/customTemplatesList.html index 5d7c9228b..73b96911f 100644 --- a/app/portainer/components/custom-templates-list/customTemplatesList.html +++ b/app/portainer/components/custom-templates-list/customTemplatesList.html @@ -8,7 +8,7 @@ -