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 { export default class {
$onInit() { $onInit() {
const secrets = (this.configurations || []) 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)) .flatMap((config) => Object.entries(config.Data))
.map(([key, value]) => ({ key, value })); .map(([key, value]) => ({ key, value }));

View File

@ -2,7 +2,7 @@ import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; 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', [ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
'$scope', '$scope',
@ -112,7 +112,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
}; };
this.hasConfigurationSecrets = function (item) { 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"> <rd-widget-body classes="no-padding">
<!-- table title and action menu --> <!-- table title and action menu -->
<div class="toolBar !flex-col gap-1"> <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="toolBarTitle">
<div class="widget-icon space-right"> <div class="widget-icon space-right">
<pr-icon icon="'lock'" feather="true"></pr-icon> <pr-icon icon="'lock'" feather="true"></pr-icon>
@ -125,11 +125,11 @@
</th> </th>
<th> <th>
<table-column-header <table-column-header
col-title="'Type'" col-title="'Kind'"
can-sort="true" can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Type'" is-sorted="$ctrl.state.orderBy === 'Kind'"
is-sorted-desc="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder" is-sorted-desc="$ctrl.state.orderBy === 'Kind' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Type')" ng-click="$ctrl.changeOrderBy('Kind')"
></table-column-header> ></table-column-header>
</th> </th>
<th> <th>
@ -160,7 +160,7 @@
<td> <td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace })">{{ item.Namespace }}</a> <a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace })">{{ item.Namespace }}</a>
</td> </td>
<td>{{ item.Type | kubernetesConfigurationTypeText }}</td> <td>{{ item.Kind | kubernetesConfigurationKindText }}</td>
<td>{{ item.CreationDate | getisodate }} {{ item.ConfigurationOwner ? 'by ' + item.ConfigurationOwner : '' }}</td> <td>{{ item.CreationDate | getisodate }} {{ item.ConfigurationOwner ? 'by ' + item.ConfigurationOwner : '' }}</td>
</tr> </tr>
<tr ng-if="!$ctrl.dataset"> <tr ng-if="!$ctrl.dataset">

View File

@ -21,33 +21,70 @@
</div> </div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple"> <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"> <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 <pr-icon class="vertical-center" icon="'plus'" feather="true"></pr-icon> Create entry
</button> </button>
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0" data-cy="k8sConfigCreate-createConfigsFromFileButton"> <button
<pr-icon class="vertical-center" icon="'upload'" feather="true"></pr-icon> Create key/value from file 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>
<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> </div>
<div ng-repeat="(index, entry) in $ctrl.formValues.Data" ng-if="$ctrl.formValues.IsSimple"> <div ng-repeat="(index, entry) in $ctrl.formValues.Data" ng-if="$ctrl.formValues.IsSimple">
<div class="form-group"> <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"> <div class="col-sm-8 col-lg-9">
<input <input
type="text" type="text"
class="form-control" class="form-control"
maxlength="253"
id="configuration_data_key_{{ index }}" id="configuration_data_key_{{ index }}"
name="configuration_data_key_{{ index }}" name="configuration_data_key_{{ index }}"
ng-model="$ctrl.formValues.Data[index].Key" ng-model="$ctrl.formValues.Data[index].Key"
ng-disabled="entry.Used" ng-disabled="entry.Used || $ctrl.isRequiredKey(entry.Key)"
required required
ng-change="$ctrl.onChangeKey(entry)" ng-change="$ctrl.onChangeKey(entry)"
/> />
<div <div
class="small text-warning" class="small text-warning mt-1"
style="margin-top: 5px"
ng-show=" ng-show="
kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid || kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid ||
(!entry.Used && $ctrl.state.duplicateKeys[index] !== undefined) || (!entry.Used && $ctrl.state.duplicateKeys[index] !== undefined) ||
@ -55,18 +92,16 @@
" "
> >
<ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$error"> <ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$error">
<p ng-message="required" class="vertical-center"> <p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required. </p>
<pr-icon class="vertical-center" icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required.
</p>
</ng-messages> </ng-messages>
<div> <div>
<p ng-if="$ctrl.state.duplicateKeys[index] !== undefined" class="vertical-center"> <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> </p>
</div> </div>
<p ng-if="$ctrl.state.invalidKeys[index]" class="vertical-center"> <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 <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
consist of alphanumeric characters, '-', '_' or '.' characters, '-', '_' or '.'
</p> </p>
</div> </div>
</div> </div>
@ -106,10 +141,11 @@
<div class="col-sm-3 col-lg-2"></div> <div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-8"> <div class="col-sm-8">
<button <button
ng-if="!$ctrl.isRequiredKey(entry.Key) || $ctrl.state.duplicateKeys[index] !== undefined"
type="button" type="button"
class="btn btn-sm btn-dangerlight !ml-0" class="btn btn-sm btn-dangerlight !ml-0"
style="margin-left: 0" style="margin-left: 0"
ng-disabled="entry.Used" ng-disabled="entry.Used || $ctrl.isEntryRequired()"
ng-click="$ctrl.removeEntry(index, entry)" ng-click="$ctrl.removeEntry(index, entry)"
data-cy="k8sConfigDetail-removeEntryButton{{ index }}" data-cy="k8sConfigDetail-removeEntryButton{{ index }}"
> >

View File

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

View File

@ -6,11 +6,12 @@ import { Base64 } from 'js-base64';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues'; import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationKinds, KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
class KubernetesConfigurationDataController { class KubernetesConfigurationDataController {
/* @ngInject */ /* @ngInject */
constructor($async) { constructor($async, Notifications) {
this.$async = $async; Object.assign(this, { $async, Notifications });
this.editorUpdate = this.editorUpdate.bind(this); this.editorUpdate = this.editorUpdate.bind(this);
this.editorUpdateAsync = this.editorUpdateAsync.bind(this); this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
@ -18,6 +19,8 @@ class KubernetesConfigurationDataController {
this.onFileLoadAsync = this.onFileLoadAsync.bind(this); this.onFileLoadAsync = this.onFileLoadAsync.bind(this);
this.showSimpleMode = this.showSimpleMode.bind(this); this.showSimpleMode = this.showSimpleMode.bind(this);
this.showAdvancedMode = this.showAdvancedMode.bind(this); this.showAdvancedMode = this.showAdvancedMode.bind(this);
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.KubernetesSecretTypes = KubernetesSecretTypes;
} }
onChangeKey(entry) { onChangeKey(entry) {
@ -25,6 +28,8 @@ class KubernetesConfigurationDataController {
return; return;
} }
this.onChangeValidation();
this.state.duplicateKeys = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.Data, (data) => data.Key)); 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.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; this.isValid = Object.keys(this.state.duplicateKeys).length === 0 && Object.keys(this.state.invalidKeys).length === 0;
@ -32,6 +37,85 @@ class KubernetesConfigurationDataController {
addEntry() { addEntry() {
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry()); 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) { removeEntry(index, entry) {
@ -55,24 +139,77 @@ class KubernetesConfigurationDataController {
} }
async onFileLoadAsync(event) { async onFileLoadAsync(event) {
const entry = new KubernetesConfigurationFormValuesEntry(); // exit if the file is too big
const encoding = chardet.detect(Buffer.from(event.target.result)); const maximumFileSizeBytes = 1024 * 1024; // 1MB
const decoder = new TextDecoder(encoding); 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.');
entry.Key = event.target.fileName; return;
entry.IsBinary = KubernetesConfigurationHelper.isBinary(encoding); }
if (!entry.IsBinary) { const entry = new KubernetesConfigurationFormValuesEntry();
entry.Value = decoder.decode(event.target.result); try {
} else { const encoding = chardet.detect(Buffer.from(event.target.result));
const stringValue = decoder.decode(event.target.result); const decoder = new TextDecoder(encoding);
entry.Value = Base64.encode(stringValue);
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(); 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) { onFileLoad(event) {
return this.$async(this.onFileLoadAsync, event); return this.$async(this.onFileLoadAsync, event);
} }

View File

@ -1,12 +1,13 @@
import _ from 'lodash-es'; import _ from 'lodash-es';
import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; import { KubernetesConfiguration, KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
class KubernetesConfigurationConverter { class KubernetesConfigurationConverter {
static secretToConfiguration(secret) { static secretToConfiguration(secret) {
const res = new KubernetesConfiguration(); const res = new KubernetesConfiguration();
res.Type = KubernetesConfigurationTypes.SECRET; res.Kind = KubernetesConfigurationKinds.SECRET;
res.Id = secret.Id; res.Id = secret.Id;
res.Name = secret.Name; res.Name = secret.Name;
res.Type = secret.Type;
res.Namespace = secret.Namespace; res.Namespace = secret.Namespace;
res.CreationDate = secret.CreationDate; res.CreationDate = secret.CreationDate;
res.Yaml = secret.Yaml; res.Yaml = secret.Yaml;
@ -21,7 +22,7 @@ class KubernetesConfigurationConverter {
static configMapToConfiguration(configMap) { static configMapToConfiguration(configMap) {
const res = new KubernetesConfiguration(); const res = new KubernetesConfiguration();
res.Type = KubernetesConfigurationTypes.CONFIGMAP; res.Kind = KubernetesConfigurationKinds.CONFIGMAP;
res.Id = configMap.Id; res.Id = configMap.Id;
res.Name = configMap.Name; res.Name = configMap.Name;
res.Namespace = configMap.Namespace; 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 { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models'; import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues'; import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesSecretTypes } from 'Kubernetes/models/configuration/models';
class KubernetesSecretConverter { class KubernetesSecretConverter {
static createPayload(secret) { static createPayload(secret) {
const res = new KubernetesSecretCreatePayload(); const res = new KubernetesSecretCreatePayload();
res.metadata.name = secret.Name; res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace; res.metadata.namespace = secret.Namespace;
res.type = secret.Type.value;
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' }); const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner; res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
@ -25,6 +26,11 @@ class KubernetesSecretConverter {
if (annotation !== '') { if (annotation !== '') {
res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation; res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation;
} }
_.forEach(secret.Annotations, (entry) => {
res.metadata.annotations[entry.name] = entry.value;
});
return res; return res;
} }
@ -32,6 +38,7 @@ class KubernetesSecretConverter {
const res = new KubernetesSecretUpdatePayload(); const res = new KubernetesSecretUpdatePayload();
res.metadata.name = secret.Name; res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace; res.metadata.namespace = secret.Namespace;
res.type = secret.Type;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner; res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
let annotation = ''; let annotation = '';
@ -54,6 +61,7 @@ class KubernetesSecretConverter {
res.Id = payload.metadata.uid; res.Id = payload.metadata.uid;
res.Name = payload.metadata.name; res.Name = payload.metadata.name;
res.Namespace = payload.metadata.namespace; res.Namespace = payload.metadata.namespace;
res.Type = payload.type;
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : ''; res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = payload.metadata.creationTimestamp; res.CreationDate = payload.metadata.creationTimestamp;
@ -84,8 +92,19 @@ class KubernetesSecretConverter {
const res = new KubernetesApplicationSecret(); const res = new KubernetesApplicationSecret();
res.Name = formValues.Name; res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name; res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Type = formValues.Type;
res.ConfigurationOwner = formValues.ConfigurationOwner; res.ConfigurationOwner = formValues.ConfigurationOwner;
res.Data = formValues.Data; 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; 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'; 'use strict';
return function (type) { return function (type) {
switch (type) { switch (type) {
case KubernetesConfigurationTypes.SECRET: case KubernetesConfigurationKinds.SECRET:
return 'Secret'; return 'Secret';
case KubernetesConfigurationTypes.CONFIGMAP: case KubernetesConfigurationKinds.CONFIGMAP:
return 'ConfigMap'; return 'ConfigMap';
} }
}; };

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { KubernetesConfigurationTypes } from './models'; import { KubernetesConfigurationKinds, KubernetesSecretTypes } from './models';
/** /**
* KubernetesConfigurationFormValues Model * KubernetesConfigurationFormValues Model
@ -8,15 +8,17 @@ const _KubernetesConfigurationFormValues = Object.freeze({
ResourcePool: '', ResourcePool: '',
Name: '', Name: '',
ConfigurationOwner: '', ConfigurationOwner: '',
Type: KubernetesConfigurationTypes.CONFIGMAP, Kind: KubernetesConfigurationKinds.CONFIGMAP,
Data: [], Data: [],
DataYaml: '', DataYaml: '',
IsSimple: true, IsSimple: true,
ServiceAccountName: '',
Type: KubernetesSecretTypes.OPAQUE,
}); });
export class KubernetesConfigurationFormValues { export class KubernetesConfigurationFormValues {
constructor() { 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({ const _KubernetesConfiguration = Object.freeze({
Id: 0, Id: 0,
Name: '', Name: '',
Type: '', Kind: '',
Namespace: '', Namespace: '',
CreationDate: '', CreationDate: '',
ConfigurationOwner: '', ConfigurationOwner: '',
@ -23,7 +23,19 @@ export class KubernetesConfiguration {
} }
} }
export const KubernetesConfigurationTypes = Object.freeze({ export const KubernetesConfigurationKinds = Object.freeze({
CONFIGMAP: 1, CONFIGMAP: 1,
SECRET: 2, 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, Id: 0,
Name: '', Name: '',
Namespace: '', Namespace: '',
Type: '',
CreationDate: '', CreationDate: '',
ConfigurationOwner: '', ConfigurationOwner: '',
Yaml: '', Yaml: '',
Data: [], Data: [],
SecretType: '', SecretType: '',
Annotations: [],
}); });
export class KubernetesApplicationSecret { export class KubernetesApplicationSecret {

View File

@ -5,7 +5,7 @@ import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloa
*/ */
const _KubernetesSecretCreatePayload = Object.freeze({ const _KubernetesSecretCreatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(), metadata: new KubernetesCommonMetadataPayload(),
type: 'Opaque', type: '',
data: {}, data: {},
stringData: {}, stringData: {},
}); });
@ -21,7 +21,7 @@ export class KubernetesSecretCreatePayload {
*/ */
const _KubernetesSecretUpdatePayload = Object.freeze({ const _KubernetesSecretUpdatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(), metadata: new KubernetesCommonMetadataPayload(),
type: 'Opaque', type: '',
data: {}, data: {},
stringData: {}, 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 KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap'; import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
import KubernetesSecretConverter from 'Kubernetes/converters/secret'; 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'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
class KubernetesConfigurationService { class KubernetesConfigurationService {
@ -62,7 +62,7 @@ class KubernetesConfigurationService {
async createAsync(formValues) { async createAsync(formValues) {
formValues.ConfigurationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ConfigurationOwner); formValues.ConfigurationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ConfigurationOwner);
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
await this.KubernetesConfigMapService.create(configMap); await this.KubernetesConfigMapService.create(configMap);
} else { } else {
@ -79,7 +79,7 @@ class KubernetesConfigurationService {
* UPDATE * UPDATE
*/ */
async updateAsync(formValues, configuration) { async updateAsync(formValues, configuration) {
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
configMap.ConfigurationOwner = configuration.ConfigurationOwner; configMap.ConfigurationOwner = configuration.ConfigurationOwner;
await this.KubernetesConfigMapService.update(configMap); await this.KubernetesConfigMapService.update(configMap);
@ -98,7 +98,7 @@ class KubernetesConfigurationService {
* DELETE * DELETE
*/ */
async deleteAsync(config) { async deleteAsync(config) {
if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) { if (config.Kind == KubernetesConfigurationKinds.CONFIGMAP) {
await this.KubernetesConfigMapService.delete(config); await this.KubernetesConfigMapService.delete(config);
} else { } else {
await this.KubernetesSecretService.delete(config); await this.KubernetesSecretService.delete(config);

View File

@ -14,9 +14,9 @@
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off"> <form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<!-- name --> <!-- 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> <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 <input
type="text" type="text"
class="form-control" class="form-control"
@ -29,21 +29,21 @@
required required
data-cy="k8sConfigCreate-nameInput" data-cy="k8sConfigCreate-nameInput"
/> />
</div> <div ng-show="kubernetesConfigurationCreationForm.configuration_name.$invalid || ctrl.state.alreadyExist">
</div> <div class="help-block small text-warning">
<div class="form-group" ng-show="kubernetesConfigurationCreationForm.configuration_name.$invalid || ctrl.state.alreadyExist"> <div ng-messages="kubernetesConfigurationCreationForm.configuration_name.$error">
<div class="col-sm-3 col-lg-2"></div> <p ng-message="required" class="vertical-center"><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required.</p>
<div class="col-sm-8 col-lg-9 small text-warning"> <p ng-message="pattern" class="vertical-center"
<div ng-messages="kubernetesConfigurationCreationForm.configuration_name.$error"> ><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'" class="vertical-center"></pr-icon> This field must consist of lower case alphanumeric
<p ng-message="required" class="vertical-center"><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This field is required.</p> characters, '-' or '.', and contain at most 63 characters, and must start and end with an alphanumeric character.</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 </div>
characters, '-' or '.', and contain at most 63 characters, and must start and end with an alphanumeric character.</p <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> </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>
</div> </div>
<!-- !name --> <!-- !name -->
@ -80,56 +80,155 @@
<!-- !resource-pool --> <!-- !resource-pool -->
<div ng-if="ctrl.formValues.ResourcePool"> <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="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> </div>
<!-- type options --> <!-- type options -->
<div class="form-group"> <div class="form-group px-[15px] mb-0">
<div class="col-sm-12"> <div class="boxselector_wrapper">
<div class="boxselector_wrapper"> <div>
<div> <input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationKinds.CONFIGMAP" ng-model="ctrl.formValues.Kind" ng-change="ctrl.onChangeKind()" />
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" /> <label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton"> <div class="boxselector_header">
<div class="boxselector_header"> <pr-icon icon="'svg-filecode'"></pr-icon>
<pr-icon icon="'svg-filecode'"></pr-icon> ConfigMap
ConfigMap </div>
</div> <p>This configuration holds non-sensitive information</p>
<p>This configuration holds non-sensitive information</p> </label>
</label> </div>
</div> <div>
<div> <input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationKinds.SECRET" ng-model="ctrl.formValues.Kind" ng-change="ctrl.onChangeKind()" />
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" /> <label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton"> <div class="boxselector_header">
<div class="boxselector_header"> <pr-icon icon="'lock'" feather="true"></pr-icon>
<pr-icon icon="'lock'" feather="true"></pr-icon> Secret
Secret </div>
</div> <p>This configuration holds sensitive information</p>
<p>This configuration holds sensitive information</p> </label>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- !type options --> <!-- !type options -->
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.Type == ctrl.KubernetesConfigurationTypes.SECRET"> Information </div> <div ng-if="ctrl.formValues.Kind === ctrl.KubernetesConfigurationKinds.SECRET">
<div class="form-group" ng-if="ctrl.formValues.Type == ctrl.KubernetesConfigurationTypes.SECRET"> <div class="col-sm-12 form-section-title"> Information </div>
<div class="col-sm-12 small text-muted"> <div class="form-group">
Creating a sensitive configuration will create a Kubernetes Secret of type <code>Opaque</code>. You can find more information about this in the <div class="col-sm-12 small text-muted vertical-center">
<a href="https://kubernetes.io/docs/concepts/configuration/secret/#secret-types" target="_blank">official documentation</a>. <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>
</div> </div>
<kubernetes-configuration-data <kubernetes-configuration-data
ng-if="ctrl.formValues" ng-if="ctrl.formValues"
form-values="ctrl.formValues" form-values="ctrl.formValues"
is-docker-config="ctrl.state.isDockerConfig"
is-valid="ctrl.state.isDataValid" is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="true" is-creation="true"
is-editor-dirty="ctrl.state.isEditorDirty" is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data> ></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> </div>
<!-- summary --> <!-- summary -->
@ -150,7 +249,7 @@
button-spinner="ctrl.state.actionInProgress" button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigCreate-CreateConfigButton" 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> <span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
</button> </button>
</div> </div>

View File

@ -1,43 +1,134 @@
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash-es'; import _ from 'lodash-es';
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues'; 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 KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { getServiceAccounts } from 'Kubernetes/rest/serviceAccount';
import { isConfigurationFormValid } from '../validation';
class KubernetesCreateConfigurationController { class KubernetesCreateConfigurationController {
/* @ngInject */ /* @ngInject */
constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService) { constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService, EndpointProvider) {
this.$async = $async; this.$async = $async;
this.$state = $state; this.$state = $state;
this.$window = $window; this.$window = $window;
this.EndpointProvider = EndpointProvider;
this.ModalService = ModalService; this.ModalService = ModalService;
this.Notifications = Notifications; this.Notifications = Notifications;
this.Authentication = Authentication; this.Authentication = Authentication;
this.KubernetesConfigurationService = KubernetesConfigurationService; this.KubernetesConfigurationService = KubernetesConfigurationService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService; this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes; this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.KubernetesSecretTypes = KubernetesSecretTypes;
this.onInit = this.onInit.bind(this); this.onInit = this.onInit.bind(this);
this.createConfigurationAsync = this.createConfigurationAsync.bind(this); this.createConfigurationAsync = this.createConfigurationAsync.bind(this);
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this); this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
this.onResourcePoolSelectionChangeAsync = this.onResourcePoolSelectionChangeAsync.bind(this);
this.onSecretTypeChange = this.onSecretTypeChange.bind(this);
} }
onChangeName() { 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; this.state.alreadyExist = _.find(filteredConfigurations, (config) => config.Name === this.formValues.Name) !== undefined;
} }
onResourcePoolSelectionChange() { onChangeKind() {
this.onChangeName(); 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() { isFormValid() {
const uniqueCheck = !this.state.alreadyExist && this.state.isDataValid; const [isValid, warningMessage] = isConfigurationFormValid(this.state.alreadyExist, this.state.isDataValid, this.formValues);
if (this.formValues.IsSimple) { this.state.secretWarningMessage = warningMessage;
return this.formValues.Data.length > 0 && uniqueCheck; return isValid;
}
return uniqueCheck;
} }
async createConfigurationAsync() { async createConfigurationAsync() {
@ -47,6 +138,7 @@ class KubernetesCreateConfigurationController {
if (!this.formValues.IsSimple) { if (!this.formValues.IsSimple) {
this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues); this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues);
} }
await this.KubernetesConfigurationService.create(this.formValues); await this.KubernetesConfigurationService.create(this.formValues);
this.Notifications.success('Success', 'Configuration succesfully created'); this.Notifications.success('Success', 'Configuration succesfully created');
this.state.isEditorDirty = false; this.state.isEditorDirty = false;
@ -87,10 +179,12 @@ class KubernetesCreateConfigurationController {
alreadyExist: false, alreadyExist: false,
isDataValid: true, isDataValid: true,
isEditorDirty: false, isEditorDirty: false,
isDockerConfig: false,
secretWarningMessage: '',
}; };
this.formValues = new KubernetesConfigurationFormValues(); this.formValues = new KubernetesConfigurationFormValues();
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry()); this.formValues.Data = [new KubernetesConfigurationFormValuesEntry()];
try { try {
const resourcePools = await this.KubernetesResourcePoolService.get(); const resourcePools = await this.KubernetesResourcePoolService.get();
@ -98,6 +192,10 @@ class KubernetesCreateConfigurationController {
this.formValues.ResourcePool = this.resourcePools[0]; this.formValues.ResourcePool = this.resourcePools[0];
await this.getConfigurations(); 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) { } catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data'); this.Notifications.error('Failure', err, 'Unable to load view data');
} finally { } finally {

View File

@ -46,9 +46,15 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td class="!pl-0">Configuration type</td> <td class="!pl-0">Configuration kind</td>
<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> </td>
</tr> </tr>
</tbody> </tbody>
@ -99,11 +105,20 @@
<kubernetes-configuration-data <kubernetes-configuration-data
ng-if="ctrl.formValues" ng-if="ctrl.formValues"
form-values="ctrl.formValues" form-values="ctrl.formValues"
is-docker-config="ctrl.state.isDockerConfig"
is-valid="ctrl.state.isDataValid" is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="false" is-creation="false"
is-editor-dirty="ctrl.state.isEditorDirty" is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data> ></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 --> <!-- summary -->
<kubernetes-summary-view <kubernetes-summary-view
ng-if="!(!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress)" ng-if="!(!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress)"
@ -122,7 +137,7 @@
button-spinner="ctrl.state.actionInProgress" button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigDetail-updateConfig" 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> <span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
</button> </button>
</div> </div>

View File

@ -2,12 +2,14 @@ import angular from 'angular';
import _ from 'lodash-es'; import _ from 'lodash-es';
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues'; 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 KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration'; import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { isConfigurationFormValid } from '../validation';
class KubernetesConfigurationController { class KubernetesConfigurationController {
/* @ngInject */ /* @ngInject */
constructor( constructor(
@ -36,7 +38,8 @@ class KubernetesConfigurationController {
this.KubernetesResourcePoolService = KubernetesResourcePoolService; this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesEventService = KubernetesEventService; this.KubernetesEventService = KubernetesEventService;
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes; this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.KubernetesSecretTypes = KubernetesSecretTypes;
this.KubernetesConfigMapService = KubernetesConfigMapService; this.KubernetesConfigMapService = KubernetesConfigMapService;
this.KubernetesSecretService = KubernetesSecretService; this.KubernetesSecretService = KubernetesSecretService;
@ -76,10 +79,9 @@ class KubernetesConfigurationController {
} }
isFormValid() { isFormValid() {
if (this.formValues.IsSimple) { const [isValid, warningMessage] = isConfigurationFormValid(this.state.alreadyExist, this.state.isDataValid, this.formValues);
return this.formValues.Data.length > 0 && this.state.isDataValid; this.state.secretWarningMessage = warningMessage;
} return isValid;
return this.state.isDataValid;
} }
// TODO: refactor // TODO: refactor
@ -89,7 +91,7 @@ class KubernetesConfigurationController {
try { try {
this.state.actionInProgress = true; this.state.actionInProgress = true;
if ( if (
this.formValues.Type !== this.configuration.Type || this.formValues.Kind !== this.configuration.Kind ||
this.formValues.ResourcePool.Namespace.Name !== this.configuration.Namespace || this.formValues.ResourcePool.Namespace.Name !== this.configuration.Namespace ||
this.formValues.Name !== this.configuration.Name this.formValues.Name !== this.configuration.Name
) { ) {
@ -153,6 +155,7 @@ class KubernetesConfigurationController {
this.formValues.Id = this.configuration.Id; this.formValues.Id = this.configuration.Id;
this.formValues.Name = this.configuration.Name; this.formValues.Name = this.configuration.Name;
this.formValues.Type = this.configuration.Type; this.formValues.Type = this.configuration.Type;
this.formValues.Kind = this.configuration.Kind;
this.oldDataYaml = this.formValues.DataYaml; this.oldDataYaml = this.formValues.DataYaml;
return this.configuration; return this.configuration;
@ -254,6 +257,8 @@ class KubernetesConfigurationController {
currentName: this.$state.$current.name, currentName: this.$state.$current.name,
isDataValid: true, isDataValid: true,
isEditorDirty: false, isEditorDirty: false,
isDockerConfig: false,
secretWarningMessage: '',
}; };
this.state.activeTab = this.LocalStorage.getActiveTab('configuration'); this.state.activeTab = this.LocalStorage.getActiveTab('configuration');
@ -267,6 +272,23 @@ class KubernetesConfigurationController {
await this.getEvents(this.configuration.Namespace); await this.getEvents(this.configuration.Namespace);
await this.getConfigurations(); 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(); this.tagUsedDataKeys();
} catch (err) { } catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data'); 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 { 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; const { CREATE, UPDATE } = KubernetesResourceActions;
export default function (formValues) { export default function (formValues) {
const action = formValues.Id ? UPDATE : CREATE; const action = formValues.Id ? UPDATE : CREATE;
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
return [{ action, kind: KubernetesResourceTypes.CONFIGMAP, name: formValues.Name }]; return [{ action, kind: KubernetesResourceTypes.CONFIGMAP, name: formValues.Name }];
} else if (formValues.Type === KubernetesConfigurationTypes.SECRET) { } else if (formValues.Kind === KubernetesConfigurationKinds.SECRET) {
return [{ action, kind: KubernetesResourceTypes.SECRET, name: formValues.Name }]; 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 }];
} }
} }