1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

feat(config): separate configmaps and secrets [EE-5078] (#9029)

This commit is contained in:
Ali 2023-06-12 09:46:48 +12:00 committed by GitHub
parent 4a331b71e1
commit d7fc2046d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 2845 additions and 665 deletions

View file

@ -223,30 +223,64 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const configurations = {
name: 'kubernetes.configurations',
url: '/configurations',
url: '/configurations?tab',
views: {
'content@': {
component: 'kubernetesConfigurationsView',
component: 'kubernetesConfigMapsAndSecretsView',
},
},
params: {
tab: null,
},
};
const configmaps = {
name: 'kubernetes.configmaps',
url: '/configmaps',
abstract: true,
};
const configurationCreation = {
name: 'kubernetes.configurations.new',
const configMapCreation = {
name: 'kubernetes.configmaps.new',
url: '/new',
views: {
'content@': {
component: 'kubernetesCreateConfigurationView',
component: 'kubernetesCreateConfigMapView',
},
},
};
const configuration = {
name: 'kubernetes.configurations.configuration',
const configMap = {
name: 'kubernetes.configmaps.configmap',
url: '/:namespace/:name',
views: {
'content@': {
component: 'kubernetesConfigurationView',
component: 'kubernetesConfigMapView',
},
},
};
const secrets = {
name: 'kubernetes.secrets',
url: '/secrets',
abstract: true,
};
const secretCreation = {
name: 'kubernetes.secrets.new',
url: '/new',
views: {
'content@': {
component: 'kubernetesCreateSecretView',
},
},
};
const secret = {
name: 'kubernetes.secrets.secret',
url: '/:namespace/:name',
views: {
'content@': {
component: 'kubernetesSecretView',
},
},
};
@ -293,7 +327,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const deploy = {
name: 'kubernetes.deploy',
url: '/deploy?templateId&referrer',
url: '/deploy?templateId&referrer&tab',
views: {
'content@': {
component: 'kubernetesDeployView',
@ -418,8 +452,12 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
$stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackLogs);
$stateRegistryProvider.register(configurations);
$stateRegistryProvider.register(configurationCreation);
$stateRegistryProvider.register(configuration);
$stateRegistryProvider.register(configmaps);
$stateRegistryProvider.register(configMapCreation);
$stateRegistryProvider.register(secrets);
$stateRegistryProvider.register(secretCreation);
$stateRegistryProvider.register(configMap);
$stateRegistryProvider.register(secret);
$stateRegistryProvider.register(cluster);
$stateRegistryProvider.register(dashboard);
$stateRegistryProvider.register(deploy);

View file

@ -1,13 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesConfigurationsDatatable', {
templateUrl: './configurationsDatatable.html',
controller: 'KubernetesConfigurationsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
refreshCallback: '<',
removeAction: '<',
},
});

View file

@ -1,5 +1,5 @@
import { PorImageRegistryModel } from '@/docker/models/porImageRegistry';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationPlacementTypes } from './models';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPlacementTypes } from './models';
/**
* KubernetesApplicationFormValues Model
@ -22,8 +22,8 @@ export function KubernetesApplicationFormValues() {
this.EnvironmentVariables = []; // KubernetesApplicationEnvironmentVariableFormValue lis;
this.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
this.PersistedFolders = []; // KubernetesApplicationPersistedFolderFormValue lis;
this.Configurations = []; // KubernetesApplicationConfigurationFormValue lis;
this.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER_IP;
this.ConfigMaps = [];
this.Secrets = [];
this.PublishedPorts = []; // KubernetesApplicationPublishedPortFormValue lis;
this.PlacementType = KubernetesApplicationPlacementTypes.PREFERRED;
this.Placements = []; // KubernetesApplicationPlacementFormValue lis;

View file

@ -6,9 +6,10 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressDatatable';
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
import { DashboardView } from '@/react/kubernetes/DashboardView';
import { ServicesView } from '@/react/kubernetes/ServicesView';
import { DashboardView } from '@/react/kubernetes/dashboard/DashboardView';
import { ServicesView } from '@/react/kubernetes/services/ServicesView';
import { ConsoleView } from '@/react/kubernetes/applications/ConsoleView';
import { ConfigmapsAndSecretsView } from '@/react/kubernetes/configs/ListView/ConfigmapsAndSecretsView';
export const viewsModule = angular
.module('portainer.kubernetes.react.views', [])
@ -27,6 +28,13 @@ export const viewsModule = angular
'kubernetesIngressesCreateView',
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateIngressView))), [])
)
.component(
'kubernetesConfigMapsAndSecretsView',
r2a(
withUIRouter(withReactQuery(withCurrentUser(ConfigmapsAndSecretsView))),
[]
)
)
.component(
'kubernetesDashboardView',
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])

View file

@ -349,28 +349,28 @@
</div>
<!-- #endregion -->
<!-- #region CONFIGURATIONS -->
<!-- #region CONFIGMAPS -->
<div class="form-group">
<div class="col-sm-12 vertical-center">
<label class="control-label !pt-0 text-left">ConfigMap or Secret</label>
<label class="control-label !pt-0 text-left">ConfigMap</label>
</div>
<div class="col-sm-12 small text-muted vertical-center" style="margin-top: 15px" ng-if="ctrl.formValues.Configurations.length">
<div class="col-sm-12 small text-muted vertical-center" style="margin-top: 15px" ng-if="ctrl.formValues.ConfigMaps.length">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Portainer will automatically expose all the keys of a ConfigMap or Secret as environment variables. This behavior can be overridden to filesystem mounts for
each key via the override option.
Portainer will automatically expose all the keys of a ConfigMap as environment variables. This behavior can be overridden to filesystem mounts for each key
via the override option.
</div>
</div>
<!-- config-element -->
<div class="form-inline clearfix" ng-repeat="(index, config) in ctrl.formValues.Configurations">
<div class="form-inline clearfix" ng-repeat="(index, config) in ctrl.formValues.ConfigMaps">
<div class="col-sm-12 !p-0">
<div class="input-group input-group-sm !mr-1">
<span class="input-group-addon">name</span>
<select
class="form-control col-sm-6"
ng-model="config.SelectedConfiguration"
ng-options="c as c.Name for c in ctrl.configurations track by c.Name"
ng-change="ctrl.resetConfiguration(index)"
ng-options="c as c.Name for c in ctrl.configMaps track by c.Name"
ng-change="ctrl.resetConfigMap(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-addConfigSelect_{{ $index }}"
></select>
@ -380,7 +380,7 @@
<label
class="btn btn-md btn-light vertical-center !ml-0"
type="button"
ng-click="ctrl.resetConfiguration(index)"
ng-click="ctrl.resetConfigMap(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-configAutoButton_{{ $index }}"
uib-btn-radio="false"
@ -390,7 +390,7 @@
</label>
<label
class="btn btn-md btn-light vertical-center !ml-0"
ng-click="ctrl.overrideConfiguration(index)"
ng-click="ctrl.overrideConfigMap(index)"
ng-disabled="!config.SelectedConfiguration || ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-configOverrideButton_{{ $index }}"
uib-btn-radio="true"
@ -403,7 +403,7 @@
<button
class="btn btn-md btn-dangerlight btn-only-icon vertical-center"
type="button"
ng-click="ctrl.removeConfiguration(index)"
ng-click="ctrl.removeConfigMap(index)"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-configRemoveButton"
>
@ -414,8 +414,7 @@
<div class="row clearfix" ng-if="config.SelectedConfiguration && !config.Overriden">
<div class="col-sm-9 small text-muted !mt-2 !p-0">
The following keys will be loaded from the <code>{{ config.SelectedConfiguration.Name }}</code>
<span ng-if="config.SelectedConfiguration.Kind === 1">ConfigMap</span><span ng-if="config.SelectedConfiguration.Kind === 2">Secret</span> as environment
variables:
ConfigMap as environment variables:
<span ng-repeat="(key, _) in config.SelectedConfiguration.Data">
<code>{{ key }}</code
>{{ $last ? '' : ', ' }}
@ -451,7 +450,7 @@
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
ng-change="ctrl.onChangeConfigurationPath()"
ng-change="ctrl.onChangeConfigMapPath()"
data-cy="k8sAppCreate-pathOnDiskInput"
/>
</div>
@ -486,11 +485,154 @@
<div class="col-sm-12 !p-0">
<span
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
ng-click="ctrl.addConfiguration()"
ng-click="ctrl.addConfigMap()"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-addConfigButton"
>
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add ConfigMap and Secret
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add ConfigMap
</span>
</div>
<!-- #region SECRETS -->
<div class="form-group">
<div class="col-sm-12 vertical-center">
<label class="control-label !pt-0 text-left">Secret</label>
</div>
<div class="col-sm-12 small text-muted vertical-center" style="margin-top: 15px" ng-if="ctrl.formValues.Configurations.length">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Portainer will automatically expose all the keys of a Secret as environment variables. This behavior can be overridden to filesystem mounts for each key via
the override option.
</div>
</div>
<!-- config-element -->
<div class="form-inline clearfix" ng-repeat="(index, config) in ctrl.formValues.Secrets">
<div class="col-sm-12 !p-0">
<div class="input-group input-group-sm !mr-1">
<span class="input-group-addon">name</span>
<select
class="form-control col-sm-6"
ng-model="config.SelectedConfiguration"
ng-options="c as c.Name for c in ctrl.secrets track by c.Name"
ng-change="ctrl.resetSecret(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-addSecretSelect_{{ $index }}"
></select>
</div>
<div class="input-group btn-group btn-group-sm">
<label
class="btn btn-md btn-light vertical-center !ml-0"
type="button"
ng-click="ctrl.resetSecret(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-secretAutoButton_{{ $index }}"
uib-btn-radio="false"
ng-model="config.Overriden"
>
<pr-icon icon="'rotate-cw'" size="'md'"></pr-icon> Auto
</label>
<label
class="btn btn-md btn-light vertical-center !ml-0"
ng-click="ctrl.overrideSecret(index)"
ng-disabled="!config.SelectedConfiguration || ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-secretOverrideButton_{{ $index }}"
uib-btn-radio="true"
ng-model="config.Overriden"
>
<pr-icon icon="'list'" size="'md'"></pr-icon> Override
</label>
</div>
<button
class="btn btn-md btn-dangerlight btn-only-icon vertical-center"
type="button"
ng-click="ctrl.removeSecret(index)"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-secretRemoveButton"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</div>
<!-- no-override -->
<div class="row clearfix" ng-if="config.SelectedConfiguration && !config.Overriden">
<div class="col-sm-9 small text-muted !mt-2 !p-0">
The following keys will be loaded from the <code>{{ config.SelectedConfiguration.Name }}</code> Secret as environment variables:
<span ng-repeat="(key, _) in config.SelectedConfiguration.Data">
<code>{{ key }}</code
>{{ $last ? '' : ', ' }}
</span>
</div>
</div>
<!-- !no-override -->
<!-- has-override -->
<div class="col-sm-12 !mt-2 !mb-4 !p-0" ng-if="config.Overriden" ng-repeat="(keyIndex, overridenKey) in config.OverridenKeys" style="margin-top: 2px">
<div class="input-group input-group-sm !mr-1">
<span class="input-group-addon">key</span>
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
</div>
<div class="input-group btn-group btn-group-sm !mr-1">
<label class="btn btn-light" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
<pr-icon icon="'list'"></pr-icon> Environment
</label>
<label class="btn btn-light" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<pr-icon icon="'file-text'"></pr-icon> Filesystem
</label>
</div>
<div class="form-group !ml-0 !mr-0 !align-top" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<div class="input-group input-group-sm">
<span class="input-group-addon required">path on disk</span>
<input
type="text"
class="form-control"
ng-model="overridenKey.Path"
placeholder="/etc/myapp/conf.d"
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
ng-change="ctrl.onChangeSecretPath()"
data-cy="k8sAppCreate-secretPathOnDiskInput"
/>
</div>
<div
class="small"
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<div class="text-warning" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<div
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Path is required.</p>
</ng-messages>
<p class="vertical-center" ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This path is already used.</p
>
</div>
</div>
</div>
</div>
</div>
<!-- !has-override -->
</div>
<!-- !config-element -->
<div class="col-sm-12 !p-0">
<span
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
ng-click="ctrl.addSecret()"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-addSecretButton"
>
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add Secret
</span>
</div>
<!-- #endregion -->

View file

@ -4,6 +4,7 @@ import filesizeParser from 'filesize-parser';
import * as JsonPatch from 'fast-json-patch';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { getServices } from '@/react/kubernetes/networks/services/service';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
import {
KubernetesApplicationDataAccessPolicies,
@ -240,20 +241,20 @@ class KubernetesCreateApplicationController {
}
/* #endregion */
/* #region CONFIGURATION UI MANAGEMENT */
addConfiguration() {
/* #region CONFIGMAP UI MANAGEMENT */
addConfigMap() {
let config = new KubernetesApplicationConfigurationFormValue();
config.SelectedConfiguration = this.configurations[0];
this.formValues.Configurations.push(config);
config.SelectedConfiguration = this.configMaps[0];
this.formValues.ConfigMaps.push(config);
}
removeConfiguration(index) {
this.formValues.Configurations.splice(index, 1);
this.onChangeConfigurationPath();
removeConfigMap(index) {
this.formValues.ConfigMaps.splice(index, 1);
this.onChangeConfigMapPath();
}
overrideConfiguration(index) {
const config = this.formValues.Configurations[index];
overrideConfigMap(index) {
const config = this.formValues.ConfigMaps[index];
config.Overriden = true;
config.OverridenKeys = _.map(_.keys(config.SelectedConfiguration.Data), (key) => {
const res = new KubernetesApplicationConfigurationFormValueOverridenKey();
@ -262,22 +263,22 @@ class KubernetesCreateApplicationController {
});
}
resetConfiguration(index) {
const config = this.formValues.Configurations[index];
resetConfigMap(index) {
const config = this.formValues.ConfigMaps[index];
config.Overriden = false;
config.OverridenKeys = [];
this.onChangeConfigurationPath();
this.onChangeConfigMapPath();
}
clearConfigurations() {
this.formValues.Configurations = [];
clearConfigMaps() {
this.formValues.ConfigMaps = [];
}
onChangeConfigurationPath() {
onChangeConfigMapPath() {
this.state.duplicates.configurationPaths.refs = [];
const paths = _.reduce(
this.formValues.Configurations,
this.formValues.ConfigMaps,
(result, config) => {
const uniqOverridenKeysPath = _.uniq(_.map(config.OverridenKeys, 'Path'));
return _.concat(result, uniqOverridenKeysPath);
@ -287,7 +288,7 @@ class KubernetesCreateApplicationController {
const duplicatePaths = KubernetesFormValidationHelper.getDuplicates(paths);
_.forEach(this.formValues.Configurations, (config, index) => {
_.forEach(this.formValues.ConfigMaps, (config, index) => {
_.forEach(config.OverridenKeys, (overridenKey, keyIndex) => {
const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path);
if (findPath) {
@ -300,6 +301,66 @@ class KubernetesCreateApplicationController {
}
/* #endregion */
/* #region SECRET UI MANAGEMENT */
addSecret() {
let secret = new KubernetesApplicationConfigurationFormValue();
secret.SelectedConfiguration = this.secrets[0];
this.formValues.Secrets.push(secret);
}
removeSecret(index) {
this.formValues.Secrets.splice(index, 1);
this.onChangeSecretPath();
}
overrideSecret(index) {
const secret = this.formValues.Secrets[index];
secret.Overriden = true;
secret.OverridenKeys = _.map(_.keys(secret.SelectedConfiguration.Data), (key) => {
const res = new KubernetesApplicationConfigurationFormValueOverridenKey();
res.Key = key;
return res;
});
}
resetSecret(index) {
const secret = this.formValues.Secrets[index];
secret.Overriden = false;
secret.OverridenKeys = [];
this.onChangeSecretPath();
}
clearSecrets() {
this.formValues.Secrets = [];
}
onChangeSecretPath() {
this.state.duplicates.configurationPaths.refs = [];
const paths = _.reduce(
this.formValues.Secrets,
(result, secret) => {
const uniqOverridenKeysPath = _.uniq(_.map(secret.OverridenKeys, 'Path'));
return _.concat(result, uniqOverridenKeysPath);
},
[]
);
const duplicatePaths = KubernetesFormValidationHelper.getDuplicates(paths);
_.forEach(this.formValues.Secrets, (secret, index) => {
_.forEach(secret.OverridenKeys, (overridenKey, keyIndex) => {
const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path);
if (findPath) {
this.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] = findPath;
}
});
});
this.state.duplicates.configurationPaths.hasRefs = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
}
/* #endregion */
/* #region ENVIRONMENT UI MANAGEMENT */
addEnvironmentVariable() {
this.formValues.EnvironmentVariables.push(new KubernetesApplicationEnvironmentVariableFormValue());
@ -959,6 +1020,8 @@ class KubernetesCreateApplicationController {
return this.$async(async () => {
try {
this.configurations = await this.KubernetesConfigurationService.get(namespace);
this.configMaps = this.configurations.filter((configuration) => configuration.Kind === KubernetesConfigurationKinds.CONFIGMAP);
this.secrets = this.configurations.filter((configuration) => configuration.Kind === KubernetesConfigurationKinds.SECRET);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
}
@ -1029,7 +1092,8 @@ class KubernetesCreateApplicationController {
}
resetFormValues() {
this.clearConfigurations();
this.clearConfigMaps();
this.clearSecrets();
this.resetPersistedFolders();
this.resetPublishedPorts();
}
@ -1051,6 +1115,8 @@ class KubernetesCreateApplicationController {
this.state.actionInProgress = true;
try {
this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username;
// combine the secrets and configmap form values when submitting the form
this.formValues.Configurations = [...this.formValues.ConfigMaps, ...this.formValues.Secrets];
_.remove(this.formValues.Configurations, (item) => item.SelectedConfiguration === undefined);
await this.KubernetesApplicationService.create(this.formValues);
this.Notifications.success('Request to deploy application successfully submitted', this.formValues.Name);

View file

@ -0,0 +1,124 @@
<page-header
ng-if="ctrl.state.viewReady"
title="'Create ConfigMap'"
breadcrumbs="[{ label:'ConfigMaps and Secrets', link:'kubernetes.configurations', linkParams:{ tab: 'configmaps' } }, 'Create a ConfigMap']"
reload="true"
></page-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<!-- resource-pool -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label text-left">Namespace</label>
<div class="col-sm-8 col-lg-9">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
data-cy="k8sConfigCreate-namespaceDropdown"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the configuration. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- !resource-pool -->
<!-- name -->
<div class="form-group">
<label for="configuration_name" class="col-sm-3 col-lg-2 control-label required text-left">Name</label>
<div class="col-sm-8 col-lg-9 mb-0">
<input
type="text"
class="form-control"
name="configuration_name"
ng-model="ctrl.formValues.Name"
ng-pattern="/^[a-z0-9]([a-z0-9-.]{0,61}[a-z0-9])?$/"
ng-change="ctrl.onChangeName()"
placeholder="my-configmap"
auto-focus
required
data-cy="k8sConfigCreate-nameInput"
/>
<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'" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" 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'" mode="'warning'"></pr-icon> A configuration with the same name already exists inside the selected namespace.</p
>
</div>
</div>
</div>
</div>
<!-- !name -->
<div class="col-sm-12 !p-0">
<annotations-be-teaser></annotations-be-teaser>
</div>
<div ng-if="ctrl.formValues.ResourcePool">
<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>
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress)"
form-values="ctrl.formValues"
></kubernetes-summary-view>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px"> Actions </div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress || !ctrl.formValues.ResourcePool"
ng-click="ctrl.createConfiguration()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigCreate-CreateConfigButton"
>
<span ng-hide="ctrl.state.actionInProgress">Create {{ ctrl.formValues.Kind | kubernetesConfigurationKindText }}</span>
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
angular.module('portainer.kubernetes').component('kubernetesCreateConfigMapView', {
templateUrl: './createConfigMap.html',
controller: 'KubernetesCreateConfigMapController',
controllerAs: 'ctrl',
});

View file

@ -0,0 +1,170 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { getServiceAccounts } from 'Kubernetes/rest/serviceAccount';
import { typeOptions } from '@/react/kubernetes/configs/CreateView/options';
import { confirmWebEditorDiscard } from '@@/modals/confirm';
import { isConfigurationFormValid } from '../../validation';
class KubernetesCreateConfigMapController {
/* @ngInject */
constructor($async, $state, $scope, $window, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService, EndpointProvider) {
this.$async = $async;
this.$state = $state;
this.$scope = $scope;
this.$window = $window;
this.EndpointProvider = EndpointProvider;
this.Notifications = Notifications;
this.Authentication = Authentication;
this.KubernetesConfigurationService = KubernetesConfigurationService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.typeOptions = typeOptions;
this.onInit = this.onInit.bind(this);
this.createConfigurationAsync = this.createConfigurationAsync.bind(this);
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
this.onResourcePoolSelectionChangeAsync = this.onResourcePoolSelectionChangeAsync.bind(this);
}
onChangeName() {
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;
}
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 to load service accounts');
}
}
onResourcePoolSelectionChange() {
this.$async(this.onResourcePoolSelectionChangeAsync);
}
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 [isValid] = isConfigurationFormValid(this.state.alreadyExist, this.state.isDataValid, this.formValues);
return isValid;
}
async createConfigurationAsync() {
try {
this.state.actionInProgress = true;
this.formValues.ConfigurationOwner = this.Authentication.getUserDetails().username;
if (!this.formValues.IsSimple) {
this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues);
}
await this.KubernetesConfigurationService.create(this.formValues);
this.Notifications.success('Success', `ConfigMap successfully created`);
this.state.isEditorDirty = false;
this.$state.go('kubernetes.configurations', { tab: 'configmaps' });
} catch (err) {
this.Notifications.error('Failure', err, `Unable to create ConfigMap`);
} finally {
this.state.actionInProgress = false;
}
}
createConfiguration() {
return this.$async(this.createConfigurationAsync);
}
async getConfigurationsAsync() {
try {
this.configurations = await this.KubernetesConfigurationService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ConfigMaps');
}
}
getConfigurations() {
return this.$async(this.getConfigurationsAsync);
}
async uiCanExit() {
if (!this.formValues.IsSimple && this.formValues.DataYaml && this.state.isEditorDirty) {
return confirmWebEditorDiscard();
}
}
async onInit() {
this.state = {
actionInProgress: false,
viewReady: false,
alreadyExist: false,
isDataValid: true,
isEditorDirty: false,
isDockerConfig: false,
};
this.formValues = new KubernetesConfigurationFormValues();
this.formValues.Kind = KubernetesConfigurationKinds.CONFIGMAP;
this.formValues.Data = [new KubernetesConfigurationFormValuesEntry()];
try {
const resourcePools = await this.KubernetesResourcePoolService.get();
this.resourcePools = _.filter(
resourcePools,
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
);
this.formValues.ResourcePool = this.resourcePools[0];
if (!this.formValues.ResourcePool) {
return;
}
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 {
this.state.viewReady = true;
}
this.$window.onbeforeunload = () => {
if (!this.formValues.IsSimple && this.formValues.DataYaml && this.state.isEditorDirty) {
return '';
}
};
}
$onDestroy() {
this.state.isEditorDirty = false;
}
$onInit() {
return this.$async(this.onInit);
}
}
export default KubernetesCreateConfigMapController;
angular.module('portainer.kubernetes').controller('KubernetesCreateConfigMapController', KubernetesCreateConfigMapController);

View file

@ -0,0 +1,173 @@
<page-header
ng-if="ctrl.state.viewReady"
title="'ConfigMap details'"
breadcrumbs="[
{ label:'Namespaces', link:'kubernetes.resourcePools' },
{
label:ctrl.configuration.Namespace,
link: 'kubernetes.resourcePools.resourcePool',
linkParams:{ id: ctrl.configuration.Namespace }
},
{ label:'ConfigMaps and Secrets', link:'kubernetes.configurations', linkParams:{ tab: 'configmaps' } },
ctrl.configuration.Name,
]"
reload="true"
>
</page-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body classes="no-padding">
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)" data-cy="k8sConfigDetail-configTab">
<uib-tab-heading>
<pr-icon icon="'file-code'"></pr-icon>
ConfigMap
</uib-tab-heading>
<div style="padding: 20px">
<table class="table" data-cy="k8sConfigDetail-configTable">
<tbody>
<tr>
<td class="w-[40%] !border-none !pl-0">Name</td>
<td class="!border-none">
{{ ctrl.configuration.Name }}
</td>
</tr>
<tr>
<td class="!pl-0">Namespace</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a>
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
</td>
</tr>
</tbody>
</table>
</div>
</uib-tab>
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)" data-cy="k8sConfigDetail-eventsTab">
<uib-tab-heading>
<pr-icon icon="'history'"></pr-icon>
Events
<div ng-if="ctrl.hasEventWarnings()">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
{{ ctrl.state.eventWarningCount }} warning(s)
</div>
</uib-tab-heading>
<kubernetes-events-datatable
title-text="Events"
title-icon="history"
dataset="ctrl.events"
table-key="kubernetes.configuration.events"
order-by="Date"
reverse-order="true"
loading="ctrl.state.eventsLoading"
refresh-callback="ctrl.getEvents"
>
</kubernetes-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
<uib-tab-heading>
<pr-icon icon="'code'"></pr-icon>
YAML
</uib-tab-heading>
<div class="px-5 !pt-5" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"></kubernetes-yaml-inspector>
</div>
</uib-tab>
</uib-tabset>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form ng-if="!ctrl.isSystemConfig()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<div class="col-sm-12 !p-0">
<annotations-be-teaser></annotations-be-teaser>
</div>
<kubernetes-configuration-data
ng-if="ctrl.formValues"
form-values="ctrl.formValues"
is-docker-config="false"
is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="false"
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress)"
form-values="ctrl.formValues"
></kubernetes-summary-view>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px"> Actions </div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress"
ng-click="ctrl.updateConfiguration()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigDetail-updateConfig"
>
<span ng-hide="ctrl.state.actionInProgress">Update {{ ctrl.configuration.Kind | kubernetesConfigurationKindText }}</span>
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
<div ng-if="ctrl.isSystemConfig()">
<div class="col-sm-12 form-section-title" style="margin-top: 10px"> Data </div>
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 10%; border-top: none">Key</td>
<td style="width: 90%; border-top: none">Value</td>
</tr>
<tr ng-repeat="item in ctrl.formValues.Data track by $index">
<td>{{ item.Key }}</td>
<td>
<div style="white-space: pre-wrap">{{ item.Value }}</div>
<div style="margin-top: 2px">
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyConfigurationValue($index)"> <pr-icon icon="'copy'" class-name="'mr-0.5'"></pr-icon>Copy </span>
<span id="copyValueNotification_{{ $index }}" style="display: none; color: #23ae89; margin-left: 5px" class="small">
<pr-icon icon="'check'"></pr-icon> copied
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="ctrl.configuration.Used">
<div class="col-sm-12">
<kubernetes-integrated-applications-datatable
dataset="ctrl.configuration.Applications"
table-key="kubernetes.configurations.applications"
order-by="Name"
refresh-callback="ctrl.getApplications"
title-text="Applications using this ConfigMap"
title-icon="svg-laptopcode"
>
</kubernetes-integrated-applications-datatable>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
angular.module('portainer.kubernetes').component('kubernetesConfigurationView', {
templateUrl: './configuration.html',
controller: 'KubernetesConfigurationController',
angular.module('portainer.kubernetes').component('kubernetesConfigMapView', {
templateUrl: './configMap.html',
controller: 'KubernetesConfigMapController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',

View file

@ -0,0 +1,304 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationKinds } 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 { pluralize } from '@/portainer/helpers/strings';
import { confirmUpdate, confirmWebEditorDiscard } from '@@/modals/confirm';
import { isConfigurationFormValid } from '../../validation';
class KubernetesConfigMapController {
/* @ngInject */
constructor(
$async,
$state,
$window,
clipboard,
EndpointProvider,
Notifications,
LocalStorage,
Authentication,
KubernetesConfigurationService,
KubernetesConfigMapService,
KubernetesResourcePoolService,
KubernetesApplicationService,
KubernetesEventService
) {
this.$async = $async;
this.$state = $state;
this.$window = $window;
this.clipboard = clipboard;
this.EndpointProvider = EndpointProvider;
this.Notifications = Notifications;
this.LocalStorage = LocalStorage;
this.Authentication = Authentication;
this.KubernetesConfigurationService = KubernetesConfigurationService;
this.KubernetesConfigMapService = KubernetesConfigMapService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesEventService = KubernetesEventService;
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.onInit = this.onInit.bind(this);
this.getConfigurationAsync = this.getConfigurationAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
this.getEventsAsync = this.getEventsAsync.bind(this);
this.getApplications = this.getApplications.bind(this);
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
this.updateConfiguration = this.updateConfiguration.bind(this);
this.updateConfigurationAsync = this.updateConfigurationAsync.bind(this);
}
isSystemNamespace() {
return KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace);
}
isSystemConfig() {
return this.isSystemNamespace();
}
selectTab(index) {
this.LocalStorage.storeActiveTab('configuration', index);
}
showEditor() {
this.state.showEditorTab = true;
this.selectTab(2);
}
copyConfigurationValue(idx) {
this.clipboard.copyText(this.formValues.Data[idx].Value);
$('#copyValueNotification_' + idx)
.show()
.fadeOut(2500);
}
isFormValid() {
const [isValid] = isConfigurationFormValid(this.state.alreadyExist, this.state.isDataValid, this.formValues);
return isValid;
}
// TODO: refactor
// It looks like we're still doing a create/delete process but we've decided to get rid of this
// approach.
async updateConfigurationAsync() {
try {
this.state.actionInProgress = true;
if (
this.formValues.Kind !== this.configuration.Kind ||
this.formValues.ResourcePool.Namespace.Name !== this.configuration.Namespace ||
this.formValues.Name !== this.configuration.Name
) {
await this.KubernetesConfigurationService.create(this.formValues);
await this.KubernetesConfigurationService.delete(this.configuration);
this.Notifications.success('Success', `ConfigMap successfully updated`);
this.$state.go(
'kubernetes.configurations.configmap',
{
namespace: this.formValues.ResourcePool.Namespace.Name,
name: this.formValues.Name,
},
{ reload: true }
);
} else {
await this.KubernetesConfigurationService.update(this.formValues, this.configuration);
this.Notifications.success('Success', `ConfigMap successfully updated`);
this.$state.reload(this.$state.current);
}
} catch (err) {
this.Notifications.error('Failure', err, `Unable to update ConfigMap`);
} finally {
this.state.actionInProgress = false;
}
}
updateConfiguration() {
if (this.configuration.Used) {
confirmUpdate(
`The changes will be propagated to ${this.configuration.Applications.length} running ${pluralize(
this.configuration.Applications.length,
'application'
)}. Are you sure you want to update this ConfigMap?`,
(confirmed) => {
if (confirmed) {
return this.$async(this.updateConfigurationAsync);
}
}
);
} else {
return this.$async(this.updateConfigurationAsync);
}
}
async getConfigurationAsync() {
try {
this.state.configurationLoading = true;
const name = this.$transition$.params().name;
const namespace = this.$transition$.params().namespace;
try {
const configMap = await this.KubernetesConfigMapService.get(namespace, name);
this.configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap);
this.formValues.Data = configMap.Data;
} catch (err) {
if (err.status === 403) {
this.$state.go('kubernetes.configurations', { tab: 'configmaps' });
throw new Error('Not authorized to edit ConfigMap');
}
}
this.formValues.ResourcePool = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.configuration.Namespace);
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;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ConfigMap');
} finally {
this.state.configurationLoading = false;
}
}
getConfiguration() {
return this.$async(this.getConfigurationAsync);
}
async getApplicationsAsync(namespace) {
try {
this.state.applicationsLoading = true;
const applications = await this.KubernetesApplicationService.get(namespace);
this.configuration.Applications = KubernetesConfigurationHelper.getUsingApplications(this.configuration, applications);
KubernetesConfigurationHelper.setConfigurationUsed(this.configuration);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
} finally {
this.state.applicationsLoading = false;
}
}
getApplications(namespace) {
return this.$async(this.getApplicationsAsync, namespace);
}
hasEventWarnings() {
return this.state.eventWarningCount;
}
async getEventsAsync(namespace) {
try {
this.state.eventsLoading = true;
this.events = await this.KubernetesEventService.get(namespace);
this.events = _.filter(this.events, (event) => event.Involved.uid === this.configuration.Id);
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
} catch (err) {
this.Notifications('Failure', err, 'Unable to retrieve events');
} finally {
this.state.eventsLoading = false;
}
}
getEvents(namespace) {
return this.$async(this.getEventsAsync, namespace);
}
async getConfigurationsAsync() {
try {
this.configurations = await this.KubernetesConfigurationService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
}
}
getConfigurations() {
return this.$async(this.getConfigurationsAsync);
}
tagUsedDataKeys() {
const configName = this.$transition$.params().name;
const usedDataKeys = _.uniq(
this.configuration.Applications.flatMap((app) =>
app.Env.filter((e) => e.valueFrom && e.valueFrom.configMapKeyRef && e.valueFrom.configMapKeyRef.name === configName).map((e) => e.name)
)
);
this.formValues.Data = this.formValues.Data.map((variable) => {
if (!usedDataKeys.includes(variable.Key)) {
return variable;
}
return { ...variable, Used: true };
});
}
async uiCanExit() {
if (!this.formValues.IsSimple && this.formValues.DataYaml !== this.oldDataYaml && this.state.isEditorDirty) {
return confirmWebEditorDiscard();
}
}
async onInit() {
try {
this.state = {
actionInProgress: false,
configurationLoading: true,
applicationsLoading: true,
eventsLoading: true,
showEditorTab: false,
viewReady: false,
eventWarningCount: 0,
activeTab: 0,
currentName: this.$state.$current.name,
isDataValid: true,
isEditorDirty: false,
};
this.state.activeTab = this.LocalStorage.getActiveTab('configuration');
this.formValues = new KubernetesConfigurationFormValues();
this.resourcePools = await this.KubernetesResourcePoolService.get();
const configuration = await this.getConfiguration();
if (configuration) {
await this.getApplications(this.configuration.Namespace);
await this.getEvents(this.configuration.Namespace);
await this.getConfigurations();
}
this.tagUsedDataKeys();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
this.$window.onbeforeunload = () => {
if (!this.formValues.IsSimple && this.formValues.DataYaml !== this.oldDataYaml && this.state.isEditorDirty) {
return '';
}
};
}
$onInit() {
return this.$async(this.onInit);
}
$onDestroy() {
if (this.state.currentName !== this.$state.$current.name) {
this.LocalStorage.storeActiveTab('configuration', 0);
}
this.state.isEditorDirty = false;
}
}
export default KubernetesConfigMapController;
angular.module('portainer.kubernetes').controller('KubernetesConfigMapController', KubernetesConfigMapController);

View file

@ -1,17 +0,0 @@
<page-header ng-if="ctrl.state.viewReady" title="'ConfigMaps & Secrets list'" breadcrumbs="['ConfigMaps & Secrets']" reload="true"></page-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-sm-12">
<kubernetes-configurations-datatable
dataset="ctrl.configurations"
table-key="kubernetes.configurations"
order-by="Name"
refresh-callback="ctrl.refreshCallback"
remove-action="ctrl.removeAction"
></kubernetes-configurations-datatable>
</div>
</div>
</div>

View file

@ -1,5 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesConfigurationsView', {
templateUrl: './configurations.html',
controller: 'KubernetesConfigurationsController',
controllerAs: 'ctrl',
});

View file

@ -1,239 +0,0 @@
<page-header
ng-if="ctrl.state.viewReady"
title="'Create ConfigMap or Secret'"
breadcrumbs="[{ label:'ConfigMaps and Secrets', link:'kubernetes.configurations' }, 'Create a ConfigMap or Secret']"
reload="true"
></page-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<!-- resource-pool -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label text-left">Namespace</label>
<div class="col-sm-8 col-lg-9">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
data-cy="k8sConfigCreate-namespaceDropdown"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the configuration. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- !resource-pool -->
<!-- name -->
<div class="form-group">
<label for="configuration_name" class="col-sm-3 col-lg-2 control-label required text-left">Name</label>
<div class="col-sm-8 col-lg-9 mb-0">
<input
type="text"
class="form-control"
name="configuration_name"
ng-model="ctrl.formValues.Name"
ng-pattern="/^[a-z0-9]([a-z0-9-.]{0,61}[a-z0-9])?$/"
ng-change="ctrl.onChangeName()"
placeholder="my-configmap-or-secret"
auto-focus
required
data-cy="k8sConfigCreate-nameInput"
/>
<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'" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" 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'" mode="'warning'"></pr-icon> A configuration with the same name already exists inside the selected namespace.</p
>
</div>
</div>
</div>
</div>
<!-- !name -->
<div class="col-sm-12 !p-0">
<annotations-be-teaser></annotations-be-teaser>
</div>
<div ng-if="ctrl.formValues.ResourcePool">
<div class="col-sm-12 form-section-title"> Kind </div>
<div class="form-group">
<div class="col-sm-12 small text-muted"> Select the kind of data that you want to save. </div>
</div>
<box-selector options="ctrl.typeOptions" value="ctrl.formValues.Kind" on-change="(ctrl.onChangeKind)" radio-name="'Kind'" slim="true"> </box-selector>
<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'"></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.value as value.name for (name, value) in ctrl.KubernetesSecretTypeOptions"
ng-change="ctrl.onSecretTypeChange()"
></select>
<div class="col-sm-3 col-lg-2"></div>
</div>
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value" class="col-sm-12 small text-warning vertical-center pt-5">
<pr-icon icon="'alert-triangle'" mode="'warning'"></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.KubernetesSecretTypeOptions.DOCKERCFG.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.TLS.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.BOOTSTRAPTOKEN.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.CUSTOM.value">
<label for="configuration_data_customtype" class="col-sm-3 col-lg-2 control-label required text-left">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'" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" 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.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value">
<label for="service_account" class="col-sm-3 col-lg-2 control-label required text-left">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'" 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'"></pr-icon>
<span>{{ ctrl.state.secretWarningMessage }}</span>
</div>
</div>
</div>
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress)"
form-values="ctrl.formValues"
></kubernetes-summary-view>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px"> Actions </div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress || !ctrl.formValues.ResourcePool"
ng-click="ctrl.createConfiguration()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigCreate-CreateConfigButton"
>
<span ng-hide="ctrl.state.actionInProgress">Create {{ ctrl.formValues.Kind | kubernetesConfigurationKindText }}</span>
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>

View file

@ -1,5 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesCreateConfigurationView', {
templateUrl: './createConfiguration.html',
controller: 'KubernetesCreateConfigurationController',
controllerAs: 'ctrl',
});

View file

@ -0,0 +1,229 @@
<page-header
ng-if="ctrl.state.viewReady"
title="'Create Secret'"
breadcrumbs="[{ label:'ConfigMaps and Secrets', link:'kubernetes.configurations', linkParams:{ tab: 'secrets' } }, 'Create a Secret']"
reload="true"
></page-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<!-- resource-pool -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label text-left">Namespace</label>
<div class="col-sm-8 col-lg-9">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
data-cy="k8sConfigCreate-namespaceDropdown"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the configuration. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- !resource-pool -->
<!-- name -->
<div class="form-group">
<label for="configuration_name" class="col-sm-3 col-lg-2 control-label required text-left">Name</label>
<div class="col-sm-8 col-lg-9 mb-0">
<input
type="text"
class="form-control"
name="configuration_name"
ng-model="ctrl.formValues.Name"
ng-pattern="/^[a-z0-9]([a-z0-9-.]{0,61}[a-z0-9])?$/"
ng-change="ctrl.onChangeName()"
placeholder="my-secret"
auto-focus
required
data-cy="k8sConfigCreate-nameInput"
/>
<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'" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" 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'" mode="'warning'"></pr-icon> A configuration with the same name already exists inside the selected namespace.</p
>
</div>
</div>
</div>
</div>
<!-- !name -->
<div class="col-sm-12 !p-0">
<annotations-be-teaser></annotations-be-teaser>
</div>
<div ng-if="ctrl.formValues.ResourcePool">
<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'"></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.value as value.name for (name, value) in ctrl.KubernetesSecretTypeOptions"
ng-change="ctrl.onSecretTypeChange()"
></select>
<div class="col-sm-3 col-lg-2"></div>
</div>
<div ng-if="ctrl.formValues.Type === ctrl.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value" class="col-sm-12 small text-warning vertical-center pt-5">
<pr-icon icon="'alert-triangle'" mode="'warning'"></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.KubernetesSecretTypeOptions.DOCKERCFG.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.TLS.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.BOOTSTRAPTOKEN.value" class="col-sm-12 small text-muted vertical-center pt-5">
<pr-icon icon="'info'" mode="'primary'"></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.KubernetesSecretTypeOptions.CUSTOM.value">
<label for="configuration_data_customtype" class="col-sm-3 col-lg-2 control-label required text-left">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'" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="pattern" class="vertical-center"
><pr-icon icon="'alert-triangle'" 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.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value">
<label for="service_account" class="col-sm-3 col-lg-2 control-label required text-left">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'" mode="'warning'"></pr-icon>This field is required.</p>
</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'"></pr-icon>
<span>{{ ctrl.state.secretWarningMessage }}</span>
</div>
</div>
</div>
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress)"
form-values="ctrl.formValues"
></kubernetes-summary-view>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px"> Actions </div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress || !ctrl.formValues.ResourcePool"
ng-click="ctrl.createConfiguration()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigCreate-CreateConfigButton"
>
<span ng-hide="ctrl.state.actionInProgress">Create {{ ctrl.formValues.Kind | kubernetesConfigurationKindText }}</span>
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
angular.module('portainer.kubernetes').component('kubernetesCreateSecretView', {
templateUrl: './createSecret.html',
controller: 'KubernetesCreateSecretController',
controllerAs: 'ctrl',
});

View file

@ -8,9 +8,9 @@ import { getServiceAccounts } from 'Kubernetes/rest/serviceAccount';
import { typeOptions } from '@/react/kubernetes/configs/CreateView/options';
import { confirmWebEditorDiscard } from '@@/modals/confirm';
import { isConfigurationFormValid } from '../validation';
import { isConfigurationFormValid } from '../../validation';
class KubernetesCreateConfigurationController {
class KubernetesCreateSecretController {
/* @ngInject */
constructor($async, $state, $scope, $window, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService, EndpointProvider) {
this.$async = $async;
@ -32,7 +32,6 @@ class KubernetesCreateConfigurationController {
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
this.onResourcePoolSelectionChangeAsync = this.onResourcePoolSelectionChangeAsync.bind(this);
this.onSecretTypeChange = this.onSecretTypeChange.bind(this);
this.onChangeKind = this.onChangeKind.bind(this);
}
onChangeName() {
@ -43,30 +42,13 @@ class KubernetesCreateConfigurationController {
this.state.alreadyExist = _.find(filteredConfigurations, (config) => config.Name === this.formValues.Name) !== undefined;
}
onChangeKind(value) {
this.$scope.$evalAsync(() => {
this.formValues.Kind = value;
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');
this.Notifications.error('Failure', err, 'Unable to load service accounts');
}
}
onResourcePoolSelectionChange() {
@ -140,11 +122,6 @@ class KubernetesCreateConfigurationController {
}
async createConfigurationAsync() {
let kind = 'ConfigMap';
if (this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET) {
kind = 'Secret';
}
try {
this.state.actionInProgress = true;
this.formValues.ConfigurationOwner = this.Authentication.getUserDetails().username;
@ -154,11 +131,11 @@ class KubernetesCreateConfigurationController {
await this.KubernetesConfigurationService.create(this.formValues);
this.Notifications.success('Success', `${kind} successfully created`);
this.Notifications.success('Success', `Secret successfully created`);
this.state.isEditorDirty = false;
this.$state.go('kubernetes.configurations');
this.$state.go('kubernetes.configurations', { tab: 'secrets' });
} catch (err) {
this.Notifications.error('Failure', err, `Unable to create ${kind}`);
this.Notifications.error('Failure', err, `Unable to create secret`);
} finally {
this.state.actionInProgress = false;
}
@ -172,7 +149,7 @@ class KubernetesCreateConfigurationController {
try {
this.configurations = await this.KubernetesConfigurationService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ConfigMaps and Secrets');
this.Notifications.error('Failure', err, 'Unable to retrieve Secrets');
}
}
@ -198,6 +175,7 @@ class KubernetesCreateConfigurationController {
};
this.formValues = new KubernetesConfigurationFormValues();
this.formValues.Kind = KubernetesConfigurationKinds.SECRET;
this.formValues.Data = [new KubernetesConfigurationFormValuesEntry()];
try {
@ -235,5 +213,5 @@ class KubernetesCreateConfigurationController {
}
}
export default KubernetesCreateConfigurationController;
angular.module('portainer.kubernetes').controller('KubernetesCreateConfigurationController', KubernetesCreateConfigurationController);
export default KubernetesCreateSecretController;
angular.module('portainer.kubernetes').controller('KubernetesCreateSecretController', KubernetesCreateSecretController);

View file

@ -1,6 +1,6 @@
<page-header
ng-if="ctrl.state.viewReady"
title="'ConfigMap or Secret details'"
title="'Secret details'"
breadcrumbs="[
{ label:'Namespaces', link:'kubernetes.resourcePools' },
{
@ -8,7 +8,7 @@
link: 'kubernetes.resourcePools.resourcePool',
linkParams:{ id: ctrl.configuration.Namespace }
},
{ label:'ConfigMaps and Secrets', link:'kubernetes.configurations' },
{ label:'ConfigMaps and Secrets', link:'kubernetes.configurations', linkParams:{ tab: 'secrets' } },
ctrl.configuration.Name,
]"
reload="true"
@ -25,8 +25,8 @@
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)" data-cy="k8sConfigDetail-configTab">
<uib-tab-heading>
<pr-icon icon="'file-code'"></pr-icon>
Configuration
<pr-icon icon="'lock'"></pr-icon>
Secret
</uib-tab-heading>
<div style="padding: 20px">
<table class="table" data-cy="k8sConfigDetail-configTable">
@ -45,12 +45,6 @@
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
</td>
</tr>
<tr>
<td class="!pl-0">Kind</td>
<td>
{{ ctrl.configuration.Kind | kubernetesConfigurationKindText }}
</td>
</tr>
<tr ng-if="ctrl.secretTypeName">
<td class="!pl-0">Secret Type</td>
<td>
@ -88,7 +82,7 @@
YAML
</uib-tab-heading>
<div class="px-5 !pt-5" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"> </kubernetes-yaml-inspector>
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"></kubernetes-yaml-inspector>
</div>
</uib-tab>
</uib-tabset>
@ -184,7 +178,7 @@
table-key="kubernetes.configurations.applications"
order-by="Name"
refresh-callback="ctrl.getApplications"
title-text="Applications using this configuration"
title-text="Applications using this secret"
title-icon="svg-laptopcode"
>
</kubernetes-integrated-applications-datatable>

View file

@ -0,0 +1,8 @@
angular.module('portainer.kubernetes').component('kubernetesSecretView', {
templateUrl: './secret.html',
controller: 'KubernetesSecretController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
},
});

View file

@ -8,10 +8,12 @@ import KubernetesConfigurationConverter from 'Kubernetes/converters/configuratio
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { confirmUpdate, confirmWebEditorDiscard } from '@@/modals/confirm';
import { isConfigurationFormValid } from '../validation';
import { pluralize } from '@/portainer/helpers/strings';
class KubernetesConfigurationController {
import { confirmUpdate, confirmWebEditorDiscard } from '@@/modals/confirm';
import { isConfigurationFormValid } from '../../validation';
class KubernetesSecretController {
/* @ngInject */
constructor(
$async,
@ -21,7 +23,6 @@ class KubernetesConfigurationController {
Notifications,
LocalStorage,
KubernetesConfigurationService,
KubernetesConfigMapService,
KubernetesSecretService,
KubernetesResourcePoolService,
KubernetesApplicationService,
@ -39,7 +40,6 @@ class KubernetesConfigurationController {
this.KubernetesEventService = KubernetesEventService;
this.KubernetesConfigurationKinds = KubernetesConfigurationKinds;
this.KubernetesSecretTypeOptions = KubernetesSecretTypeOptions;
this.KubernetesConfigMapService = KubernetesConfigMapService;
this.KubernetesSecretService = KubernetesSecretService;
this.onInit = this.onInit.bind(this);
@ -48,7 +48,6 @@ class KubernetesConfigurationController {
this.getEventsAsync = this.getEventsAsync.bind(this);
this.getApplications = this.getApplications.bind(this);
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
this.updateConfiguration = this.updateConfiguration.bind(this);
this.updateConfigurationAsync = this.updateConfigurationAsync.bind(this);
}
@ -87,11 +86,6 @@ class KubernetesConfigurationController {
// It looks like we're still doing a create/delete process but we've decided to get rid of this
// approach.
async updateConfigurationAsync() {
let kind = 'ConfigMap';
if (this.formValues.Kind === KubernetesConfigurationKinds.SECRET) {
kind = 'Secret';
}
try {
this.state.actionInProgress = true;
if (
@ -101,9 +95,9 @@ class KubernetesConfigurationController {
) {
await this.KubernetesConfigurationService.create(this.formValues);
await this.KubernetesConfigurationService.delete(this.configuration);
this.Notifications.success('Success', `${kind} successfully updated`);
this.Notifications.success('Success', `Secret successfully updated`);
this.$state.go(
'kubernetes.configurations.configuration',
'kubernetes.secrets.secret',
{
namespace: this.formValues.ResourcePool.Namespace.Name,
name: this.formValues.Name,
@ -112,11 +106,11 @@ class KubernetesConfigurationController {
);
} else {
await this.KubernetesConfigurationService.update(this.formValues, this.configuration);
this.Notifications.success('Success', `${kind} successfully updated`);
this.Notifications.success('Success', `Secret successfully updated`);
this.$state.reload(this.$state.current);
}
} catch (err) {
this.Notifications.error('Failure', err, `Unable to update ${kind}`);
this.Notifications.error('Failure', err, `Unable to update Secret`);
} finally {
this.state.actionInProgress = false;
}
@ -124,10 +118,11 @@ class KubernetesConfigurationController {
updateConfiguration() {
if (this.configuration.Used) {
const plural = this.configuration.Applications.length > 1 ? 's' : '';
const thisorthese = this.configuration.Applications.length > 1 ? 'these' : 'this';
confirmUpdate(
`The changes will be propagated to ${this.configuration.Applications.length} running application${plural}. Are you sure you want to update ${thisorthese} ConfigMap${plural} or Secret${plural}?`,
`The changes will be propagated to ${this.configuration.Applications.length} running ${pluralize(
this.configuration.Applications.length,
'application'
)}. Are you sure you want to update this Secret?`,
(confirmed) => {
if (confirmed) {
return this.$async(this.updateConfigurationAsync);
@ -144,18 +139,15 @@ class KubernetesConfigurationController {
this.state.configurationLoading = true;
const name = this.$transition$.params().name;
const namespace = this.$transition$.params().namespace;
const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]);
if (secret.status === 'rejected' && secret.reason.err.status === 403) {
this.$state.go('kubernetes.configurations');
throw new Error('Not authorized to edit secret');
}
if (secret.status === 'fulfilled') {
this.configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value);
this.formValues.Data = secret.value.Data;
// this.formValues.ServiceAccountName = secret.value.ServiceAccountName;
} else {
this.configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value);
this.formValues.Data = configMap.value.Data;
try {
const secret = await this.KubernetesSecretService.get(namespace, name);
this.configuration = KubernetesConfigurationConverter.secretToConfiguration(secret);
this.formValues.Data = secret.Data;
} catch (err) {
if (err.status === 403) {
this.$state.go('kubernetes.configurations', { tab: 'secrets' });
throw new Error('Not authorized to edit secret');
}
}
this.formValues.ResourcePool = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.configuration.Namespace);
this.formValues.Id = this.configuration.Id;
@ -166,7 +158,7 @@ class KubernetesConfigurationController {
return this.configuration;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configuration');
this.Notifications.error('Failure', err, 'Unable to retrieve secret');
} finally {
this.state.configurationLoading = false;
}
@ -214,18 +206,6 @@ class KubernetesConfigurationController {
return this.$async(this.getEventsAsync, namespace);
}
async getConfigurationsAsync() {
try {
this.configurations = await this.KubernetesConfigurationService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
}
}
getConfigurations() {
return this.$async(this.getConfigurationsAsync);
}
tagUsedDataKeys() {
const configName = this.$transition$.params().name;
const usedDataKeys = _.uniq(
@ -276,7 +256,6 @@ class KubernetesConfigurationController {
if (configuration) {
await this.getApplications(this.configuration.Namespace);
await this.getEvents(this.configuration.Namespace);
await this.getConfigurations();
}
// after loading the configuration, check if it is a docker config secret type
@ -325,5 +304,5 @@ class KubernetesConfigurationController {
}
}
export default KubernetesConfigurationController;
angular.module('portainer.kubernetes').controller('KubernetesConfigurationController', KubernetesConfigurationController);
export default KubernetesSecretController;
angular.module('portainer.kubernetes').controller('KubernetesSecretController', KubernetesSecretController);

View file

@ -284,6 +284,11 @@ class KubernetesDeployController {
this.Notifications.success('Success', 'Request to deploy manifest successfully submitted');
this.state.isEditorDirty = false;
if (this.$state.params.referrer && this.$state.params.tab) {
this.$state.go(this.$state.params.referrer, { tab: this.$state.params.tab });
return;
}
if (this.$state.params.referrer) {
this.$state.go(this.$state.params.referrer);
return;