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 @@
-
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 @@
+
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 @@
-