feat(webhook): teasers of pull images and webhook for EE EE-1332 (#6278)

* feat(webhook): teasers of pull images and webhook for EE
pull/6557/head
Hao Zhang 2022-02-14 21:51:43 +08:00 committed by GitHub
parent fa208c7f2a
commit 37ca62eb06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 212 additions and 49 deletions

View File

@ -3,6 +3,7 @@ import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { FeatureId } from 'Portainer/feature-flags/enums';
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
import { ContainerDetailsViewModel } from '../../../models/container';
@ -65,7 +66,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.create = create;
$scope.update = update;
$scope.endpoint = endpoint;
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
$scope.formValues = {
alwaysPull: true,
Console: 'none',

View File

@ -65,6 +65,28 @@
</por-image-registry>
<!-- !image-and-registry -->
</div>
<!-- create-webhook -->
<div ng-if="isAdmin && applicationState.endpoint.type !== 4">
<div class="col-sm-12 form-section-title"> Webhooks </div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Create a container webhook
<portainer-tooltip
position="top"
message="Create a webhook (or callback URI) to automate the recreate this container. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and recreate this container."
></portainer-tooltip>
</label>
<label class="switch box-selector-item limited business" style="margin-left: 20px">
<input type="checkbox" ng-model="formValues.EnableWebhook" disabled="disabled" ng-checked="true" />
<i class="orange-icon" aria-hidden="true" style="margin-right: 2px"></i>
</label>
<be-feature-indicator feature="containerWebhookFeature"></be-feature-indicator>
</div>
</div>
</div>
<!-- !create-webhook -->
<div class="col-sm-12 form-section-title"> Network ports configuration </div>
<!-- publish-exposed-ports -->
<div class="form-group">

View File

@ -110,6 +110,19 @@
<td>Finished</td>
<td>{{ container.State.FinishedAt | getisodate }}</td>
</tr>
<tr ng-if="isAdmin && displayRecreateButton && applicationState.endpoint.type !== 4">
<td colspan="1">
Container webhook
<portainer-tooltip
position="top"
message="Webhook (or callback URI) used to automate the recreate this container. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and recreate this container."
></portainer-tooltip>
<label class="switch box-selector-item limited business" style="margin-left: 20px">
<input disable-authorization="DockerContainerUpdate" type="checkbox" ng-model="WebhookExists" disabled="disabled" ng-checked="true" /><i></i>
</label>
<be-feature-indicator feature="containerWebhookFeature"></be-feature-indicator>
</td>
</tr>
<tr authorization="DockerContainerLogs, DockerContainerInspect, DockerContainerStats, DockerExecStart">
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">

View File

@ -2,6 +2,7 @@ import moment from 'moment';
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
import { FeatureId } from 'Portainer/feature-flags/enums';
angular.module('portainer.docker').controller('ContainerController', [
'$q',
@ -49,6 +50,7 @@ angular.module('portainer.docker').controller('ContainerController', [
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.displayRecreateButton = false;
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
$scope.config = {
RegistryModel: new PorImageRegistryModel(),

View File

@ -10,6 +10,7 @@ class GitFormAutoUpdateFieldsetController {
this.onChangeInterval = this.onChangeField('RepositoryFetchInterval');
this.limitedFeature = FeatureId.FORCE_REDEPLOYMENT;
this.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE;
}
copyWebhook() {

View File

@ -51,6 +51,11 @@
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<div class="col-sm-12">
<por-switch-field name="forcePullImage" feature-id="$ctrl.stackPullImageFeature" checked="$ctrl.model.ForcePullImage" label="'Pull latest image'"> </por-switch-field>
</div>
</div>
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<div class="col-sm-12">
<por-switch-field

View File

@ -1,16 +1,19 @@
import uuidv4 from 'uuid/v4';
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
import { FeatureId } from 'Portainer/feature-flags/enums';
class StackRedeployGitFormController {
/* @ngInject */
constructor($async, $state, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
constructor($async, $state, $compile, $scope, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
this.$async = $async;
this.$state = $state;
this.$compile = $compile;
this.$scope = $scope;
this.StackService = StackService;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.WebhookHelper = WebhookHelper;
this.FormHelper = FormHelper;
$scope.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE;
this.state = {
inProgress: false,
redeployInProgress: false,
@ -86,27 +89,21 @@ class StackRedeployGitFormController {
}
async submit() {
return this.$async(async () => {
const tplCrop =
'<div>Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption.</div>' +
'<div"><div style="position: absolute; right: 110px; top: 68px; z-index: 999">' +
'<be-feature-indicator feature="stackPullImageFeature"></be-feature-indicator></div></div>';
const template = angular.element(tplCrop);
const html = this.$compile(template)(this.$scope);
this.ModalService.confirmStackUpdate(html, true, true, 'btn-warning', function (result) {
if (!result) {
return;
}
try {
const confirmed = await this.ModalService.confirmAsync({
title: 'Are you sure?',
message: 'Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption.',
buttons: {
confirm: {
label: 'Update',
className: 'btn-warning',
},
},
});
if (!confirmed) {
return;
}
this.state.redeployInProgress = true;
await this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), false, this.formValues);
this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), false, this.formValues);
this.Notifications.success('Pulled and redeployed stack successfully');
await this.$state.reload();
this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Failed redeploying stack');
} finally {

View File

@ -23,4 +23,7 @@ export enum FeatureId {
ACTIVITY_AUDIT = 'activity-audit',
FORCE_REDEPLOYMENT = 'force-redeployment',
HIDE_AUTO_UPDATE_WINDOW = 'hide-auto-update-window',
STACK_PULL_IMAGE = 'stack-pull-image',
STACK_WEBHOOK = 'stack-webhook',
CONTAINER_WEBHOOK = 'container-webhook',
}

View File

@ -27,6 +27,9 @@ export async function init(edition: Edition) {
[FeatureId.TEAM_MEMBERSHIP]: Edition.BE,
[FeatureId.FORCE_REDEPLOYMENT]: Edition.BE,
[FeatureId.HIDE_AUTO_UPDATE_WINDOW]: Edition.BE,
[FeatureId.STACK_PULL_IMAGE]: Edition.BE,
[FeatureId.STACK_WEBHOOK]: Edition.BE,
[FeatureId.CONTAINER_WEBHOOK]: Edition.BE,
};
state.currentEdition = currentEdition;

View File

@ -0,0 +1,16 @@
export const K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota';
export const K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota';
export const RBAC_ROLES = 'rbac-roles';
export const REGISTRY_MANAGEMENT = 'registry-management';
export const K8S_SETUP_DEFAULT = 'k8s-setup-default';
export const S3_BACKUP_SETTING = 's3-backup-setting';
export const HIDE_INTERNAL_AUTHENTICATION_PROMPT = 'hide-internal-authentication-prompt';
export const TEAM_MEMBERSHIP = 'team-membership';
export const HIDE_INTERNAL_AUTH = 'hide-internal-auth';
export const EXTERNAL_AUTH_LDAP = 'external-auth-ldap';
export const ACTIVITY_AUDIT = 'activity-audit';
export const HIDE_AUTO_UPDATE_WINDOW = 'hide-auto-update-window';
export const FORCE_REDEPLOYMENT = 'force-redeployment';
export const STACK_PULL_IMAGE = 'stack-pull-image';
export const STACK_WEBHOOK = 'stack-webhook';
export const CONTAINER_WEBHOOK = 'container-webhook';

View File

@ -22,6 +22,7 @@ import {
confirmContainerDeletion,
confirmContainerRecreation,
confirmServiceForceUpdate,
confirmStackUpdate,
confirmKubeconfigSelection,
selectRegistry,
} from './prompt';
@ -57,6 +58,7 @@ export function ModalServiceAngular() {
confirmChangePassword,
confirmImageExport,
confirmServiceForceUpdate,
confirmStackUpdate,
selectRegistry,
confirmContainerDeletion,
confirmKubeconfigSelection,

View File

@ -1,5 +1,6 @@
import sanitize from 'sanitize-html';
import bootbox from 'bootbox';
import '@/portainer/components/BoxSelector/BoxSelectorItem.css';
import { applyBoxCSS, ButtonsOptions, confirmButtons } from './utils';
@ -136,6 +137,46 @@ export function confirmServiceForceUpdate(
customizeCheckboxPrompt(box, sanitizedMessage);
}
export function confirmStackUpdate(
message: string,
defaultDisabled: boolean,
defaultToggle: boolean,
confirmButtonClassName: string | undefined,
callback: PromptCallback
) {
const box = prompt({
title: 'Are you sure?',
inputType: 'checkbox',
inputOptions: [
{
text: 'Pull latest image version<i></i>',
value: '1',
},
],
buttons: {
confirm: {
label: 'Update',
className: confirmButtonClassName || 'btn-primary',
},
},
callback,
});
box.find('.bootbox-body').prepend(message);
const checkbox = box.find('.bootbox-input-checkbox');
checkbox.prop('checked', defaultToggle);
checkbox.prop('disabled', defaultDisabled);
const checkboxDiv = box.find('.checkbox');
checkboxDiv.removeClass('checkbox');
checkboxDiv.prop(
'style',
'position: relative; display: block; margin-top: 10px; margin-bottom: 10px;'
);
const checkboxLabel = box.find('.form-check-label');
checkboxLabel.addClass('switch box-selector-item limited business');
const switchEle = checkboxLabel.find('i');
switchEle.prop('style', 'margin-left:20px');
}
export function confirmKubeconfigSelection(
options: InputOption[],
expiryMessage: string,

View File

@ -4,6 +4,7 @@ import uuidv4 from 'uuid/v4';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
import { FeatureId } from 'Portainer/feature-flags/enums';
angular
.module('portainer.app')
@ -31,8 +32,9 @@ angular
) {
$scope.onChangeTemplateId = onChangeTemplateId;
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
$scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
$scope.isAdmin = Authentication.isAdmin();
$scope.formValues = {
Name: '',

View File

@ -157,6 +157,25 @@
</editor-description>
</web-editor-form>
<div ng-if="state.Method !== 'repository' && isAdmin && applicationState.endpoint.type !== 4">
<div class="col-sm-12 form-section-title"> Webhooks </div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Create a Stack webhook
<portainer-tooltip
position="top"
message="Create a webhook (or callback URI) to automate the update of this stack. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this stack."
></portainer-tooltip>
</label>
<label class="switch box-selector-item limited business" style="margin-left: 20px">
<input type="checkbox" ng-model="formValues.EnableWebhook" disabled="disabled" ng-checked="true" /><i></i>
</label>
<be-feature-indicator feature="stackWebhookFeature"></be-feature-indicator>
</div>
</div>
</div>
<!-- environment-variables -->
<environment-variables-panel ng-model="formValues.Env" explanation="These values will be used as substitutions in the stack file" on-change="(handleEnvVarChange)">
</environment-variables-panel>

View File

@ -160,6 +160,26 @@
></code-editor>
</div>
</div>
<div ng-if="isAdmin && applicationState.endpoint.type !== 4">
<div class="col-sm-12 form-section-title"> Webhooks </div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Create a Stack webhook
<portainer-tooltip
position="top"
message="Create a webhook (or callback URI) to automate the update of this stack. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this stack."
></portainer-tooltip>
</label>
<label class="switch box-selector-item limited business" style="margin-left: 20px">
<input type="checkbox" ng-model="formValues.EnableWebhook" disabled="disabled" ng-checked="true" /><i></i>
</label>
<be-feature-indicator feature="stackWebhookFeature"></be-feature-indicator>
</div>
</div>
</div>
<!-- environment-variables -->
<div ng-if="stack">
<environment-variables-panel

View File

@ -1,7 +1,9 @@
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { FeatureId } from 'Portainer/feature-flags/enums';
angular.module('portainer.app').controller('StackController', [
'$async',
'$compile',
'$q',
'$scope',
'$state',
@ -27,6 +29,7 @@ angular.module('portainer.app').controller('StackController', [
'endpoint',
function (
$async,
$compile,
$q,
$scope,
$state,
@ -52,6 +55,9 @@ angular.module('portainer.app').controller('StackController', [
endpoint
) {
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
$scope.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE;
$scope.state = {
actionInProgress: false,
migrationInProgress: false,
@ -216,32 +222,43 @@ angular.module('portainer.app').controller('StackController', [
};
$scope.deployStack = function () {
var stackFile = $scope.stackFileContent;
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
var prune = $scope.formValues.Prune;
var stack = $scope.stack;
const stack = $scope.stack;
const tplCrop =
'<div>Do you want to force an update of the stack?</div>' +
'<div style="position: absolute; right: 110px; top: 48px; z-index: 999"><be-feature-indicator feature="stackPullImageFeature"></be-feature-indicator></div>';
const template = angular.element(tplCrop);
const html = $compile(template)($scope);
// 'Do you want to force an update of the stack?'
ModalService.confirmStackUpdate(html, true, true, null, function (result) {
if (!result) {
return;
}
var stackFile = $scope.stackFileContent;
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
var prune = $scope.formValues.Prune;
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
// The EndpointID property is not available for these stacks, we can pass
// the current endpoint identifier as a part of the update request. It will be used if
// the EndpointID property is not defined on the stack.
if (stack.EndpointId === 0) {
stack.EndpointId = endpoint.Id;
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
// The EndpointID property is not available for these stacks, we can pass
// the current endpoint identifier as a part of the update request. It will be used if
// the EndpointID property is not defined on the stack.
if (stack.EndpointId === 0) {
stack.EndpointId = endpoint.Id;
}
$scope.state.actionInProgress = true;
StackService.updateStack(stack, stackFile, env, prune)
.then(function success() {
Notifications.success('Stack successfully deployed');
$scope.state.isEditorDirty = false;
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create stack');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
$scope.state.actionInProgress = true;
StackService.updateStack(stack, stackFile, env, prune)
.then(function success() {
Notifications.success('Stack successfully deployed');
$scope.state.isEditorDirty = false;
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create stack');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
});
};
$scope.editorUpdate = function (cm) {

View File

@ -4547,7 +4547,6 @@ angular-moment-picker@^0.10.2:
dependencies:
angular-mocks "1.6.1"
angular-sanitize "1.6.1"
lodash-es "^4.17.15"
angular-resource@1.8.2:
version "1.8.2"
@ -12598,9 +12597,9 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
lodash-es@^4.17.15, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash-webpack-plugin@^0.11.6: