mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(templates): allow managing git based templates [EE-2600] (#7855)
Co-authored-by: itsconquest <william.conquest@portainer.io> Co-authored-by: oscarzhou <oscar.zhou@portainer.io> Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
This commit is contained in:
parent
30a2bb0495
commit
c650868fe9
32 changed files with 944 additions and 101 deletions
|
@ -1,7 +1,7 @@
|
|||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { editor, upload } from '@@/BoxSelector/common-options/build-methods';
|
||||
import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
|
||||
class KubeCreateCustomTemplateViewController {
|
||||
|
@ -9,7 +9,7 @@ class KubeCreateCustomTemplateViewController {
|
|||
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
||||
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
|
||||
|
||||
this.methodOptions = [editor, upload];
|
||||
this.methodOptions = [editor, upload, git];
|
||||
|
||||
this.templates = null;
|
||||
this.isTemplateVariablesEnabled = isBE;
|
||||
|
@ -31,6 +31,13 @@ class KubeCreateCustomTemplateViewController {
|
|||
Logo: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
Variables: [],
|
||||
RepositoryURL: '',
|
||||
RepositoryURLValid: false,
|
||||
RepositoryReferenceName: 'refs/heads/main',
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
ComposeFilePathInRepository: 'manifest.yml',
|
||||
};
|
||||
|
||||
this.onChangeFile = this.onChangeFile.bind(this);
|
||||
|
@ -121,6 +128,8 @@ class KubeCreateCustomTemplateViewController {
|
|||
return this.createCustomTemplateFromFileContent(template);
|
||||
case 'upload':
|
||||
return this.createCustomTemplateFromFileUpload(template);
|
||||
case 'repository':
|
||||
return this.createCustomTemplateFromGitRepository(template);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,6 +141,10 @@ class KubeCreateCustomTemplateViewController {
|
|||
return this.CustomTemplateService.createCustomTemplateFromFileUpload(template);
|
||||
}
|
||||
|
||||
createCustomTemplateFromGitRepository(template) {
|
||||
return this.CustomTemplateService.createCustomTemplateFromGitRepository(template);
|
||||
}
|
||||
|
||||
validateForm(method) {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@
|
|||
<file-upload-description> You can upload a Manifest file from your computer. </file-upload-description>
|
||||
</file-upload-form>
|
||||
|
||||
<git-form deploy-method="kubernetes" ng-if="$ctrl.state.method === 'repository'" value="$ctrl.formValues" on-change="($ctrl.handleChange)"></git-form>
|
||||
|
||||
<custom-templates-variables-definition-field
|
||||
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||
value="$ctrl.formValues.Variables"
|
||||
|
|
|
@ -3,6 +3,7 @@ import { AccessControlFormData } from '@/portainer/components/accessControlForm/
|
|||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { getFilePreview } from '@/react/portainer/gitops/gitops.service';
|
||||
|
||||
class KubeEditCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
|
@ -11,11 +12,18 @@ class KubeEditCustomTemplateViewController {
|
|||
|
||||
this.isTemplateVariablesEnabled = isBE;
|
||||
|
||||
this.formValues = null;
|
||||
this.formValues = {
|
||||
Variables: [],
|
||||
TLSSkipVerify: false,
|
||||
};
|
||||
this.state = {
|
||||
formValidationError: '',
|
||||
isEditorDirty: false,
|
||||
isTemplateValid: true,
|
||||
isEditorReadOnly: false,
|
||||
templateLoadFailed: false,
|
||||
templatePreviewFailed: false,
|
||||
templatePreviewError: '',
|
||||
};
|
||||
this.templates = [];
|
||||
|
||||
|
@ -25,6 +33,7 @@ class KubeEditCustomTemplateViewController {
|
|||
this.onBeforeUnload = this.onBeforeUnload.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.onVariablesChange = this.onVariablesChange.bind(this);
|
||||
this.previewFileFromGitRepository = this.previewFileFromGitRepository.bind(this);
|
||||
}
|
||||
|
||||
getTemplate() {
|
||||
|
@ -32,13 +41,25 @@ class KubeEditCustomTemplateViewController {
|
|||
try {
|
||||
const { id } = this.$state.params;
|
||||
|
||||
const [template, file] = await Promise.all([this.CustomTemplateService.customTemplate(id), this.CustomTemplateService.customTemplateFile(id)]);
|
||||
template.FileContent = file;
|
||||
const template = await this.CustomTemplateService.customTemplate(id);
|
||||
|
||||
if (template.GitConfig !== null) {
|
||||
this.state.isEditorReadOnly = true;
|
||||
}
|
||||
|
||||
try {
|
||||
template.FileContent = await this.CustomTemplateService.customTemplateFile(id, template.GitConfig !== null);
|
||||
} catch (err) {
|
||||
this.state.templateLoadFailed = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
template.Variables = template.Variables || [];
|
||||
|
||||
this.formValues = template;
|
||||
this.formValues = { ...this.formValues, ...template };
|
||||
|
||||
this.parseTemplate(file);
|
||||
this.parseTemplate(template.FileContent);
|
||||
this.parseGitConfig(template.GitConfig);
|
||||
|
||||
this.oldFileContent = this.formValues.FileContent;
|
||||
|
||||
|
@ -79,6 +100,62 @@ class KubeEditCustomTemplateViewController {
|
|||
}
|
||||
}
|
||||
|
||||
parseGitConfig(config) {
|
||||
if (config === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let flatConfig = {
|
||||
RepositoryURL: config.URL,
|
||||
RepositoryReferenceName: config.ReferenceName,
|
||||
ComposeFilePathInRepository: config.ConfigFilePath,
|
||||
RepositoryAuthentication: config.Authentication !== null,
|
||||
TLSSkipVerify: config.TLSSkipVerify,
|
||||
};
|
||||
|
||||
if (config.Authentication) {
|
||||
flatConfig = {
|
||||
...flatConfig,
|
||||
RepositoryUsername: config.Authentication.Username,
|
||||
RepositoryPassword: config.Authentication.Password,
|
||||
};
|
||||
}
|
||||
|
||||
this.formValues = { ...this.formValues, ...flatConfig };
|
||||
}
|
||||
|
||||
previewFileFromGitRepository() {
|
||||
this.state.templatePreviewFailed = false;
|
||||
this.state.templatePreviewError = '';
|
||||
|
||||
let creds = {};
|
||||
if (this.formValues.RepositoryAuthentication) {
|
||||
creds = {
|
||||
username: this.formValues.RepositoryUsername,
|
||||
password: this.formValues.RepositoryPassword,
|
||||
};
|
||||
}
|
||||
const payload = {
|
||||
repository: this.formValues.RepositoryURL,
|
||||
targetFile: this.formValues.ComposeFilePathInRepository,
|
||||
tlsSkipVerify: this.formValues.TLSSkipVerify,
|
||||
...creds,
|
||||
};
|
||||
|
||||
this.$async(async () => {
|
||||
try {
|
||||
this.formValues.FileContent = await getFilePreview(payload);
|
||||
this.state.isEditorDirty = true;
|
||||
|
||||
// check if the template contains mustache template symbol
|
||||
this.parseTemplate(this.formValues.FileContent);
|
||||
} catch (err) {
|
||||
this.state.templatePreviewError = err.message;
|
||||
this.state.templatePreviewFailed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
|
|
|
@ -7,6 +7,22 @@
|
|||
<form class="form-horizontal" name="$ctrl.form">
|
||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
||||
|
||||
<git-form value="$ctrl.formValues" on-change="($ctrl.handleChange)" ng-if="$ctrl.formValues.GitConfig"></git-form>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12"
|
||||
><button type="button" class="btn btn-sm btn-light !ml-0" ng-if="$ctrl.formValues.GitConfig" ng-click="$ctrl.previewFileFromGitRepository()">
|
||||
<pr-icon icon="'refresh-cw'" feather="true"></pr-icon>Reload custom template</button
|
||||
>
|
||||
</div>
|
||||
<div class="col-sm-12" ng-if="$ctrl.state.templatePreviewFailed">
|
||||
<p class="small vertical-center text-danger mt-5">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>
|
||||
Custom template could not be loaded, {{ $ctrl.state.templatePreviewError }}.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<web-editor-form
|
||||
identifier="template-editor"
|
||||
value="$ctrl.formValues.FileContent"
|
||||
|
@ -14,6 +30,7 @@
|
|||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your manifest file here"
|
||||
read-only="$ctrl.state.isEditorReadOnly"
|
||||
>
|
||||
<editor-description>
|
||||
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>
|
||||
|
@ -31,7 +48,11 @@
|
|||
is-variables-names-from-parent="true"
|
||||
></custom-templates-variables-definition-field>
|
||||
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData" resource-control="$ctrl.formValues.ResourceControl"></por-access-control-form>
|
||||
<por-access-control-form
|
||||
form-data="$ctrl.formValues.AccessControlData"
|
||||
resource-control="$ctrl.formValues.ResourceControl"
|
||||
ng-if="$ctrl.formValues.AccessControlData"
|
||||
></por-access-control-form>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
<div class="form-group">
|
||||
|
|
|
@ -105,6 +105,16 @@
|
|||
></custom-templates-variables-field>
|
||||
</div>
|
||||
|
||||
<span ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.CUSTOM_TEMPLATE && ctrl.state.templateId && ctrl.state.templateLoadFailed">
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="ctrl.currentUser.isAdmin || ctrl.currentUser.id === ctrl.state.template.CreatedByUserId">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
|
||||
<a ui-sref="kubernetes.templates.custom.edit({id: ctrl.state.templateId})">click here</a> for configuration.</p
|
||||
>
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="!(ctrl.currentUser.isAdmin || ctrl.currentUser.id === ctrl.state.template.CreatedByUserId)">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
|
||||
>
|
||||
</span>
|
||||
|
||||
<!-- editor -->
|
||||
<div class="mt-4">
|
||||
<web-editor-form
|
||||
|
@ -115,6 +125,7 @@
|
|||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your manifest file here"
|
||||
read-only="ctrl.state.isEditorReadOnly"
|
||||
>
|
||||
<editor-description>
|
||||
<p class="vertical-center">
|
||||
|
|
|
@ -46,6 +46,13 @@ class KubernetesDeployController {
|
|||
template: null,
|
||||
baseWebhookUrl: baseStackWebhookUrl(),
|
||||
webhookId: createWebhookId(),
|
||||
templateLoadFailed: false,
|
||||
isEditorReadOnly: false,
|
||||
};
|
||||
|
||||
this.currentUser = {
|
||||
isAdmin: false,
|
||||
id: null,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
|
@ -95,7 +102,7 @@ class KubernetesDeployController {
|
|||
const metadata = {
|
||||
type: buildLabel(this.state.BuildMethod),
|
||||
format: formatLabel(this.state.DeployType),
|
||||
role: roleLabel(this.Authentication.isAdmin()),
|
||||
role: roleLabel(this.currentUser.isAdmin),
|
||||
'automatic-updates': automaticUpdatesLabel(this.formValues.RepositoryAutomaticUpdates, this.formValues.RepositoryMechanism),
|
||||
};
|
||||
|
||||
|
@ -183,9 +190,15 @@ class KubernetesDeployController {
|
|||
this.state.template = template;
|
||||
|
||||
try {
|
||||
const fileContent = await this.CustomTemplateService.customTemplateFile(templateId);
|
||||
this.state.templateContent = fileContent;
|
||||
this.onChangeFileContent(fileContent);
|
||||
try {
|
||||
this.state.templateContent = await this.CustomTemplateService.customTemplateFile(templateId, template.GitConfig !== null);
|
||||
this.onChangeFileContent(this.state.templateContent);
|
||||
|
||||
this.state.isEditorReadOnly = true;
|
||||
} catch (err) {
|
||||
this.state.templateLoadFailed = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (template.Variables && template.Variables.length > 0) {
|
||||
const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, '']));
|
||||
|
@ -318,6 +331,9 @@ class KubernetesDeployController {
|
|||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.currentUser.isAdmin = this.Authentication.isAdmin();
|
||||
this.currentUser.id = this.Authentication.getUserDetails().ID;
|
||||
|
||||
this.formValues.namespace_toggle = false;
|
||||
await this.getNamespaces();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue