Compare commits
9 Commits
fix/EE-672
...
feat/EE-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
859ec3b4aa | ||
|
|
d3089b318d | ||
|
|
b9dc698cd6 | ||
|
|
72da47946c | ||
|
|
b5a0a88eb8 | ||
|
|
e0bec81d78 | ||
|
|
6af762d073 | ||
|
|
2bff3d134e | ||
|
|
603030163f |
@@ -92,7 +92,7 @@
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
focus-if="!$ctrl.notAutoFocus"
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,5 +12,6 @@ angular.module('portainer.docker').component('containersDatatable', {
|
||||
showAddAction: '<',
|
||||
offlineMode: '<',
|
||||
refreshCallback: '<',
|
||||
notAutoFocus: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
focus-if="!$ctrl.notAutoFocus"
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -15,5 +15,6 @@ angular.module('portainer.docker').component('servicesDatatable', {
|
||||
showStackColumn: '<',
|
||||
showTaskLogsButton: '<',
|
||||
refreshCallback: '<',
|
||||
notAutoFocus: '<',
|
||||
},
|
||||
});
|
||||
|
||||
20
app/portainer/components/focusIf.js
Normal file
20
app/portainer/components/focusIf.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular.module('portainer.app').directive('focusIf', function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function ($scope, $element, $attrs) {
|
||||
var dom = $element[0];
|
||||
if ($attrs.focusIf) {
|
||||
$scope.$watch($attrs.focusIf, focus);
|
||||
} else {
|
||||
focus(true);
|
||||
}
|
||||
function focus(condition) {
|
||||
if (condition) {
|
||||
$timeout(function () {
|
||||
dom.focus();
|
||||
}, $scope.$eval($attrs.focusDelay) || 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
class GitFormAdditionalFileItemController {
|
||||
/* @ngInject */
|
||||
constructor() {}
|
||||
|
||||
onChangePath(value) {
|
||||
const fieldIsInvalid = typeof value === 'undefined';
|
||||
if (fieldIsInvalid) {
|
||||
return;
|
||||
}
|
||||
this.onChange(this.index, { value });
|
||||
}
|
||||
|
||||
removeValue() {
|
||||
this.onChange(this.index);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.formName = `variableForm${this.index}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default GitFormAdditionalFileItemController;
|
||||
@@ -0,0 +1,20 @@
|
||||
<ng-form class="env-item form-horizontal" name="$ctrl.{{ $ctrl.formName }}">
|
||||
<div class="form-group col-sm-12">
|
||||
<div class="form-inline" style="margin-top: 10px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">path</span>
|
||||
<input type="text" name="name" class="form-control" ng-model="$ctrl.variable" ng-change="$ctrl.onChangePath($ctrl.variable)" required />
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeValue()">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div ng-show="$ctrl[$ctrl.formName].name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="$ctrl[$ctrl.formName].name.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required. </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -0,0 +1,17 @@
|
||||
import angular from 'angular';
|
||||
import controller from './git-form-additional-file-item.controller.js';
|
||||
|
||||
export const gitFormAdditionalFileItem = {
|
||||
templateUrl: './git-form-additional-file-item.html',
|
||||
controller,
|
||||
|
||||
bindings: {
|
||||
variable: '<',
|
||||
index: '<',
|
||||
|
||||
onChange: '<',
|
||||
onRemove: '<',
|
||||
},
|
||||
};
|
||||
|
||||
angular.module('portainer.app').component('gitFormAdditionalFileItem', gitFormAdditionalFileItem);
|
||||
@@ -0,0 +1,26 @@
|
||||
class GitFormAutoUpdateFieldsetController {
|
||||
/* @ngInject */
|
||||
constructor() {
|
||||
this.add = this.add.bind(this);
|
||||
this.onChangeVariable = this.onChangeVariable.bind(this);
|
||||
}
|
||||
|
||||
add() {
|
||||
this.model.AdditionalFiles.push('');
|
||||
}
|
||||
|
||||
onChangeVariable(index, variable) {
|
||||
if (!variable) {
|
||||
this.model.AdditionalFiles.splice(index, 1);
|
||||
} else {
|
||||
this.model.AdditionalFiles[index] = variable.value;
|
||||
}
|
||||
|
||||
this.onChange({
|
||||
...this.model,
|
||||
AdditionalFiles: this.model.AdditionalFiles,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default GitFormAutoUpdateFieldsetController;
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Additional paths</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.add()"> <i class="fa fa-plus-circle" aria-hidden="true"></i> add file </span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<git-form-additional-file-item
|
||||
ng-repeat="variable in $ctrl.model.AdditionalFiles track by $index"
|
||||
variable="variable"
|
||||
index="$index"
|
||||
on-change="($ctrl.onChangeVariable)"
|
||||
></git-form-additional-file-item>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
import controller from './git-form-additional-files-panel.controller.js';
|
||||
|
||||
export const gitFormAdditionalFilesPanel = {
|
||||
templateUrl: './git-form-additional-files-panel.html',
|
||||
controller,
|
||||
bindings: {
|
||||
model: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
};
|
||||
@@ -3,35 +3,36 @@
|
||||
<por-switch-field ng-model="$ctrl.model.RepositoryAuthentication" label="Authentication" on-change="($ctrl.onChangeAuth)"></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-warning" style="margin: 5px 0 15px 0;" ng-if="$ctrl.model.RepositoryAuthentication && $ctrl.showAuthExplanation">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="text-muted">Enabling authentication will store the credentials and it is advisable to use a git service account</span>
|
||||
</div>
|
||||
<div ng-if="$ctrl.model.RepositoryAuthentication">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
If your git account has 2FA enabled, you may receive an <code>authentication required</code> error when deploying your stack. In this case, you will need to provide a
|
||||
personal-access token instead of your password.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
|
||||
<div class="col-sm-11 col-md-5">
|
||||
<label for="repository_username" class="col-sm-2 control-label text-left">Username</label>
|
||||
<div class="col-sm-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.model.RepositoryUsername"
|
||||
name="repository_username"
|
||||
placeholder="myGitUser"
|
||||
placeholder="git username"
|
||||
ng-change="$ctrl.onChangeUsername($ctrl.model.RepositoryUsername)"
|
||||
/>
|
||||
</div>
|
||||
<label for="repository_password" class="col-sm-1 control-label text-left">
|
||||
Password
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repository_password" class="col-sm-2 control-label text-left">
|
||||
Personal Access Token
|
||||
<portainer-tooltip position="bottom" message="Provide a personal access token or password"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-11 col-md-5">
|
||||
<div class="col-sm-3">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.model.RepositoryPassword"
|
||||
name="repository_password"
|
||||
placeholder="myPassword"
|
||||
placeholder="personal access token"
|
||||
ng-change="$ctrl.onChangePassword($ctrl.model.RepositoryPassword)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
class GitFormAutoUpdateFieldsetController {
|
||||
/* @ngInject */
|
||||
constructor(clipboard) {
|
||||
this.onChangeAutoUpdate = this.onChangeField('RepositoryAutomaticUpdates');
|
||||
this.onChangeMechanism = this.onChangeField('RepositoryMechanism');
|
||||
this.onChangeInterval = this.onChangeField('RepositoryFetchInterval');
|
||||
this.clipboard = clipboard;
|
||||
}
|
||||
|
||||
copyWebhook() {
|
||||
this.clipboard.copyText(this.model.RepositoryWebhookURL);
|
||||
$('#copyNotification').show();
|
||||
$('#copyNotification').fadeOut(2000);
|
||||
}
|
||||
|
||||
onChangeField(field) {
|
||||
return (value) => {
|
||||
this.onChange({
|
||||
...this.model,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default GitFormAutoUpdateFieldsetController;
|
||||
@@ -0,0 +1,69 @@
|
||||
<ng-form name="autoUpdateForm">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field name="autoUpdate" ng-model="$ctrl.model.RepositoryAutomaticUpdates" label="Automatic updates" on-change="($ctrl.onChangeAutoUpdate)"></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-warning" style="margin: 5px 0 10px 0;" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="text-muted">Any changes to this stack made locally in Portainer will be overriden by the definition in git and may cause service interruption.</span>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
||||
<label for="repository_mechanism" class="col-sm-1 control-label text-left">
|
||||
Mechanism
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<div class="input-group col-sm-10 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-click="$ctrl.onChangeMechanism($ctrl.model.RepositoryMechanism)" ng-model="$ctrl.model.RepositoryMechanism" uib-btn-radio="'Interval'"
|
||||
>Polling</label
|
||||
>
|
||||
<label class="btn btn-primary" ng-click="$ctrl.onChangeMechanism($ctrl.model.RepositoryMechanism)" ng-model="$ctrl.model.RepositoryMechanism" uib-btn-radio="'Webhook'"
|
||||
>Webhook</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates && $ctrl.model.RepositoryMechanism === 'Webhook'">
|
||||
<label for="repository_mechanism" class="col-sm-1 control-label text-left">
|
||||
Webhook
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="text-muted"> {{ $ctrl.model.RepositoryWebhookURL | truncatelr }} </span>
|
||||
<button type="button" class="btn btn-sm btn-primary btn-sm space-left" ng-if="$ctrl.model.RepositoryWebhookURL" ng-click="$ctrl.copyWebhook()">
|
||||
<span><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy link</span>
|
||||
</button>
|
||||
<span>
|
||||
<i id="copyNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates && $ctrl.model.RepositoryMechanism === 'Interval'">
|
||||
<label for="repository_fetch_interval" class="col-sm-1 control-label text-left">
|
||||
Fetch interval
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-change="$ctrl.onChangeInterval($ctrl.model.RepositoryFetchInterval)"
|
||||
ng-model="$ctrl.model.RepositoryFetchInterval"
|
||||
name="repository_fetch_interval"
|
||||
placeholder="5m"
|
||||
required
|
||||
interval-format
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="autoUpdateForm.repository_fetch_interval.$touched && autoUpdateForm.repository_fetch_interval.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="autoUpdateForm.repository_fetch_interval.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="invalidIntervalFormat"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Please enter a valid time interval.</p>
|
||||
<p ng-message="minimumInterval"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum interval is 1m</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -0,0 +1,10 @@
|
||||
import controller from './git-form-auto-update-fieldset.controller.js';
|
||||
|
||||
export const gitFormAutoUpdateFieldset = {
|
||||
templateUrl: './git-form-auto-update-fieldset.html',
|
||||
controller,
|
||||
bindings: {
|
||||
model: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
};
|
||||
@@ -5,8 +5,8 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stack_repository_reference_name" class="col-sm-2 control-label text-left">Repository reference</label>
|
||||
<div class="col-sm-10">
|
||||
<label for="stack_repository_reference_name" class="col-sm-1 control-label text-left">Repository reference</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.value" id="stack_repository_reference_name" placeholder="refs/heads/master" ng-change="$ctrl.onChange($ctrl.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
<git-form-url-field value="$ctrl.model.RepositoryURL" on-change="($ctrl.onChangeURL)"></git-form-url-field>
|
||||
<git-form-ref-field value="$ctrl.model.RepositoryReferenceName" on-change="($ctrl.onChangeRefName)"></git-form-ref-field>
|
||||
<git-form-compose-path-field value="$ctrl.model.ComposeFilePathInRepository" on-change="($ctrl.onChangeComposePath)"></git-form-compose-path-field>
|
||||
<git-form-auth-fieldset model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-auth-fieldset>
|
||||
<git-form-additional-files-panel ng-if="$ctrl.additionalFile" model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-additional-files-panel>
|
||||
<git-form-auth-fieldset model="$ctrl.model" on-change="($ctrl.onChange)" show-auth-explanation="$ctrl.showAuthExplanation"></git-form-auth-fieldset>
|
||||
<git-form-auto-update-fieldset ng-if="$ctrl.autoUpdate" model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-auto-update-fieldset>
|
||||
</div>
|
||||
|
||||
@@ -6,5 +6,8 @@ export const gitForm = {
|
||||
bindings: {
|
||||
model: '<',
|
||||
onChange: '<',
|
||||
additionalFile: '<',
|
||||
autoUpdate: '<',
|
||||
showAuthExplanation: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import angular from 'angular';
|
||||
|
||||
import { gitForm } from './git-form';
|
||||
import { gitFormAuthFieldset } from './git-form-auth-fieldset';
|
||||
import { gitFormAdditionalFilesPanel } from './git-form-additional-files-panel';
|
||||
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';
|
||||
@@ -12,4 +14,6 @@ export default angular
|
||||
.component('gitFormRefField', gitFormRefField)
|
||||
.component('gitForm', gitForm)
|
||||
.component('gitFormUrlField', gitFormUrlField)
|
||||
.component('gitFormAdditionalFilesPanel', gitFormAdditionalFilesPanel)
|
||||
.component('gitFormAutoUpdateFieldset', gitFormAutoUpdateFieldset)
|
||||
.component('gitFormAuthFieldset', gitFormAuthFieldset).name;
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import uuidv4 from 'uuid/v4';
|
||||
class StackRedeployGitFormController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, StackService, ModalService, Notifications) {
|
||||
constructor($async, $state, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.StackService = StackService;
|
||||
this.ModalService = ModalService;
|
||||
this.Notifications = Notifications;
|
||||
this.WebhookHelper = WebhookHelper;
|
||||
this.FormHelper = FormHelper;
|
||||
|
||||
this.state = {
|
||||
inProgress: false,
|
||||
redeployInProgress: false,
|
||||
showConfig: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
@@ -16,10 +21,19 @@ class StackRedeployGitFormController {
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
Env: [],
|
||||
// auto upadte
|
||||
AutoUpdate: {
|
||||
RepositoryAutomaticUpdates: false,
|
||||
RepositoryMechanism: 'Interval',
|
||||
RepositoryFetchInterval: '',
|
||||
RepositoryWebhookURL: '',
|
||||
},
|
||||
};
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onChangeRef = this.onChangeRef.bind(this);
|
||||
this.handleEnvVarChange = this.handleEnvVarChange.bind(this);
|
||||
}
|
||||
|
||||
onChangeRef(value) {
|
||||
@@ -50,13 +64,27 @@ class StackRedeployGitFormController {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.inProgress = true;
|
||||
this.state.redeployInProgress = true;
|
||||
|
||||
await this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, [], false, this.formValues);
|
||||
await this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), false, this.formValues);
|
||||
|
||||
await this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed redeploying stack');
|
||||
} finally {
|
||||
this.state.redeployInProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async saveGitSettings() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.state.inProgress = true;
|
||||
await this.StackService.updateGitStackSettings(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), this.formValues);
|
||||
this.Notifications.success('Save stack settings successfully');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to save stack settings');
|
||||
} finally {
|
||||
this.state.inProgress = false;
|
||||
}
|
||||
@@ -64,11 +92,38 @@ class StackRedeployGitFormController {
|
||||
}
|
||||
|
||||
isSubmitButtonDisabled() {
|
||||
return this.state.inProgress;
|
||||
return this.state.inProgress || this.state.redeployInProgress;
|
||||
}
|
||||
|
||||
handleEnvVarChange(value) {
|
||||
this.formValues.Env = value;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.formValues.RefName = this.model.ReferenceName;
|
||||
this.formValues.Env = this.stack.Env;
|
||||
// Init auto update
|
||||
if (this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook)) {
|
||||
this.formValues.AutoUpdate.RepositoryAutomaticUpdates = true;
|
||||
|
||||
if (this.stack.AutoUpdate.Interval) {
|
||||
this.formValues.AutoUpdate.RepositoryMechanism = `Interval`;
|
||||
this.formValues.AutoUpdate.RepositoryFetchInterval = this.stack.AutoUpdate.Interval;
|
||||
} else if (this.stack.AutoUpdate.Webhook) {
|
||||
this.formValues.AutoUpdate.RepositoryMechanism = `Webhook`;
|
||||
this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(this.stack.AutoUpdate.Webhook);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.formValues.AutoUpdate.RepositoryWebhookURL) {
|
||||
this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(uuidv4());
|
||||
}
|
||||
|
||||
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
|
||||
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
|
||||
this.formValues.RepositoryPassword = this.stack.GitConfig.Authentication.Password;
|
||||
this.formValues.RepositoryAuthentication = this.formValues.RepositoryPassword !== '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,22 +9,56 @@
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Update <code>{{ $ctrl.model.ConfigFilePath }}</code> in git and pull from here to update the stack.
|
||||
Update
|
||||
<code
|
||||
>{{ $ctrl.model.ConfigFilePath }}<span ng-if="$ctrl.stack.AdditionalFiles.length > 0">,{{ $ctrl.stack.AdditionalFiles.join(',') }}</span></code
|
||||
>
|
||||
in git and pull from here to update the stack.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<git-form-ref-field value="$ctrl.formValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
|
||||
<git-form-auth-fieldset model="$ctrl.formValues" on-change="($ctrl.onChange)"></git-form-auth-fieldset>
|
||||
<git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChange)"></git-form-auto-update-fieldset>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
<a class="small interactive" ng-click="$ctrl.state.showConfig = !$ctrl.state.showConfig">
|
||||
<i ng-class="{ 'fa fa-minus space-right': $ctrl.state.showConfig, 'fa fa-plus space-right': !$ctrl.state.showConfig }" aria-hidden="true"></i>
|
||||
{{ $ctrl.state.showConfig ? 'Hide' : 'Advanced' }} configuration
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<git-form-ref-field ng-if="$ctrl.state.showConfig" value="$ctrl.formValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
|
||||
<git-form-auth-fieldset ng-if="$ctrl.state.showConfig" model="$ctrl.formValues" on-change="($ctrl.onChange)" show-auth-explanation="true"></git-form-auth-fieldset>
|
||||
<environment-variables-panel
|
||||
ng-model="$ctrl.formValues.Env"
|
||||
explanation="These values will be used as substitutions in the stack file"
|
||||
on-change="($ctrl.handleEnvVarChange)"
|
||||
></environment-variables-panel>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.submit()"
|
||||
ng-disabled="$ctrl.isSubmitButtonDisabled()"
|
||||
ng-if="!$ctrl.formValues.AutoUpdate.RepositoryAutomaticUpdates"
|
||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.redeployInProgress"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.inProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.inProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and redeploy </span>
|
||||
<span ng-hide="$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and redeploy </span>
|
||||
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.saveGitSettings()"
|
||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.inProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.inProgress"> Save settings </span>
|
||||
<span ng-show="$ctrl.state.inProgress">In progress...</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
24
app/portainer/components/intervalFormat.js
Normal file
24
app/portainer/components/intervalFormat.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import parse from 'parse-duration';
|
||||
angular.module('portainer.app').directive('intervalFormat', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function ($scope, $element, $attrs, ngModel) {
|
||||
ngModel.$validators.invalidIntervalFormat = function (modelValue) {
|
||||
try {
|
||||
return modelValue && modelValue.toUpperCase().match(/^P?(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T?(?=\d+[HMS])(\d+H)?(\d+M)?(\d+S)?)?$/gm) !== null;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
ngModel.$validators.minimumInterval = function (modelValue) {
|
||||
try {
|
||||
return modelValue && parse(modelValue, 'minute') >= 1;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,16 +1,21 @@
|
||||
angular.module('portainer.app').factory('WebhookHelper', [
|
||||
'$location',
|
||||
'API_ENDPOINT_WEBHOOKS',
|
||||
function WebhookHelperFactory($location, API_ENDPOINT_WEBHOOKS) {
|
||||
'API_ENDPOINT_STACKS',
|
||||
function WebhookHelperFactory($location, API_ENDPOINT_WEBHOOKS, API_ENDPOINT_STACKS) {
|
||||
'use strict';
|
||||
|
||||
var helper = {};
|
||||
const protocol = $location.protocol().toLowerCase();
|
||||
const port = $location.port();
|
||||
const displayPort = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : ':' + port;
|
||||
|
||||
helper.returnWebhookUrl = function (token) {
|
||||
var displayPort =
|
||||
($location.protocol().toLowerCase() === 'http' && $location.port() === 80) || ($location.protocol().toLowerCase() === 'https' && $location.port() === 443)
|
||||
? ''
|
||||
: ':' + $location.port();
|
||||
return $location.protocol() + '://' + $location.host() + displayPort + '/' + API_ENDPOINT_WEBHOOKS + '/' + token;
|
||||
return `${protocol}://${$location.host()}${displayPort}/${API_ENDPOINT_WEBHOOKS}/${token}`;
|
||||
};
|
||||
|
||||
helper.returnStackWebhookUrl = function (token) {
|
||||
return `${protocol}://${$location.host()}${displayPort}/${API_ENDPOINT_STACKS}/webhooks/${token}`;
|
||||
};
|
||||
|
||||
return helper;
|
||||
|
||||
@@ -20,6 +20,8 @@ export function StackViewModel(data) {
|
||||
this.Orphaned = false;
|
||||
this.Checked = false;
|
||||
this.GitConfig = data.GitConfig;
|
||||
this.AdditionalFiles = data.AdditionalFiles;
|
||||
this.AutoUpdate = data.AutoUpdate;
|
||||
}
|
||||
|
||||
export function ExternalStackViewModel(name, type, creationDate) {
|
||||
|
||||
@@ -18,7 +18,8 @@ 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' } },
|
||||
updateGit: { method: 'PUT', params: { id: '@id', action: 'git/redeploy' } },
|
||||
updateGitStackSettings: { method: 'POST', params: { id: '@id', action: 'git' }, ignoreLoadingBar: true },
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -326,12 +326,18 @@ angular.module('portainer.app').factory('StackService', [
|
||||
Name: name,
|
||||
RepositoryURL: repositoryOptions.RepositoryURL,
|
||||
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
|
||||
ComposeFile: repositoryOptions.ComposeFilePathInRepository,
|
||||
AdditionalFiles: repositoryOptions.AdditionalFiles,
|
||||
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
|
||||
RepositoryUsername: repositoryOptions.RepositoryUsername,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
Env: env,
|
||||
};
|
||||
|
||||
if (repositoryOptions.AutoUpdate) {
|
||||
payload.AutoUpdate = repositoryOptions.AutoUpdate;
|
||||
}
|
||||
|
||||
return Stack.create({ method: 'repository', type: 2, endpointId: endpointId }, payload).$promise;
|
||||
};
|
||||
|
||||
@@ -346,12 +352,18 @@ angular.module('portainer.app').factory('StackService', [
|
||||
SwarmID: swarm.Id,
|
||||
RepositoryURL: repositoryOptions.RepositoryURL,
|
||||
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
|
||||
ComposeFile: repositoryOptions.ComposeFilePathInRepository,
|
||||
AdditionalFiles: repositoryOptions.AdditionalFiles,
|
||||
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
|
||||
RepositoryUsername: repositoryOptions.RepositoryUsername,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
Env: env,
|
||||
};
|
||||
|
||||
if (repositoryOptions.AutoUpdate) {
|
||||
payload.AutoUpdate = repositoryOptions.AutoUpdate;
|
||||
}
|
||||
|
||||
return Stack.create({ method: 'repository', type: 1, endpointId: endpointId }, payload).$promise;
|
||||
})
|
||||
.then(function success(data) {
|
||||
@@ -405,6 +417,31 @@ angular.module('portainer.app').factory('StackService', [
|
||||
).$promise;
|
||||
}
|
||||
|
||||
service.updateGitStackSettings = function (id, endpointId, env, gitConfig) {
|
||||
// prepare auto update
|
||||
const autoUpdate = {};
|
||||
|
||||
if (gitConfig.AutoUpdate.RepositoryAutomaticUpdates) {
|
||||
if (gitConfig.AutoUpdate.RepositoryMechanism === 'Interval') {
|
||||
autoUpdate.Interval = gitConfig.AutoUpdate.RepositoryFetchInterval;
|
||||
} else if (gitConfig.AutoUpdate.RepositoryMechanism === 'Webhook') {
|
||||
autoUpdate.Webhook = gitConfig.AutoUpdate.RepositoryWebhookURL.split('/').reverse()[0];
|
||||
}
|
||||
}
|
||||
|
||||
return Stack.updateGitStackSettings(
|
||||
{ endpointId, id },
|
||||
{
|
||||
AutoUpdate: autoUpdate,
|
||||
Env: env,
|
||||
RepositoryReferenceName: gitConfig.RefName,
|
||||
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
||||
RepositoryUsername: gitConfig.RepositoryUsername,
|
||||
RepositoryPassword: gitConfig.RepositoryPassword,
|
||||
}
|
||||
).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
|
||||
|
||||
angular
|
||||
@@ -21,7 +21,9 @@ angular
|
||||
StackHelper,
|
||||
ContainerHelper,
|
||||
CustomTemplateService,
|
||||
ContainerService
|
||||
ContainerService,
|
||||
WebhookHelper,
|
||||
clipboard
|
||||
) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
@@ -29,12 +31,17 @@ angular
|
||||
StackFile: null,
|
||||
RepositoryURL: '',
|
||||
RepositoryReferenceName: '',
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryAuthentication: true,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
Env: [],
|
||||
AdditionalFiles: [],
|
||||
ComposeFilePathInRepository: 'docker-compose.yml',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
RepositoryAutomaticUpdates: true,
|
||||
RepositoryMechanism: 'Interval',
|
||||
RepositoryFetchInterval: '',
|
||||
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
@@ -63,6 +70,14 @@ angular
|
||||
$scope.formValues.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addAdditionalFiles = function () {
|
||||
$scope.formValues.AdditionalFiles.push('');
|
||||
};
|
||||
|
||||
$scope.removeAdditionalFiles = function (index) {
|
||||
$scope.formValues.AdditionalFiles.splice(index, 1);
|
||||
};
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
@@ -91,6 +106,7 @@ angular
|
||||
|
||||
if (method === 'repository') {
|
||||
var repositoryOptions = {
|
||||
AdditionalFiles: $scope.formValues.AdditionalFiles,
|
||||
RepositoryURL: $scope.formValues.RepositoryURL,
|
||||
RepositoryReferenceName: $scope.formValues.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository,
|
||||
@@ -98,10 +114,24 @@ angular
|
||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||
};
|
||||
|
||||
getAutoUpdatesProperty(repositoryOptions);
|
||||
|
||||
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||
}
|
||||
}
|
||||
|
||||
function getAutoUpdatesProperty(repositoryOptions) {
|
||||
if ($scope.formValues.RepositoryAutomaticUpdates) {
|
||||
repositoryOptions.AutoUpdate = {};
|
||||
if ($scope.formValues.RepositoryMechanism === 'Interval') {
|
||||
repositoryOptions.AutoUpdate.Interval = $scope.formValues.RepositoryFetchInterval;
|
||||
} else if ($scope.formValues.RepositoryMechanism === 'Webhook') {
|
||||
repositoryOptions.AutoUpdate.Webhook = $scope.formValues.RepositoryWebhookURL.split('/').reverse()[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createComposeStack(name, method) {
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||
const endpointId = +$state.params.endpointId;
|
||||
@@ -114,6 +144,7 @@ angular
|
||||
return StackService.createComposeStackFromFileUpload(name, stackFile, env, endpointId);
|
||||
} else if (method === 'repository') {
|
||||
var repositoryOptions = {
|
||||
AdditionalFiles: $scope.formValues.AdditionalFiles,
|
||||
RepositoryURL: $scope.formValues.RepositoryURL,
|
||||
RepositoryReferenceName: $scope.formValues.RepositoryReferenceName,
|
||||
ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository,
|
||||
@@ -121,10 +152,19 @@ angular
|
||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||
};
|
||||
|
||||
getAutoUpdatesProperty(repositoryOptions);
|
||||
|
||||
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.copyWebhook = function () {
|
||||
clipboard.copyText($scope.formValues.RepositoryWebhookURL);
|
||||
$('#copyNotification').show();
|
||||
$('#copyNotification').fadeOut(2000);
|
||||
};
|
||||
|
||||
$scope.handleEnvVarChange = handleEnvVarChange;
|
||||
function handleEnvVarChange(value) {
|
||||
$scope.formValues.Env = value;
|
||||
|
||||
@@ -130,7 +130,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !upload -->
|
||||
<git-form ng-if="state.Method === 'repository'" model="formValues" on-change="(onChangeFormValues)"></git-form>
|
||||
<git-form
|
||||
ng-if="state.Method === 'repository'"
|
||||
model="formValues"
|
||||
on-change="(onChangeFormValues)"
|
||||
additional-file="true"
|
||||
auto-update="true"
|
||||
show-auth-explanation="true"
|
||||
></git-form>
|
||||
<!-- custom-template -->
|
||||
<div ng-show="state.Method === 'template'">
|
||||
<div class="form-group">
|
||||
@@ -207,7 +214,7 @@
|
||||
|| (state.Method === 'editor' && (!formValues.StackFileContent || state.editorYamlValidationError))
|
||||
|| (state.Method === 'upload' && (!formValues.StackFile || state.uploadYamlValidationError))
|
||||
|| (state.Method === 'template' && (!formValues.StackFileContent || !selectedTemplate || state.editorYamlValidationError))
|
||||
|| (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && (!formValues.RepositoryUsername || !formValues.RepositoryPassword))))
|
||||
|| (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && !formValues.RepositoryPassword)))
|
||||
|| !formValues.Name"
|
||||
ng-click="deployStack()"
|
||||
button-spinner="state.actionInProgress"
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</uib-tab>
|
||||
<!-- !tab-info -->
|
||||
<!-- tab-file -->
|
||||
<uib-tab index="1" select="showEditor()" ng-if="!external">
|
||||
<uib-tab index="1" select="showEditor()" ng-if="!external && !stack.GitConfig">
|
||||
<uib-tab-heading> <i class="fa fa-pencil-alt space-right" aria-hidden="true"></i> Editor </uib-tab-heading>
|
||||
<form class="form-horizontal" ng-if="state.showEditorTab" style="margin-top: 10px;" name="stackUpdateForm">
|
||||
<div class="form-group">
|
||||
@@ -185,7 +185,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-disabled="state.actionInProgress || !stackUpdateForm.$valid || stack.Status === 2 || !stackFileContent || orphaned"
|
||||
ng-disabled="state.actionInProgress || stack.Status === 2 || !stackFileContent || orphaned"
|
||||
ng-click="deployStack()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
@@ -214,6 +214,7 @@
|
||||
order-by="Status"
|
||||
show-host-column="false"
|
||||
show-add-action="false"
|
||||
not-auto-focus="true"
|
||||
></containers-datatable>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,6 +234,7 @@
|
||||
show-task-logs-button="applicationState.endpoint.apiVersion >= 1.30"
|
||||
show-add-action="false"
|
||||
show-stack-column="false"
|
||||
not-auto-focus="true"
|
||||
></services-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,9 +88,10 @@
|
||||
"jquery": "^3.5.1",
|
||||
"js-base64": "^3.6.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"lodash-es": "^4.17.15",
|
||||
"lodash-es": "^4.17.15",
|
||||
"moment": "^2.21.0",
|
||||
"ng-file-upload": "~12.2.13",
|
||||
"parse-duration": "^1.0.0",
|
||||
"source-map-loader": "^1.1.2",
|
||||
"spinkit": "^2.0.1",
|
||||
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
||||
|
||||
@@ -7967,6 +7967,11 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5:
|
||||
pbkdf2 "^3.0.3"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
parse-duration@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-1.0.0.tgz#8605651745f61088f6fb14045c887526c291858c"
|
||||
integrity sha512-X4kUkCTHU1N/kEbwK9FpUJ0UZQa90VzeczfS704frR30gljxDG0pSziws06XlK+CGRSo/1wtG1mFIdBFQTMQNw==
|
||||
|
||||
parse-filepath@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
|
||||
|
||||
Reference in New Issue
Block a user