diff --git a/app/assets/css/app.css b/app/assets/css/app.css index e41e1b01c..1989727d0 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -1023,3 +1023,8 @@ json-tree .branch-preview { overflow-y: auto; } /* !uib-typeahead override */ + +.my-8 { + margin-top: 2rem; + margin-bottom: 2rem; +} diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html index 025071149..c0a4b3332 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html +++ b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html @@ -136,78 +136,7 @@ -
-
- Git repository -
-
- - You can use the URL of a git repository. - -
-
- -
- -
-
-
- - Specify a reference of the repository using the following syntax: branches with - refs/heads/branch_name or tags with refs/tags/tag_name. If not specified, will use the default HEAD reference normally the - master branch. - -
-
- -
- -
-
-
- - Indicate the path to the Compose file from the root of your repository. - -
-
- -
- -
-
-
-
- - -
-
-
- - If your git account has 2FA enabled, you may receive an - authentication required error when deploying your stack. In this case, you will need to provide a personal-access token instead of your password. - -
-
- -
- -
- -
- -
-
-
+
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js index 7636f2bf0..aaf10b64b 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js @@ -40,6 +40,7 @@ export class CreateEdgeStackViewController { this.onChangeTemplate = this.onChangeTemplate.bind(this); this.onChangeTemplateAsync = this.onChangeTemplateAsync.bind(this); this.onChangeMethod = this.onChangeMethod.bind(this); + this.onChangeFormValues = this.onChangeFormValues.bind(this); } async uiCanExit() { @@ -161,6 +162,10 @@ export class CreateEdgeStackViewController { return this.EdgeStackService.createStackFromGitRepository(name, repositoryOptions, this.formValues.Groups); } + onChangeFormValues(values) { + this.formValues = values; + } + editorUpdate(cm) { this.formValues.StackFileContent = cm.getValue(); this.state.isEditorDirty = true; diff --git a/app/portainer/__module.js b/app/portainer/__module.js index b8f01310c..21e14120b 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -1,5 +1,7 @@ import _ from 'lodash-es'; +import componentsModule from './components'; + async function initAuthentication(authManager, Authentication, $rootScope, $state) { authManager.checkAuthOnRefresh(); // The unauthenticated event is broadcasted by the jwtInterceptor when @@ -15,7 +17,7 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat return await Authentication.init(); } -angular.module('portainer.app', ['portainer.oauth']).config([ +angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([ '$stateRegistryProvider', function ($stateRegistryProvider) { 'use strict'; diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.controller.js b/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.controller.js new file mode 100644 index 000000000..1fac1f4fd --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.controller.js @@ -0,0 +1,20 @@ +class GitFormComposeAuthFieldsetController { + /* @ngInject */ + constructor() { + this.onChangeField = this.onChangeField.bind(this); + this.onChangeAuth = this.onChangeField('RepositoryAuthentication'); + this.onChangeUsername = this.onChangeField('RepositoryUsername'); + this.onChangePassword = this.onChangeField('RepositoryPassword'); + } + + onChangeField(field) { + return (value) => { + this.onChange({ + ...this.model, + [field]: value, + }); + }; + } +} + +export default GitFormComposeAuthFieldsetController; diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.html b/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.html new file mode 100644 index 000000000..b884c248d --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.html @@ -0,0 +1,39 @@ +
+
+ +
+
+
+
+ + If your git account has 2FA enabled, you may receive an authentication required error when deploying your stack. In this case, you will need to provide a + personal-access token instead of your password. + +
+
+ +
+ +
+ +
+ +
+
+
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 new file mode 100644 index 000000000..5c2373387 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js @@ -0,0 +1,10 @@ +import controller from './git-form-auth-fieldset.controller.js'; + +export const gitFormAuthFieldset = { + templateUrl: './git-form-auth-fieldset.html', + controller, + bindings: { + model: '<', + onChange: '<', + }, +}; diff --git a/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html b/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html new file mode 100644 index 000000000..541023c49 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html @@ -0,0 +1,11 @@ +
+ + Indicate the path to the Compose file from the root of your repository. + +
+
+ +
+ +
+
diff --git a/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js b/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js new file mode 100644 index 000000000..7906d0e85 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js @@ -0,0 +1,7 @@ +export const gitFormComposePathField = { + templateUrl: './git-form-compose-path-field.html', + bindings: { + value: '<', + onChange: '<', + }, +}; diff --git a/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html b/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html new file mode 100644 index 000000000..a12639d24 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html @@ -0,0 +1,12 @@ +
+ + Specify a reference of the repository using the following syntax: branches with refs/heads/branch_name or tags with refs/tags/tag_name. If not + specified, will use the default HEAD reference normally the master branch. + +
+
+ +
+ +
+
diff --git a/app/portainer/components/forms/git-form/git-form-ref-field/index.js b/app/portainer/components/forms/git-form/git-form-ref-field/index.js new file mode 100644 index 000000000..df8145061 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-ref-field/index.js @@ -0,0 +1,7 @@ +export const gitFormRefField = { + templateUrl: './git-form-ref-field.html', + bindings: { + value: '<', + onChange: '<', + }, +}; diff --git a/app/portainer/components/forms/git-form/git-form-url-field/git-form-url-field.html b/app/portainer/components/forms/git-form/git-form-url-field/git-form-url-field.html new file mode 100644 index 000000000..c9083ce60 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-url-field/git-form-url-field.html @@ -0,0 +1,18 @@ +
+ + You can use the URL of a git repository. + +
+
+ +
+ +
+
diff --git a/app/portainer/components/forms/git-form/git-form-url-field/index.js b/app/portainer/components/forms/git-form/git-form-url-field/index.js new file mode 100644 index 000000000..972df9aaa --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-url-field/index.js @@ -0,0 +1,7 @@ +export const gitFormUrlField = { + templateUrl: './git-form-url-field.html', + bindings: { + value: '<', + onChange: '<', + }, +}; diff --git a/app/portainer/components/forms/git-form/git-form.controller.js b/app/portainer/components/forms/git-form/git-form.controller.js new file mode 100644 index 000000000..795e9a7ba --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form.controller.js @@ -0,0 +1,18 @@ +export default class GitFormController { + /* @ngInject */ + constructor() { + this.onChangeField = this.onChangeField.bind(this); + this.onChangeURL = this.onChangeField('RepositoryURL'); + this.onChangeRefName = this.onChangeField('RepositoryReferenceName'); + this.onChangeComposePath = this.onChangeField('ComposeFilePathInRepository'); + } + + onChangeField(field) { + return (value) => { + this.onChange({ + ...this.model, + [field]: value, + }); + }; + } +} diff --git a/app/portainer/components/forms/git-form/git-form.html b/app/portainer/components/forms/git-form/git-form.html new file mode 100644 index 000000000..16b131008 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form.html @@ -0,0 +1,9 @@ +
+
+ Git repository +
+ + + + +
diff --git a/app/portainer/components/forms/git-form/git-form.js b/app/portainer/components/forms/git-form/git-form.js new file mode 100644 index 000000000..4a1c5733e --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form.js @@ -0,0 +1,10 @@ +import controller from './git-form.controller.js'; + +export const gitForm = { + templateUrl: './git-form.html', + controller, + bindings: { + model: '<', + onChange: '<', + }, +}; diff --git a/app/portainer/components/forms/git-form/index.js b/app/portainer/components/forms/git-form/index.js new file mode 100644 index 000000000..323c1d271 --- /dev/null +++ b/app/portainer/components/forms/git-form/index.js @@ -0,0 +1,15 @@ +import angular from 'angular'; + +import { gitForm } from './git-form'; +import { gitFormAuthFieldset } from './git-form-auth-fieldset'; +import { gitFormComposePathField } from './git-form-compose-path-field'; +import { gitFormRefField } from './git-form-ref-field'; +import { gitFormUrlField } from './git-form-url-field'; + +export default angular + .module('portainer.app.components.forms.git', []) + .component('gitFormComposePathField', gitFormComposePathField) + .component('gitFormRefField', gitFormRefField) + .component('gitForm', gitForm) + .component('gitFormUrlField', gitFormUrlField) + .component('gitFormAuthFieldset', gitFormAuthFieldset).name; diff --git a/app/portainer/components/forms/stack-redeploy-git-form/index.js b/app/portainer/components/forms/stack-redeploy-git-form/index.js new file mode 100644 index 000000000..2372e8b58 --- /dev/null +++ b/app/portainer/components/forms/stack-redeploy-git-form/index.js @@ -0,0 +1,13 @@ +import angular from 'angular'; +import controller from './stack-redeploy-git-form.controller.js'; + +export const stackRedeployGitForm = { + templateUrl: './stack-redeploy-git-form.html', + controller, + bindings: { + model: '<', + stack: '<', + }, +}; + +angular.module('portainer.app').component('stackRedeployGitForm', stackRedeployGitForm); 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 new file mode 100644 index 000000000..0de3a2cb5 --- /dev/null +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js @@ -0,0 +1,72 @@ +class StackRedployGitFormController { + /* @ngInject */ + constructor($async, StackService, ModalService, Notifications) { + this.$async = $async; + this.StackService = StackService; + this.ModalService = ModalService; + this.Notifications = Notifications; + + this.state = { + inProgress: 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 submit() { + return this.$async(async () => { + try { + this.state.inProgress = true; + + const confirmed = await this.ModalService.confirmAsync({ + title: 'Are you sure?', + message: 'Any changes to this stack made locally in Portainer will be overridden 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; + } + + await this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, [], false, this.formValues); + } catch (err) { + this.Notifications.error('Failure', err, 'Failed redeploying stack'); + } finally { + this.state.inProgress = false; + } + }); + } + + isSubmitButtonDisabled() { + return this.state.inProgress; + } + + $onInit() { + this.formValues.RefName = this.model.ReferenceName; + } +} + +export default StackRedployGitFormController; 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 new file mode 100644 index 000000000..b52f4d3a2 --- /dev/null +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html @@ -0,0 +1,28 @@ +
+
+ Redeploy from git repository +
+
+

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

+

+ Update {{ $ctrl.model.ConfigFilePath }} in git and pull from here to update the stack. +

+
+ + + + + +
diff --git a/app/portainer/components/index.js b/app/portainer/components/index.js new file mode 100644 index 000000000..8fa3030a0 --- /dev/null +++ b/app/portainer/components/index.js @@ -0,0 +1,5 @@ +import angular from 'angular'; + +import gitFormModule from './forms/git-form'; + +export default angular.module('portainer.app.components', [gitFormModule]).name; diff --git a/app/portainer/models/stack.js b/app/portainer/models/stack.js index c2c6d5d1a..4e356db08 100644 --- a/app/portainer/models/stack.js +++ b/app/portainer/models/stack.js @@ -17,6 +17,7 @@ export function StackViewModel(data) { this.CreatedBy = data.CreatedBy; this.UpdateDate = data.UpdateDate; this.UpdatedBy = data.UpdatedBy; + this.GitConfig = data.GitConfig; } export function ExternalStackViewModel(name, type, creationDate) { diff --git a/app/portainer/rest/stack.js b/app/portainer/rest/stack.js index d521c876c..a2885cb72 100644 --- a/app/portainer/rest/stack.js +++ b/app/portainer/rest/stack.js @@ -17,6 +17,7 @@ angular.module('portainer.app').factory('Stack', [ migrate: { method: 'POST', params: { id: '@id', action: 'migrate', endpointId: '@endpointId' }, ignoreLoadingBar: true }, start: { method: 'POST', params: { id: '@id', action: 'start' } }, stop: { method: 'POST', params: { id: '@id', action: 'stop' } }, + updateGit: { method: 'PUT', params: { action: 'git' } }, } ); }, diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index 477740725..692ff5480 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -13,7 +13,9 @@ angular.module('portainer.app').factory('StackService', [ 'EndpointProvider', function StackServiceFactory($q, $async, Stack, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService, EndpointProvider) { 'use strict'; - var service = {}; + var service = { + updateGit, + }; service.stack = function (id) { var deferred = $q.defer(); @@ -349,6 +351,20 @@ angular.module('portainer.app').factory('StackService', [ return Stack.stop({ id }).$promise; } + function updateGit(id, endpointId, env, prune, gitConfig) { + return Stack.updateGit( + { endpointId, id }, + { + env, + prune, + RepositoryReferenceName: gitConfig.RefName, + RepositoryAuthentication: gitConfig.RepositoryAuthentication, + RepositoryUsername: gitConfig.RepositoryUsername, + RepositoryPassword: gitConfig.RepositoryPassword, + } + ).$promise; + } + return service; }, ]); diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html index cb4b96d90..c84198744 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html @@ -103,84 +103,7 @@
-
-
- Git repository -
-
- - You can use the URL of a git repository. - -
-
- -
- -
-
-
- - Specify a reference of the repository using the following syntax: branches with - refs/heads/branch_name or tags with refs/tags/tag_name. If not specified, will use the default HEAD reference normally the - master branch. - -
-
- -
- -
-
-
- - Indicate the path to the Compose file from the root of your repository. - -
-
- -
- -
-
-
-
- - -
-
-
- - If your git account has 2FA enabled, you may receive an - authentication required error when creating your template. In this case, you will need to provide a personal-access token instead of your password. - -
-
- -
- -
- -
- -
-
-
+ diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js index e24b54853..f64778957 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js @@ -54,6 +54,7 @@ class CreateCustomTemplateViewController { this.createCustomTemplateFromGitRepository = this.createCustomTemplateFromGitRepository.bind(this); this.editorUpdate = this.editorUpdate.bind(this); this.onChangeMethod = this.onChangeMethod.bind(this); + this.onChangeFormValues = this.onChangeFormValues.bind(this); } createCustomTemplate() { @@ -150,6 +151,10 @@ class CreateCustomTemplateViewController { this.state.isEditorDirty = true; } + onChangeFormValues(newValues) { + this.formValues = newValues; + } + async $onInit() { const applicationState = this.StateManager.getState(); diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js index 76a932e67..28165452b 100644 --- a/app/portainer/views/stacks/create/createStackController.js +++ b/app/portainer/views/stacks/create/createStackController.js @@ -53,6 +53,8 @@ angular } }; + $scope.onChangeFormValues = onChangeFormValues; + $scope.addEnvironmentVariable = function () { $scope.formValues.Env.push({ name: '', value: '' }); }; @@ -239,4 +241,8 @@ angular }; initView(); + + function onChangeFormValues(newValues) { + $scope.formValues = newValues; + } }); diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html index 6719edfec..39fb147a5 100644 --- a/app/portainer/views/stacks/create/createstack.html +++ b/app/portainer/views/stacks/create/createstack.html @@ -130,79 +130,7 @@ - -
-
- Git repository -
-
- - You can use the URL of a git repository. - -
-
- -
- -
-
-
- - Specify a reference of the repository using the following syntax: branches with refs/heads/branch_name or tags with refs/tags/tag_name. If - not specified, will use the default HEAD reference normally the master branch. - -
-
- -
- -
-
-
- - Indicate the path to the Compose file from the root of your repository. - -
-
- -
- -
-
-
-
- - -
-
-
- - If your git account has 2FA enabled, you may receive an authentication required error when deploying your stack. In this case, you will need to provide - a personal-access token instead of your password. - -
-
- -
- -
- -
- -
-
-
- +
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html index da43c701c..db3fbc4fa 100644 --- a/app/portainer/views/stacks/edit/stack.html +++ b/app/portainer/views/stacks/edit/stack.html @@ -77,6 +77,7 @@
+