feat(secrets): allow creating secrets beyond opaque [EE-2625] (#7709)

pull/7727/head
Ali 2022-09-23 16:35:47 +12:00 committed by GitHub
parent 3b2f0ff9eb
commit 4e20d70a99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 659 additions and 135 deletions

View File

@ -1,9 +1,9 @@
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
export default class {
$onInit() {
const secrets = (this.configurations || [])
.filter((config) => config.Data && config.Type === KubernetesConfigurationTypes.SECRET)
.filter((config) => config.Data && config.Type === KubernetesConfigurationKinds.SECRET)
.flatMap((config) => Object.entries(config.Data))
.map(([key, value]) => ({ key, value }));

View File

@ -2,7 +2,7 @@ import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
'$scope',
@ -112,7 +112,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
};
this.hasConfigurationSecrets = function (item) {
return item.Configurations && item.Configurations.some((config) => config.Data && config.Type === KubernetesConfigurationTypes.SECRET);
return item.Configurations && item.Configurations.some((config) => config.Data && config.Type === KubernetesConfigurationKinds.SECRET);
};
/**

View File

@ -3,7 +3,7 @@
<rd-widget-body classes="no-padding">
<!-- table title and action menu -->
<div class="toolBar !flex-col gap-1">
<div class="toolBar vertical-center !gap-x-5 !gap-y-1 flex-wrap !p-0 w-full">
<div class="toolBar vertical-center !gap-x-5 !gap-y-1 flex-wrap !px-0 !py-1 w-full">
<div class="toolBarTitle">
<div class="widget-icon space-right">
<pr-icon icon="'lock'" feather="true"></pr-icon>
@ -125,11 +125,11 @@
</th>
<th>
<table-column-header
col-title="'Type'"
col-title="'Kind'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Type'"
is-sorted-desc="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Type')"
is-sorted="$ctrl.state.orderBy === 'Kind'"
is-sorted-desc="$ctrl.state.orderBy === 'Kind' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Kind')"
></table-column-header>
</th>
<th>
@ -160,7 +160,7 @@
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace })">{{ item.Namespace }}</a>
</td>
<td>{{ item.Type | kubernetesConfigurationTypeText }}</td>
<td>{{ item.Kind | kubernetesConfigurationKindText }}</td>
<td>{{ item.CreationDate | getisodate }} {{ item.ConfigurationOwner ? 'by ' + item.ConfigurationOwner : '' }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">

View File

@ -21,33 +21,70 @@
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="col-sm-12">
<div class="col-sm-12 vertical-center">
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0" ng-click="$ctrl.addEntry()" data-cy="k8sConfigCreate-createEntryButton">
<pr-icon class="vertical-center" icon="'plus'" feather="true"></pr-icon> Create entry
</button>
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0" data-cy="k8sConfigCreate-createConfigsFromFileButton">
<pr-icon class="vertical-center" icon="'upload'" feather="true"></pr-icon> Create key/value from file
<button
ng-if="
!(
($ctrl.isDockerConfig || $ctrl.formValues.Type.name === $ctrl.KubernetesSecretTypes.TLS.name || $ctrl.formValues.Type === $ctrl.KubernetesSecretTypes.TLS.value) &&
$ctrl.formValues.Kind === $ctrl.KubernetesConfigurationKinds.SECRET
)
"
type="button"
class="btn btn-sm btn-default ml-0"
ngf-select="$ctrl.addEntryFromFile($file)"
data-cy="k8sConfigCreate-createConfigsFromFileButton"
>
<pr-icon icon="'upload'" feather="true" class="vertical-center"></pr-icon> Create key/value from file
</button>
<button
ng-if="$ctrl.isDockerConfig && $ctrl.formValues.Kind === $ctrl.KubernetesConfigurationKinds.SECRET"
type="button"
class="btn btn-sm btn-default ml-0"
ngf-select="$ctrl.addEntryFromFile($file)"
ngf-accept="'.json'"
data-cy="k8sConfigCreate-createConfigsFromFileButton"
>
<pr-icon icon="'upload'" feather="true" class="vertical-center"></pr-icon> Upload docker config file
</button>
<button
ng-if="
($ctrl.formValues.Type.name === $ctrl.KubernetesSecretTypes.TLS.name || $ctrl.formValues.Type === $ctrl.KubernetesSecretTypes.TLS.value) &&
$ctrl.formValues.Kind === $ctrl.KubernetesConfigurationKinds.SECRET
"
type="button"
class="btn btn-sm btn-default ml-0"
ngf-select="$ctrl.addEntryFromFile($file)"
data-cy="k8sConfigCreate-createConfigsFromFileButton"
>
<pr-icon icon="'upload'" feather="true" class="vertical-center"></pr-icon> Upload TLS key/cert file
</button>
<portainer-tooltip message="'Maximum upload file size is 1MB'"></portainer-tooltip>
</div>
</div>
<div ng-repeat="(index, entry) in $ctrl.formValues.Data" ng-if="$ctrl.formValues.IsSimple">
<div class="form-group">
<label for="configuration_data_key_{{ index }}" class="col-sm-3 col-lg-2 control-label text-left required">Key</label>
<label for="configuration_data_key_{{ index }}" class="col-sm-3 col-lg-2 control-label text-left required"
>Key
<portainer-tooltip message="'The key must consist of alphanumeric characters, \'-\', \'_\' or \'.\' and be up to 253 characters in length.'"></portainer-tooltip>
</label>
<div class="col-sm-8 col-lg-9">
<input
type="text"
class="form-control"
maxlength="253"
id="configuration_data_key_{{ index }}"
name="configuration_data_key_{{ index }}"
ng-model="$ctrl.formValues.Data[index].Key"
ng-disabled="entry.Used"
ng-disabled="entry.Used || $ctrl.isRequiredKey(entry.Key)"
required
ng-change="$ctrl.onChangeKey(entry)"
/>
<div
class="small text-warning"
style="margin-top: 5px"
class="small text-warning mt-1"
ng-show="
kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid ||
(!entry.Used && $ctrl.state.duplicateKeys[index] !== undefined) ||
@ -55,18 +92,16 @@
"
>
<ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$error">
<p ng-message="required" class="vertical-center">
<pr-icon class="vertical-center" icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required.
</p>
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required. </p>
</ng-messages>
<div>
<p ng-if="$ctrl.state.duplicateKeys[index] !== undefined" class="vertical-center">
<pr-icon class="vertical-center" icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon>This key is already defined.
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon>This key is already defined.
</p>
</div>
<p ng-if="$ctrl.state.invalidKeys[index]" class="vertical-center">
<pr-icon class="vertical-center" icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon> This key is invalid. A valid key must
consist of alphanumeric characters, '-', '_' or '.'
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon> This key is invalid. A valid key must consist of alphanumeric
characters, '-', '_' or '.'
</p>
</div>
</div>
@ -106,10 +141,11 @@
<div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-8">
<button
ng-if="!$ctrl.isRequiredKey(entry.Key) || $ctrl.state.duplicateKeys[index] !== undefined"
type="button"
class="btn btn-sm btn-dangerlight !ml-0"
style="margin-left: 0"
ng-disabled="entry.Used"
ng-disabled="entry.Used || $ctrl.isEntryRequired()"
ng-click="$ctrl.removeEntry(index, entry)"
data-cy="k8sConfigDetail-removeEntryButton{{ index }}"
>

View File

@ -3,6 +3,8 @@ angular.module('portainer.kubernetes').component('kubernetesConfigurationData',
controller: 'KubernetesConfigurationDataController',
bindings: {
formValues: '=',
isDockerConfig: '=',
onChangeValidation: '&',
isValid: '=',
isCreation: '=',
isEditorDirty: '=',

View File

@ -6,11 +6,12 @@ import { Base64 } from 'js-base64';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
class KubernetesConfigurationDataController {
/* @ngInject */
constructor($async) {
this.$async = $async;
constructor($async, Notifications) {
Object.assign(this, { $async, Notifications });
this.editorUpdate = this.editorUpdate.bind(this);
this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
@ -18,6 +19,8 @@ class KubernetesConfigurationDataController {
this.onFileLoadAsync = this.onFileLoadAsync.bind(this);
this.showSimpleMode = this.showSimpleMode.bind(this);
this.showAdvancedMode = this.showAdvancedMode.bind(this);
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.KubernetesSecretTypes = KubernetesSecretTypes;
}
onChangeKey(entry) {
@ -25,6 +28,8 @@ class KubernetesConfigurationDataController {
return;
}
this.onChangeValidation();
this.state.duplicateKeys = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.Data, (data) => data.Key));
this.state.invalidKeys = KubernetesFormValidationHelper.getInvalidKeys(_.map(this.formValues.Data, (data) => data.Key));
this.isValid = Object.keys(this.state.duplicateKeys).length === 0 && Object.keys(this.state.invalidKeys).length === 0;
@ -32,6 +37,85 @@ class KubernetesConfigurationDataController {
addEntry() {
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
// logic for setting required keys for new entries, based on the secret type
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
const newDataIndex = this.formValues.Data.length - 1;
const typeValue = typeof this.formValues.Type === 'string' ? this.formValues.Type : this.formValues.Type.value;
switch (typeValue) {
case this.KubernetesSecretTypes.DOCKERCFG.value:
this.addMissingKeys(['dockercfg'], newDataIndex);
break;
case this.KubernetesSecretTypes.DOCKERCONFIGJSON.value:
this.addMissingKeys(['.dockerconfigjson'], newDataIndex);
break;
case this.KubernetesSecretTypes.BASICAUTH.value:
// only add a required key if there is no required key out of username and password
if (!this.formValues.Data.some((entry) => entry.Key === 'username' || entry.Key === 'password')) {
this.addMissingKeys(['username', 'password'], newDataIndex);
}
break;
case this.KubernetesSecretTypes.SSHAUTH.value:
this.addMissingKeys(['ssh-privatekey'], newDataIndex);
break;
case this.KubernetesSecretTypes.TLS.value:
this.addMissingKeys(['tls.crt', 'tls.key'], newDataIndex);
break;
case this.KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
this.addMissingKeys(['token-id', 'token-secret'], newDataIndex);
break;
default:
break;
}
}
this.onChangeValidation();
}
// addMissingKeys adds the keys in the keys array to the entry at the index provided and stops when the first one is added
addMissingKeys(keys, newDataIndex) {
for (let key of keys) {
if (this.formValues.Data.every((entry) => entry.Key !== key)) {
this.formValues.Data[newDataIndex].Key = key;
return;
}
}
}
isRequiredKey(key) {
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
const secretTypeValue = typeof this.formValues.Type === 'string' ? this.formValues.Type : this.formValues.Type.value;
switch (secretTypeValue) {
case this.KubernetesSecretTypes.DOCKERCONFIGJSON.value:
if (key === '.dockerconfigjson') {
return true;
}
break;
case this.KubernetesSecretTypes.DOCKERCFG.value:
if (key === '.dockercfg') {
return true;
}
break;
case this.KubernetesSecretTypes.SSHAUTH.value:
if (key === 'ssh-privatekey') {
return true;
}
break;
case this.KubernetesSecretTypes.TLS.value:
if (key === 'tls.crt' || key === 'tls.key') {
return true;
}
break;
case this.KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
if (key === 'token-id' || key === 'token-secret') {
return true;
}
break;
default:
break;
}
}
return false;
}
removeEntry(index, entry) {
@ -55,24 +139,77 @@ class KubernetesConfigurationDataController {
}
async onFileLoadAsync(event) {
const entry = new KubernetesConfigurationFormValuesEntry();
const encoding = chardet.detect(Buffer.from(event.target.result));
const decoder = new TextDecoder(encoding);
entry.Key = event.target.fileName;
entry.IsBinary = KubernetesConfigurationHelper.isBinary(encoding);
if (!entry.IsBinary) {
entry.Value = decoder.decode(event.target.result);
} else {
const stringValue = decoder.decode(event.target.result);
entry.Value = Base64.encode(stringValue);
// exit if the file is too big
const maximumFileSizeBytes = 1024 * 1024; // 1MB
if (event.target.result.byteLength > maximumFileSizeBytes) {
this.Notifications.error('File size is too big', 'File size is too big', 'Select a file that is 1MB or smaller.');
return;
}
const entry = new KubernetesConfigurationFormValuesEntry();
try {
const encoding = chardet.detect(Buffer.from(event.target.result));
const decoder = new TextDecoder(encoding);
entry.IsBinary = KubernetesConfigurationHelper.isBinary(encoding);
if (!entry.IsBinary) {
entry.Value = decoder.decode(event.target.result);
} else {
const stringValue = decoder.decode(event.target.result);
entry.Value = Base64.encode(stringValue);
}
} catch (error) {
this.Notifications.error('Failed to upload file', error, 'Failed to upload file');
return;
}
entry.Key = event.target.fileName;
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
if (this.isDockerConfig) {
if (this.formValues.Type.name === this.KubernetesSecretTypes.DOCKERCFG.name) {
entry.Key = '.dockercfg';
} else {
entry.Key = '.dockerconfigjson';
}
}
if (this.formValues.Type.name === this.KubernetesSecretTypes.TLS.name) {
const isCrt = entry.Value.indexOf('BEGIN CERTIFICATE') !== -1;
if (isCrt) {
entry.Key = 'tls.crt';
}
const isKey = entry.Value.indexOf('PRIVATE KEY') !== -1;
if (isKey) {
entry.Key = 'tls.key';
}
}
}
// if this.formValues.Data has a key that matches an existing key, then replace it
const existingEntryIndex = this.formValues.Data.findIndex((data) => data.Key === entry.Key || (data.Value === '' && data.Key === ''));
if (existingEntryIndex !== -1) {
this.formValues.Data[existingEntryIndex] = entry;
} else {
this.formValues.Data.push(entry);
}
this.formValues.Data.push(entry);
this.onChangeKey();
}
isEntryRequired() {
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
const typeValue = typeof this.formValues.Type === 'string' ? this.formValues.Type : this.formValues.Type.value;
if (this.formValues.Data.length === 1) {
if (typeValue !== this.KubernetesSecretTypes.SERVICEACCOUNTTOKEN.value) {
return true;
}
}
}
return false;
}
onFileLoad(event) {
return this.$async(this.onFileLoadAsync, event);
}

View File

@ -1,12 +1,13 @@
import _ from 'lodash-es';
import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfiguration, KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
class KubernetesConfigurationConverter {
static secretToConfiguration(secret) {
const res = new KubernetesConfiguration();
res.Type = KubernetesConfigurationTypes.SECRET;
res.Kind = KubernetesConfigurationKinds.SECRET;
res.Id = secret.Id;
res.Name = secret.Name;
res.Type = secret.Type;
res.Namespace = secret.Namespace;
res.CreationDate = secret.CreationDate;
res.Yaml = secret.Yaml;
@ -21,7 +22,7 @@ class KubernetesConfigurationConverter {
static configMapToConfiguration(configMap) {
const res = new KubernetesConfiguration();
res.Type = KubernetesConfigurationTypes.CONFIGMAP;
res.Kind = KubernetesConfigurationKinds.CONFIGMAP;
res.Id = configMap.Id;
res.Name = configMap.Name;
res.Namespace = configMap.Namespace;

View File

@ -4,12 +4,13 @@ import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
class KubernetesSecretConverter {
static createPayload(secret) {
const res = new KubernetesSecretCreatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.type = secret.Type.value;
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
@ -25,6 +26,11 @@ class KubernetesSecretConverter {
if (annotation !== '') {
res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation;
}
_.forEach(secret.Annotations, (entry) => {
res.metadata.annotations[entry.name] = entry.value;
});
return res;
}
@ -32,6 +38,7 @@ class KubernetesSecretConverter {
const res = new KubernetesSecretUpdatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.type = secret.Type;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
let annotation = '';
@ -54,6 +61,7 @@ class KubernetesSecretConverter {
res.Id = payload.metadata.uid;
res.Name = payload.metadata.name;
res.Namespace = payload.metadata.namespace;
res.Type = payload.type;
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = payload.metadata.creationTimestamp;
@ -84,8 +92,19 @@ class KubernetesSecretConverter {
const res = new KubernetesApplicationSecret();
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Type = formValues.Type;
res.ConfigurationOwner = formValues.ConfigurationOwner;
res.Data = formValues.Data;
switch (formValues.Type) {
case KubernetesSecretTypes.CUSTOM:
res.Type.value = formValues.customType;
break;
case KubernetesSecretTypes.SERVICEACCOUNTTOKEN:
res.Annotations = [{ name: 'kubernetes.io/service-account.name', value: formValues.ServiceAccountName }];
break;
}
return res;
}
}

View File

@ -1,12 +1,12 @@
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
angular.module('portainer.kubernetes').filter('kubernetesConfigurationTypeText', function () {
angular.module('portainer.kubernetes').filter('kubernetesConfigurationKindText', function () {
'use strict';
return function (type) {
switch (type) {
case KubernetesConfigurationTypes.SECRET:
case KubernetesConfigurationKinds.SECRET:
return 'Secret';
case KubernetesConfigurationTypes.CONFIGMAP:
case KubernetesConfigurationKinds.CONFIGMAP:
return 'ConfigMap';
}
};

View File

@ -1,7 +1,7 @@
import _ from 'lodash-es';
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
import {
KubernetesApplicationAutoScalerFormValue,
KubernetesApplicationConfigurationFormValue,
@ -147,7 +147,7 @@ class KubernetesApplicationHelper {
/* #region CONFIGURATIONS FV <> ENV & VOLUMES */
static generateConfigurationFormValuesFromEnvAndVolumes(env, volumes, configurations) {
const finalRes = _.flatMap(configurations, (cfg) => {
const filterCondition = cfg.Type === KubernetesConfigurationTypes.CONFIGMAP ? 'valueFrom.configMapKeyRef.name' : 'valueFrom.secretKeyRef.name';
const filterCondition = cfg.Type === KubernetesConfigurationKinds.CONFIGMAP ? 'valueFrom.configMapKeyRef.name' : 'valueFrom.secretKeyRef.name';
const cfgEnv = _.filter(env, [filterCondition, cfg.Name]);
const cfgVol = _.filter(volumes, { configurationName: cfg.Name });
@ -207,7 +207,7 @@ class KubernetesApplicationHelper {
let finalMounts = [];
_.forEach(configurations, (config) => {
const isBasic = config.SelectedConfiguration.Type === KubernetesConfigurationTypes.CONFIGMAP;
const isBasic = config.SelectedConfiguration.Type === KubernetesConfigurationKinds.CONFIGMAP;
if (!config.Overriden) {
const envKeys = _.keys(config.SelectedConfiguration.Data);

View File

@ -1,6 +1,6 @@
import _ from 'lodash-es';
import YAML from 'yaml';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesConfigurationHelper {
@ -8,7 +8,7 @@ class KubernetesConfigurationHelper {
return _.filter(applications, (app) => {
let envFind;
let volumeFind;
if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) {
if (config.Type === KubernetesConfigurationKinds.CONFIGMAP) {
envFind = _.find(app.Env, { valueFrom: { configMapKeyRef: { name: config.Name } } });
volumeFind = _.find(app.Volumes, { configMap: { name: config.Name } });
} else {

View File

@ -1,4 +1,4 @@
import { KubernetesConfigurationTypes } from './models';
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from './models';
/**
* KubernetesConfigurationFormValues Model
@ -8,15 +8,17 @@ const _KubernetesConfigurationFormValues = Object.freeze({
ResourcePool: '',
Name: '',
ConfigurationOwner: '',
Type: KubernetesConfigurationTypes.CONFIGMAP,
Kind: KubernetesConfigurationKinds.CONFIGMAP,
Data: [],
DataYaml: '',
IsSimple: true,
ServiceAccountName: '',
Type: KubernetesSecretTypes.OPAQUE,
});
export class KubernetesConfigurationFormValues {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigurationFormValues)));
Object.assign(this, _KubernetesConfigurationFormValues);
}
}

View File

@ -7,7 +7,7 @@ export const KubernetesPortainerConfigurationDataAnnotation = 'io.portainer.kube
const _KubernetesConfiguration = Object.freeze({
Id: 0,
Name: '',
Type: '',
Kind: '',
Namespace: '',
CreationDate: '',
ConfigurationOwner: '',
@ -23,7 +23,19 @@ export class KubernetesConfiguration {
}
}
export const KubernetesConfigurationTypes = Object.freeze({
export const KubernetesConfigurationKinds = Object.freeze({
CONFIGMAP: 1,
SECRET: 2,
});
export const KubernetesSecretTypes = Object.freeze({
OPAQUE: { name: 'Opaque', value: 'Opaque' },
SERVICEACCOUNTTOKEN: { name: 'Service account token', value: 'kubernetes.io/service-account-token' },
DOCKERCFG: { name: 'Dockercfg', value: 'kubernetes.io/dockercfg' },
DOCKERCONFIGJSON: { name: 'Dockerconfigjson', value: 'kubernetes.io/dockerconfigjson' },
BASICAUTH: { name: 'Basic auth', value: 'kubernetes.io/basic-auth' },
SSHAUTH: { name: 'SSH auth', value: 'kubernetes.io/ssh-auth' },
TLS: { name: 'TLS', value: 'kubernetes.io/tls' },
BOOTSTRAPTOKEN: { name: 'Bootstrap token', value: 'bootstrap.kubernetes.io/token' },
CUSTOM: { name: 'Custom', value: 'Custom' },
});

View File

@ -5,11 +5,13 @@ const _KubernetesApplicationSecret = Object.freeze({
Id: 0,
Name: '',
Namespace: '',
Type: '',
CreationDate: '',
ConfigurationOwner: '',
Yaml: '',
Data: [],
SecretType: '',
Annotations: [],
});
export class KubernetesApplicationSecret {

View File

@ -5,7 +5,7 @@ import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloa
*/
const _KubernetesSecretCreatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(),
type: 'Opaque',
type: '',
data: {},
stringData: {},
});
@ -21,7 +21,7 @@ export class KubernetesSecretCreatePayload {
*/
const _KubernetesSecretUpdatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(),
type: 'Opaque',
type: '',
data: {},
stringData: {},
});

View File

@ -0,0 +1,16 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
export async function getServiceAccounts(environmentId, namespaceId) {
try {
const {
data: { items },
} = await axios.get(urlBuilder(environmentId, namespaceId));
return items;
} catch (error) {
throw parseAxiosError(error);
}
}
function urlBuilder(environmentId, namespaceId) {
return `endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespaceId}/serviceaccounts`;
}

View File

@ -3,7 +3,7 @@ import _ from 'lodash-es';
import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
import KubernetesSecretConverter from 'Kubernetes/converters/secret';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
class KubernetesConfigurationService {
@ -62,7 +62,7 @@ class KubernetesConfigurationService {
async createAsync(formValues) {
formValues.ConfigurationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ConfigurationOwner);
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
await this.KubernetesConfigMapService.create(configMap);
} else {
@ -79,7 +79,7 @@ class KubernetesConfigurationService {
* UPDATE
*/
async updateAsync(formValues, configuration) {
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
configMap.ConfigurationOwner = configuration.ConfigurationOwner;
await this.KubernetesConfigMapService.update(configMap);
@ -98,7 +98,7 @@ class KubernetesConfigurationService {
* DELETE
*/
async deleteAsync(config) {
if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) {
if (config.Kind == KubernetesConfigurationKinds.CONFIGMAP) {
await this.KubernetesConfigMapService.delete(config);
} else {
await this.KubernetesSecretService.delete(config);

View File

@ -14,9 +14,9 @@
<rd-widget-body>
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<!-- name -->
<div class="form-group">
<div class="form-group mb-0">
<label for="configuration_name" class="col-sm-3 col-lg-2 control-label text-left required">Name</label>
<div class="col-sm-8 col-lg-9">
<div class="col-sm-8 col-lg-9 mb-0">
<input
type="text"
class="form-control"
@ -29,21 +29,21 @@
required
data-cy="k8sConfigCreate-nameInput"
/>
</div>
</div>
<div class="form-group" ng-show="kubernetesConfigurationCreationForm.configuration_name.$invalid || ctrl.state.alreadyExist">
<div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-8 col-lg-9 small text-warning">
<div ng-messages="kubernetesConfigurationCreationForm.configuration_name.$error">
<p ng-message="required" class="vertical-center"><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon> This field must consist of lower case alphanumeric
characters, '-' or '.', and contain at most 63 characters, and must start and end with an alphanumeric character.</p
>
<div ng-show="kubernetesConfigurationCreationForm.configuration_name.$invalid || ctrl.state.alreadyExist">
<div class="help-block small text-warning">
<div ng-messages="kubernetesConfigurationCreationForm.configuration_name.$error">
<p ng-message="required" class="vertical-center"><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon> This field must consist of lower case alphanumeric
characters, '-' or '.', and contain at most 63 characters, and must start and end with an alphanumeric character.</p
>
</div>
<p ng-if="ctrl.state.alreadyExist" class="vertical-center"
><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> A configuration with the same name already exists inside the selected
namespace.</p
>
</div>
</div>
<p ng-if="ctrl.state.alreadyExist" class="vertical-center"
><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> A configuration with the same name already exists inside the selected namespace.</p
>
</div>
</div>
<!-- !name -->
@ -80,56 +80,155 @@
<!-- !resource-pool -->
<div ng-if="ctrl.formValues.ResourcePool">
<div class="col-sm-12 form-section-title"> Configuration type </div>
<div class="col-sm-12 form-section-title"> Configuration kind </div>
<div class="form-group">
<div class="col-sm-12 small text-muted"> Select the type of data that you want to save in the configuration. </div>
<div class="col-sm-12 small text-muted"> Select the kind of data that you want to save in the configuration. </div>
</div>
<!-- type options -->
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" />
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'svg-filecode'"></pr-icon>
ConfigMap
</div>
<p>This configuration holds non-sensitive information</p>
</label>
</div>
<div>
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" />
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'lock'" feather="true"></pr-icon>
Secret
</div>
<p>This configuration holds sensitive information</p>
</label>
</div>
<div class="form-group px-[15px] mb-0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationKinds.CONFIGMAP" ng-model="ctrl.formValues.Kind" ng-change="ctrl.onChangeKind()" />
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'svg-filecode'"></pr-icon>
ConfigMap
</div>
<p>This configuration holds non-sensitive information</p>
</label>
</div>
<div>
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationKinds.SECRET" ng-model="ctrl.formValues.Kind" ng-change="ctrl.onChangeKind()" />
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'lock'" feather="true"></pr-icon>
Secret
</div>
<p>This configuration holds sensitive information</p>
</label>
</div>
</div>
</div>
<!-- !type options -->
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.Type == ctrl.KubernetesConfigurationTypes.SECRET"> Information </div>
<div class="form-group" ng-if="ctrl.formValues.Type == ctrl.KubernetesConfigurationTypes.SECRET">
<div class="col-sm-12 small text-muted">
Creating a sensitive configuration will create a Kubernetes Secret of type <code>Opaque</code>. You can find more information about this in the
<a href="https://kubernetes.io/docs/concepts/configuration/secret/#secret-types" target="_blank">official documentation</a>.
<div ng-if="ctrl.formValues.Kind === ctrl.KubernetesConfigurationKinds.SECRET">
<div class="col-sm-12 form-section-title"> Information </div>
<div class="form-group">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
<span>
More information about types of secret can be found in the official
<a class="hyperlink" href="https://kubernetes.io/docs/concepts/configuration/secret/#secret-types" target="_blank">kubernetes documentation</a>.
</span>
</div>
</div>
<div class="form-group">
<label for="configuration_data_type" class="col-sm-3 col-lg-2 control-label text-left">Type</label>
<div class="col-sm-8 col-lg-9">
<select
class="form-control"
id="configuration_data_type"
ng-model="ctrl.formValues.Type"
ng-options="value.name for (name, value) in ctrl.KubernetesSecretTypes"
ng-change="ctrl.onSecretTypeChange()"
></select>
<div class="col-sm-3 col-lg-2"></div>
</div>
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.SERVICEACCOUNTTOKEN" class="col-sm-12 small text-warning vertical-center pt-5">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
<span
>You should only create a service account token Secret object if you can't use the TokenRequest API to obtain a token, and the security exposure of persisting
a non-expiring token credential in a readable API object is acceptable to you. <br />See
<a href="https://kubernetes.io/docs/concepts/configuration/secret/#service-account-token-secrets" target="_blank">service account token secrets</a> in the
kubernetes documentation.</span
>
</div>
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.DOCKERCFG" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
<span>Ensure the Secret data field contains a <code>.dockercfg</code> key whose value is content of a legacy <code>~/.dockercfg</code> file.</span>
</div>
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.DOCKERCONFIGJSON" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
<span>Ensure the Secret data field contains a <code>.dockerconfigjson</code> key whose value is content of a <code>~/.docker/config.json</code> file.</span>
</div>
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.TLS" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
<span>Ensure the Secret data field contains a <code>tls.key</code> key and a <code>tls.crt</code> key.</span>
</div>
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.BOOTSTRAPTOKEN" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
<span
>Ensure the Secret data field contains a <code>token-id</code> key and a <code>token-secret</code> key. See
<a href="https://kubernetes.io/docs/concepts/configuration/secret/#bootstrap-token-secrets" target="_blank">bootstrap token secrets</a> in the kubernetes
documentation for optional keys.</span
>
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.CUSTOM">
<label for="configuration_data_customtype" class="col-sm-3 col-lg-2 control-label text-left required">Custom Type</label>
<div class="col-sm-8 col-lg-9">
<input
type="text"
name="custom_type"
class="form-control"
id="configuration_data_customtype"
ng-model="ctrl.formValues.customType"
ng-pattern="/^[a-z0-9]([a-z0-9-.]{0,61}[a-z0-9])?$/"
required
/>
<div ng-show="kubernetesConfigurationCreationForm.custom_type.$invalid">
<div class="help-block small text-warning">
<div ng-messages="kubernetesConfigurationCreationForm.custom_type.$error">
<p ng-message="required" class="vertical-center"><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon> This field must consist of lower case alphanumeric
characters, '-' or '.', and contain at most 63 characters, and must start and end with an alphanumeric character.</p
>
</div>
</div>
</div>
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypes.SERVICEACCOUNTTOKEN">
<label for="service_account" class="col-sm-3 col-lg-2 control-label text-left required">Service Account</label>
<div class="col-sm-8 col-lg-9">
<select
class="form-control"
id="service_account"
ng-selected="$first"
ng-model="ctrl.formValues.ServiceAccountName"
ng-options="value.metadata.name as value.metadata.name for (name, value) in ctrl.availableServiceAccounts"
data-cy="k8sConfigCreate-serviceAccountDropdown"
ng-change="ctrl.onChangeServiceAccount()"
required
></select>
<div class="help-block small text-warning" ng-messages="kubernetesConfigurationCreationForm.service_account.$error">
<p class="vertical-center" ng-message="required"> <pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon>This field is required.</p>
</div>
</div>
</div>
</div>
<kubernetes-configuration-data
ng-if="ctrl.formValues"
form-values="ctrl.formValues"
is-docker-config="ctrl.state.isDockerConfig"
is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="true"
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>
<div class="form-group" ng-if="ctrl.state.secretWarningMessage">
<div class="col-sm-12 small text-warning vertical-center pt-5">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
<span>{{ ctrl.state.secretWarningMessage }}</span>
</div>
</div>
</div>
<!-- summary -->
@ -150,7 +249,7 @@
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigCreate-CreateConfigButton"
>
<span ng-hide="ctrl.state.actionInProgress">Create {{ ctrl.formValues.Type | kubernetesConfigurationTypeText }}</span>
<span ng-hide="ctrl.state.actionInProgress">Create {{ ctrl.formValues.Kind | kubernetesConfigurationKindText }}</span>
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>

View File

@ -1,43 +1,134 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { getServiceAccounts } from 'Kubernetes/rest/serviceAccount';
import { isConfigurationFormValid } from '../validation';
class KubernetesCreateConfigurationController {
/* @ngInject */
constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService) {
constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService, EndpointProvider) {
this.$async = $async;
this.$state = $state;
this.$window = $window;
this.EndpointProvider = EndpointProvider;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.Authentication = Authentication;
this.KubernetesConfigurationService = KubernetesConfigurationService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.KubernetesSecretTypes = KubernetesSecretTypes;
this.onInit = this.onInit.bind(this);
this.createConfigurationAsync = this.createConfigurationAsync.bind(this);
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
this.onResourcePoolSelectionChangeAsync = this.onResourcePoolSelectionChangeAsync.bind(this);
this.onSecretTypeChange = this.onSecretTypeChange.bind(this);
}
onChangeName() {
const filteredConfigurations = _.filter(this.configurations, (config) => config.Namespace === this.formValues.ResourcePool.Namespace.Name);
const filteredConfigurations = _.filter(
this.configurations,
(config) => config.Namespace === this.formValues.ResourcePool.Namespace.Name && config.Kind === this.formValues.Kind
);
this.state.alreadyExist = _.find(filteredConfigurations, (config) => config.Name === this.formValues.Name) !== undefined;
}
onResourcePoolSelectionChange() {
onChangeKind() {
this.onChangeName();
// if there is no data field, add one
if (this.formValues.Data.length === 0) {
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
}
// if changing back to a secret, that is a service account token, remove the data field
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
this.onSecretTypeChange();
} else {
this.isDockerConfig = false;
}
}
async onResourcePoolSelectionChangeAsync() {
try {
this.onChangeName();
this.availableServiceAccounts = await getServiceAccounts(this.environmentId, this.formValues.ResourcePool.Namespace.Name);
this.formValues.ServiceAccountName = this.availableServiceAccounts.length > 0 ? this.availableServiceAccounts[0].metadata.name : '';
} catch (err) {
this.Notifications.error('Failure', err, 'Unable load service accounts');
}
}
onResourcePoolSelectionChange() {
this.$async(this.onResourcePoolSelectionChangeAsync);
}
onSecretTypeChange() {
switch (this.formValues.Type.value) {
case KubernetesSecretTypes.OPAQUE.value:
case KubernetesSecretTypes.CUSTOM.value:
this.formValues.Data = this.formValues.Data.filter((entry) => entry.Value !== '');
if (this.formValues.Data.length === 0) {
this.addRequiredKeysToForm(['']);
}
this.state.isDockerConfig = false;
break;
case KubernetesSecretTypes.SERVICEACCOUNTTOKEN.value:
// data isn't required for service account tokens, so remove the data fields if they are empty
this.addRequiredKeysToForm([]);
this.state.isDockerConfig = false;
break;
case KubernetesSecretTypes.DOCKERCONFIGJSON.value:
this.addRequiredKeysToForm(['.dockerconfigjson']);
this.state.isDockerConfig = true;
break;
case KubernetesSecretTypes.DOCKERCFG.value:
this.addRequiredKeysToForm(['.dockercfg']);
this.state.isDockerConfig = true;
break;
case KubernetesSecretTypes.BASICAUTH.value:
this.addRequiredKeysToForm(['username', 'password']);
this.state.isDockerConfig = false;
break;
case KubernetesSecretTypes.SSHAUTH.value:
this.addRequiredKeysToForm(['ssh-privatekey']);
this.state.isDockerConfig = false;
break;
case KubernetesSecretTypes.TLS.value:
this.addRequiredKeysToForm(['tls.crt', 'tls.key']);
this.state.isDockerConfig = false;
break;
case KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
this.addRequiredKeysToForm(['token-id', 'token-secret']);
this.state.isDockerConfig = false;
break;
default:
this.state.isDockerConfig = false;
break;
}
this.isFormValid();
}
addRequiredKeysToForm(keys) {
// remove data entries that have an empty value
this.formValues.Data = this.formValues.Data.filter((entry) => entry.Value);
keys.forEach((key) => {
// if the key doesn't exist on the form, add a new formValues.Data entry
if (!this.formValues.Data.some((data) => data.Key === key)) {
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
const index = this.formValues.Data.length - 1;
this.formValues.Data[index].Key = key;
}
});
}
isFormValid() {
const uniqueCheck = !this.state.alreadyExist && this.state.isDataValid;
if (this.formValues.IsSimple) {
return this.formValues.Data.length > 0 && uniqueCheck;
}
return uniqueCheck;
const [isValid, warningMessage] = isConfigurationFormValid(this.state.alreadyExist, this.state.isDataValid, this.formValues);
this.state.secretWarningMessage = warningMessage;
return isValid;
}
async createConfigurationAsync() {
@ -47,6 +138,7 @@ class KubernetesCreateConfigurationController {
if (!this.formValues.IsSimple) {
this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues);
}
await this.KubernetesConfigurationService.create(this.formValues);
this.Notifications.success('Success', 'Configuration succesfully created');
this.state.isEditorDirty = false;
@ -87,10 +179,12 @@ class KubernetesCreateConfigurationController {
alreadyExist: false,
isDataValid: true,
isEditorDirty: false,
isDockerConfig: false,
secretWarningMessage: '',
};
this.formValues = new KubernetesConfigurationFormValues();
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
this.formValues.Data = [new KubernetesConfigurationFormValuesEntry()];
try {
const resourcePools = await this.KubernetesResourcePoolService.get();
@ -98,6 +192,10 @@ class KubernetesCreateConfigurationController {
this.formValues.ResourcePool = this.resourcePools[0];
await this.getConfigurations();
this.environmentId = this.EndpointProvider.endpointID();
this.availableServiceAccounts = await getServiceAccounts(this.environmentId, this.resourcePools[0].Namespace.Name);
this.formValues.ServiceAccountName = this.availableServiceAccounts.length > 0 ? this.availableServiceAccounts[0].metadata.name : '';
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {

View File

@ -46,9 +46,15 @@
</td>
</tr>
<tr>
<td class="!pl-0">Configuration type</td>
<td class="!pl-0">Configuration kind</td>
<td>
{{ ctrl.configuration.Type | kubernetesConfigurationTypeText }}
{{ ctrl.configuration.Kind | kubernetesConfigurationKindText }}
</td>
</tr>
<tr ng-if="ctrl.secretTypeName">
<td class="!pl-0">Secret Type</td>
<td>
{{ ctrl.secretTypeName }}
</td>
</tr>
</tbody>
@ -99,11 +105,20 @@
<kubernetes-configuration-data
ng-if="ctrl.formValues"
form-values="ctrl.formValues"
is-docker-config="ctrl.state.isDockerConfig"
is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="false"
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>
<div class="form-group" ng-if="ctrl.state.secretWarningMessage">
<div class="col-sm-12 small text-warning vertical-center pt-5">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
<span>{{ ctrl.state.secretWarningMessage }}</span>
</div>
</div>
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress)"
@ -122,7 +137,7 @@
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigDetail-updateConfig"
>
<span ng-hide="ctrl.state.actionInProgress">Update {{ ctrl.configuration.Type | kubernetesConfigurationTypeText }}</span>
<span ng-hide="ctrl.state.actionInProgress">Update {{ ctrl.configuration.Kind | kubernetesConfigurationKindText }}</span>
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
</button>
</div>

View File

@ -2,12 +2,14 @@ import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { isConfigurationFormValid } from '../validation';
class KubernetesConfigurationController {
/* @ngInject */
constructor(
@ -36,7 +38,8 @@ class KubernetesConfigurationController {
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesEventService = KubernetesEventService;
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.KubernetesSecretTypes = KubernetesSecretTypes;
this.KubernetesConfigMapService = KubernetesConfigMapService;
this.KubernetesSecretService = KubernetesSecretService;
@ -76,10 +79,9 @@ class KubernetesConfigurationController {
}
isFormValid() {
if (this.formValues.IsSimple) {
return this.formValues.Data.length > 0 && this.state.isDataValid;
}
return this.state.isDataValid;
const [isValid, warningMessage] = isConfigurationFormValid(this.state.alreadyExist, this.state.isDataValid, this.formValues);
this.state.secretWarningMessage = warningMessage;
return isValid;
}
// TODO: refactor
@ -89,7 +91,7 @@ class KubernetesConfigurationController {
try {
this.state.actionInProgress = true;
if (
this.formValues.Type !== this.configuration.Type ||
this.formValues.Kind !== this.configuration.Kind ||
this.formValues.ResourcePool.Namespace.Name !== this.configuration.Namespace ||
this.formValues.Name !== this.configuration.Name
) {
@ -153,6 +155,7 @@ class KubernetesConfigurationController {
this.formValues.Id = this.configuration.Id;
this.formValues.Name = this.configuration.Name;
this.formValues.Type = this.configuration.Type;
this.formValues.Kind = this.configuration.Kind;
this.oldDataYaml = this.formValues.DataYaml;
return this.configuration;
@ -254,6 +257,8 @@ class KubernetesConfigurationController {
currentName: this.$state.$current.name,
isDataValid: true,
isEditorDirty: false,
isDockerConfig: false,
secretWarningMessage: '',
};
this.state.activeTab = this.LocalStorage.getActiveTab('configuration');
@ -267,6 +272,23 @@ class KubernetesConfigurationController {
await this.getEvents(this.configuration.Namespace);
await this.getConfigurations();
}
// after loading the configuration, check if it is a docker config secret type
if (
this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET &&
(this.formValues.Type === this.KubernetesSecretTypes.DOCKERCONFIGJSON.value || this.formValues.Type === this.KubernetesSecretTypes.DOCKERCFG.value)
) {
this.state.isDockerConfig = true;
}
// convert the secret type to a human readable value
if (this.formValues.Type) {
const secretTypeValues = Object.values(this.KubernetesSecretTypes);
const secretType = secretTypeValues.find((secretType) => secretType.value === this.formValues.Type);
this.secretTypeName = secretType ? secretType.name : this.formValues.Type;
} else {
this.secretTypeName = '';
}
this.tagUsedDataKeys();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');

View File

@ -0,0 +1,59 @@
import { KubernetesSecretTypes } from '@/kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from '@/kubernetes/models/configuration/models';
export function isConfigurationFormValid(alreadyExist, isDataValid, formValues) {
const uniqueCheck = !alreadyExist && isDataValid;
let secretWarningMessage = '';
let isFormValid = false;
if (formValues.IsSimple) {
if (formValues.Kind === KubernetesConfigurationKinds.SECRET) {
let isSecretDataValid = true;
const secretTypeValue = typeof formValues.Type === 'string' ? formValues.Type : formValues.Type.value;
switch (secretTypeValue) {
case KubernetesSecretTypes.SERVICEACCOUNTTOKEN.value:
// data isn't required for service account tokens
isFormValid = uniqueCheck && formValues.ResourcePool;
return [isFormValid, ''];
case KubernetesSecretTypes.DOCKERCFG.value:
// needs to contain a .dockercfg key
isSecretDataValid = formValues.Data.some((entry) => entry.Key === '.dockercfg');
secretWarningMessage = isSecretDataValid ? '' : 'A data entry with a .dockercfg key is required.';
break;
case KubernetesSecretTypes.DOCKERCONFIGJSON.value:
// needs to contain a .dockerconfigjson key
isSecretDataValid = formValues.Data.some((entry) => entry.Key === '.dockerconfigjson');
secretWarningMessage = isSecretDataValid ? '' : 'A data entry with a .dockerconfigjson key. is required.';
break;
case KubernetesSecretTypes.BASICAUTH.value:
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'username' || entry.Key === 'password');
secretWarningMessage = isSecretDataValid ? '' : 'A data entry with a username or password key is required.';
break;
case KubernetesSecretTypes.SSHAUTH.value:
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'ssh-privatekey');
secretWarningMessage = isSecretDataValid ? '' : `A data entry with a 'ssh-privatekey' key is required.`;
break;
case KubernetesSecretTypes.TLS.value:
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'tls.crt') && formValues.Data.some((entry) => entry.Key === 'tls.key');
secretWarningMessage = isSecretDataValid ? '' : `Data entries containing a 'tls.crt' key and a 'tls.key' key are required.`;
break;
case KubernetesSecretTypes.BOOTSTRAPTOKEN.value:
isSecretDataValid = formValues.Data.some((entry) => entry.Key === 'token-id') && formValues.Data.some((entry) => entry.Key === 'token-secret');
secretWarningMessage = isSecretDataValid ? '' : `Data entries containing a 'token-id' key and a 'token-secret' key are required.`;
break;
default:
break;
}
isFormValid = uniqueCheck && formValues.ResourcePool && formValues.Data.length >= 1 && isSecretDataValid;
return [isFormValid, secretWarningMessage];
}
isFormValid = formValues.Data.length > 0 && uniqueCheck && formValues.ResourcePool;
return [isFormValid, secretWarningMessage];
}
isFormValid = uniqueCheck && formValues.ResourcePool;
return [isFormValid, secretWarningMessage];
}

View File

@ -1,13 +1,17 @@
import { KubernetesResourceTypes, KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
const { CREATE, UPDATE } = KubernetesResourceActions;
export default function (formValues) {
const action = formValues.Id ? UPDATE : CREATE;
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
return [{ action, kind: KubernetesResourceTypes.CONFIGMAP, name: formValues.Name }];
} else if (formValues.Type === KubernetesConfigurationTypes.SECRET) {
return [{ action, kind: KubernetesResourceTypes.SECRET, name: formValues.Name }];
} else if (formValues.Kind === KubernetesConfigurationKinds.SECRET) {
let type = typeof formValues.Type === 'string' ? formValues.Type : formValues.Type.name;
if (formValues.customType) {
type = formValues.customType;
}
return [{ action, kind: KubernetesResourceTypes.SECRET, name: formValues.Name, type }];
}
}