From 693f1319a471760b784b9f372513815781411031 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 1 Nov 2017 10:30:02 +0100 Subject: [PATCH] feat(stacks): add the ability to specify env vars when deploying stacks (#1345) --- api/exec/stack_manager.go | 24 ++++++++++---- api/http/handler/stack.go | 31 +++++++++++++++---- api/portainer.go | 1 + .../createStack/createStackController.js | 20 +++++++++--- app/components/createStack/createstack.html | 30 ++++++++++++++++++ app/components/stack/stack.html | 30 ++++++++++++++++++ app/components/stack/stackController.js | 15 +++++++-- app/helpers/formHelper.js | 18 +++++++++++ app/models/api/stack.js | 1 + app/services/api/stackService.js | 29 ++++++++++++----- app/services/fileUpload.js | 4 +-- 11 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 app/helpers/formHelper.js diff --git a/api/exec/stack_manager.go b/api/exec/stack_manager.go index 61051f01c..7e418d70f 100644 --- a/api/exec/stack_manager.go +++ b/api/exec/stack_manager.go @@ -2,6 +2,7 @@ package exec import ( "bytes" + "os" "os/exec" "path" "runtime" @@ -27,7 +28,7 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries [] for _, registry := range registries { if registry.Authentication { registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL) - err := runCommandAndCaptureStdErr(command, registryArgs) + err := runCommandAndCaptureStdErr(command, registryArgs, nil) if err != nil { return err } @@ -36,7 +37,7 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries [] if dockerhub.Authentication { dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password) - err := runCommandAndCaptureStdErr(command, dockerhubArgs) + err := runCommandAndCaptureStdErr(command, dockerhubArgs, nil) if err != nil { return err } @@ -49,7 +50,7 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries [] func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error { command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint) args = append(args, "logout") - return runCommandAndCaptureStdErr(command, args) + return runCommandAndCaptureStdErr(command, args, nil) } // Deploy executes the docker stack deploy command. @@ -57,21 +58,32 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer. stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint) command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint) args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name) - return runCommandAndCaptureStdErr(command, args) + + env := make([]string, 0) + for _, envvar := range stack.Env { + env = append(env, envvar.Name+"="+envvar.Value) + } + + return runCommandAndCaptureStdErr(command, args, env) } // Remove executes the docker stack rm command. func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error { command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint) args = append(args, "stack", "rm", stack.Name) - return runCommandAndCaptureStdErr(command, args) + return runCommandAndCaptureStdErr(command, args, nil) } -func runCommandAndCaptureStdErr(command string, args []string) error { +func runCommandAndCaptureStdErr(command string, args []string, env []string) error { var stderr bytes.Buffer cmd := exec.Command(command, args...) cmd.Stderr = &stderr + if env != nil { + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, env...) + } + err := cmd.Run() if err != nil { return portainer.Error(stderr.String()) diff --git a/api/http/handler/stack.go b/api/http/handler/stack.go index f90f68928..1f6b23d6e 100644 --- a/api/http/handler/stack.go +++ b/api/http/handler/stack.go @@ -62,11 +62,12 @@ func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler { type ( postStacksRequest struct { - Name string `valid:"required"` - SwarmID string `valid:"required"` - StackFileContent string `valid:""` - GitRepository string `valid:""` - PathInRepository string `valid:""` + Name string `valid:"required"` + SwarmID string `valid:"required"` + StackFileContent string `valid:""` + GitRepository string `valid:""` + PathInRepository string `valid:""` + Env []portainer.Pair `valid:""` } postStacksResponse struct { ID string `json:"Id"` @@ -75,7 +76,8 @@ type ( StackFileContent string `json:"StackFileContent"` } putStackRequest struct { - StackFileContent string `valid:"required"` + StackFileContent string `valid:"required"` + Env []portainer.Pair `valid:""` } ) @@ -165,6 +167,7 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter, Name: stackName, SwarmID: swarmID, EntryPoint: file.ComposeFileDefaultName, + Env: req.Env, } projectPath, err := handler.FileService.StoreStackFileFromString(string(stack.ID), stackFileContent) @@ -282,6 +285,7 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri Name: stackName, SwarmID: swarmID, EntryPoint: req.PathInRepository, + Env: req.Env, } projectPath := handler.FileService.GetStackProjectPath(string(stack.ID)) @@ -369,6 +373,13 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r return } + envParam := r.FormValue("Env") + var env []portainer.Pair + if err = json.Unmarshal([]byte(envParam), &env); err != nil { + httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + stackFile, _, err := r.FormFile("file") if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) @@ -394,6 +405,7 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r Name: stackName, SwarmID: swarmID, EntryPoint: file.ComposeFileDefaultName, + Env: env, } projectPath, err := handler.FileService.StoreStackFileFromReader(string(stack.ID), stackFile) @@ -587,6 +599,7 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) return } + stack.Env = req.Env _, err = handler.FileService.StoreStackFileFromString(string(stack.ID), req.StackFileContent) if err != nil { @@ -594,6 +607,12 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque return } + err = handler.StackService.UpdateStack(stack.ID, stack) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + securityContext, err := security.RetrieveRestrictedRequestContext(r) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) diff --git a/api/portainer.go b/api/portainer.go index 1e9b2f3a2..23de2f91f 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -138,6 +138,7 @@ type ( EntryPoint string `json:"EntryPoint"` SwarmID string `json:"SwarmId"` ProjectPath string + Env []Pair `json:"Env"` } // RegistryID represents a registry identifier. diff --git a/app/components/createStack/createStackController.js b/app/components/createStack/createStackController.js index 840b81e8c..7b577373d 100644 --- a/app/components/createStack/createStackController.js +++ b/app/components/createStack/createStackController.js @@ -1,6 +1,6 @@ angular.module('createStack', []) -.controller('CreateStackController', ['$scope', '$state', '$document', 'StackService', 'CodeMirrorService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', -function ($scope, $state, $document, StackService, CodeMirrorService, Authentication, Notifications, FormValidator, ResourceControlService) { +.controller('CreateStackController', ['$scope', '$state', '$document', 'StackService', 'CodeMirrorService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', 'FormHelper', +function ($scope, $state, $document, StackService, CodeMirrorService, Authentication, Notifications, FormValidator, ResourceControlService, FormHelper) { // Store the editor content when switching builder methods var editorContent = ''; @@ -11,6 +11,7 @@ function ($scope, $state, $document, StackService, CodeMirrorService, Authentica StackFileContent: '# Define or paste the content of your docker-compose file here', StackFile: null, RepositoryURL: '', + Env: [], RepositoryPath: 'docker-compose.yml', AccessControlData: new AccessControlFormData() }; @@ -20,6 +21,14 @@ function ($scope, $state, $document, StackService, CodeMirrorService, Authentica formValidationError: '' }; + $scope.addEnvironmentVariable = function() { + $scope.formValues.Env.push({ name: '', value: ''}); + }; + + $scope.removeEnvironmentVariable = function(index) { + $scope.formValues.Env.splice(index, 1); + }; + function validateForm(accessControlData, isAdmin) { $scope.state.formValidationError = ''; var error = ''; @@ -34,20 +43,21 @@ function ($scope, $state, $document, StackService, CodeMirrorService, Authentica function createStack(name) { var method = $scope.state.Method; + var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env); if (method === 'editor') { // The codemirror editor does not work with ng-model so we need to retrieve // the value directly from the editor. var stackFileContent = $scope.editor.getValue(); - return StackService.createStackFromFileContent(name, stackFileContent); + return StackService.createStackFromFileContent(name, stackFileContent, env); } else if (method === 'upload') { var stackFile = $scope.formValues.StackFile; - return StackService.createStackFromFileUpload(name, stackFile); + return StackService.createStackFromFileUpload(name, stackFile, env); } else if (method === 'repository') { var gitRepository = $scope.formValues.RepositoryURL; var pathInRepository = $scope.formValues.RepositoryPath; - return StackService.createStackFromGitRepository(name, gitRepository, pathInRepository); + return StackService.createStackFromGitRepository(name, gitRepository, pathInRepository, env); } } diff --git a/app/components/createStack/createstack.html b/app/components/createStack/createstack.html index 4845e58e3..fabbbbab7 100644 --- a/app/components/createStack/createstack.html +++ b/app/components/createStack/createstack.html @@ -131,6 +131,36 @@ +
+ Environment +
+ +
+
+ + + add environment variable + +
+ +
+
+
+ name + +
+
+ value + +
+ +
+
+ +
+ diff --git a/app/components/stack/stack.html b/app/components/stack/stack.html index 8795ce307..c98c6bdd1 100644 --- a/app/components/stack/stack.html +++ b/app/components/stack/stack.html @@ -38,6 +38,36 @@ +
+ Environment +
+ +
+
+ + + add environment variable + +
+ +
+
+
+ name + +
+
+ value + +
+ +
+
+ +
+
Actions
diff --git a/app/components/stack/stackController.js b/app/components/stack/stackController.js index b483c1cc7..7d287c0f5 100644 --- a/app/components/stack/stackController.js +++ b/app/components/stack/stackController.js @@ -1,6 +1,6 @@ angular.module('stack', []) -.controller('StackController', ['$q', '$scope', '$state', '$stateParams', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications', -function ($q, $scope, $state, $stateParams, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications) { +.controller('StackController', ['$q', '$scope', '$state', '$stateParams', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications', 'FormHelper', +function ($q, $scope, $state, $stateParams, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications, FormHelper) { $scope.deployStack = function () { $('#createResourceSpinner').show(); @@ -8,8 +8,9 @@ function ($q, $scope, $state, $stateParams, $document, StackService, NodeService // The codemirror editor does not work with ng-model so we need to retrieve // the value directly from the editor. var stackFile = $scope.editor.getValue(); + var env = FormHelper.removeInvalidEnvVars($scope.stack.Env); - StackService.updateStack($scope.stack.Id, stackFile) + StackService.updateStack($scope.stack.Id, stackFile, env) .then(function success(data) { Notifications.success('Stack successfully deployed'); $state.reload(); @@ -22,6 +23,14 @@ function ($q, $scope, $state, $stateParams, $document, StackService, NodeService }); }; + $scope.addEnvironmentVariable = function() { + $scope.stack.Env.push({ name: '', value: ''}); + }; + + $scope.removeEnvironmentVariable = function(index) { + $scope.stack.Env.splice(index, 1); + }; + function initView() { $('#loadingViewSpinner').show(); var stackId = $stateParams.id; diff --git a/app/helpers/formHelper.js b/app/helpers/formHelper.js new file mode 100644 index 000000000..7cb3456f1 --- /dev/null +++ b/app/helpers/formHelper.js @@ -0,0 +1,18 @@ +angular.module('portainer.helpers') +.factory('FormHelper', [function FormHelperFactory() { + 'use strict'; + var helper = {}; + + helper.removeInvalidEnvVars = function(env) { + for (var i = env.length - 1; i >= 0; i--) { + var envvar = env[i]; + if (!envvar.value || !envvar.name) { + env.splice(i, 1); + } + } + + return env; + }; + + return helper; +}]); diff --git a/app/models/api/stack.js b/app/models/api/stack.js index 3d4645913..30b130943 100644 --- a/app/models/api/stack.js +++ b/app/models/api/stack.js @@ -2,6 +2,7 @@ function StackViewModel(data) { this.Id = data.Id; this.Name = data.Name; this.Checked = false; + this.Env = data.Env; if (data.ResourceControl && data.ResourceControl.Id !== 0) { this.ResourceControl = new ResourceControlViewModel(data.ResourceControl); } diff --git a/app/services/api/stackService.js b/app/services/api/stackService.js index 6ab6e7f8a..e206b620d 100644 --- a/app/services/api/stackService.js +++ b/app/services/api/stackService.js @@ -100,13 +100,19 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic return deferred.promise; }; - service.createStackFromFileContent = function(name, stackFileContent) { + service.createStackFromFileContent = function(name, stackFileContent, env) { var deferred = $q.defer(); SwarmService.swarm() .then(function success(data) { var swarm = data; - return Stack.create({ method: 'string' }, { Name: name, SwarmID: swarm.Id, StackFileContent: stackFileContent }).$promise; + var payload = { + Name: name, + SwarmID: swarm.Id, + StackFileContent: stackFileContent, + Env: env + }; + return Stack.create({ method: 'string' }, payload).$promise; }) .then(function success(data) { deferred.resolve(data); @@ -118,13 +124,20 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic return deferred.promise; }; - service.createStackFromGitRepository = function(name, gitRepository, pathInRepository) { + service.createStackFromGitRepository = function(name, gitRepository, pathInRepository, env) { var deferred = $q.defer(); SwarmService.swarm() .then(function success(data) { var swarm = data; - return Stack.create({ method: 'repository' }, { Name: name, SwarmID: swarm.Id, GitRepository: gitRepository, PathInRepository: pathInRepository }).$promise; + var payload = { + Name: name, + SwarmID: swarm.Id, + GitRepository: gitRepository, + PathInRepository: pathInRepository, + Env: env + }; + return Stack.create({ method: 'repository' }, payload).$promise; }) .then(function success(data) { deferred.resolve(data); @@ -136,13 +149,13 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic return deferred.promise; }; - service.createStackFromFileUpload = function(name, stackFile) { + service.createStackFromFileUpload = function(name, stackFile, env) { var deferred = $q.defer(); SwarmService.swarm() .then(function success(data) { var swarm = data; - return FileUploadService.createStack(name, swarm.Id, stackFile); + return FileUploadService.createStack(name, swarm.Id, stackFile, env); }) .then(function success(data) { deferred.resolve(data.data); @@ -154,8 +167,8 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic return deferred.promise; }; - service.updateStack = function(id, stackFile) { - return Stack.update({ id: id, StackFileContent: stackFile }).$promise; + service.updateStack = function(id, stackFile, env) { + return Stack.update({ id: id, StackFileContent: stackFile, Env: env }).$promise; }; return service; diff --git a/app/services/fileUpload.js b/app/services/fileUpload.js index 0f3f46643..da121eec6 100644 --- a/app/services/fileUpload.js +++ b/app/services/fileUpload.js @@ -8,9 +8,9 @@ angular.module('portainer.services') return Upload.upload({ url: url, data: { file: file }}); } - service.createStack = function(stackName, swarmId, file) { + service.createStack = function(stackName, swarmId, file, env) { var endpointID = EndpointProvider.endpointID(); - return Upload.upload({ url: 'api/endpoints/' + endpointID + '/stacks?method=file', data: { file: file, Name: stackName, SwarmID: swarmId } }); + return Upload.upload({ url: 'api/endpoints/' + endpointID + '/stacks?method=file', data: { file: file, Name: stackName, SwarmID: swarmId, Env: Upload.json(env) } }); }; service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {