import angular from 'angular'; import _ from 'lodash-es'; import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues'; import { KubernetesConfigurationKinds, KubernetesSecretTypeOptions } 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 KubernetesSecretController { /* @ngInject */ constructor( $async, $state, $window, clipboard, Notifications, LocalStorage, KubernetesConfigurationService, KubernetesSecretService, KubernetesResourcePoolService, KubernetesApplicationService, KubernetesEventService ) { this.$async = $async; this.$state = $state; this.$window = $window; this.clipboard = clipboard; this.Notifications = Notifications; this.LocalStorage = LocalStorage; this.KubernetesConfigurationService = KubernetesConfigurationService; this.KubernetesResourcePoolService = KubernetesResourcePoolService; this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesEventService = KubernetesEventService; this.KubernetesConfigurationKinds = KubernetesConfigurationKinds; this.KubernetesSecretTypeOptions = KubernetesSecretTypeOptions; this.KubernetesSecretService = KubernetesSecretService; 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.updateConfiguration = this.updateConfiguration.bind(this); this.updateConfigurationAsync = this.updateConfigurationAsync.bind(this); } isSystemNamespace() { return KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace); } isSystemConfig() { return this.isSystemNamespace() || this.configuration.IsRegistrySecret; } 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, warningMessage] = isConfigurationFormValid(this.state.alreadyExist, this.state.isDataValid, this.formValues); this.state.secretWarningMessage = warningMessage; 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', `Secret successfully updated`); this.$state.go( 'kubernetes.secrets.secret', { 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', `Secret successfully updated`); this.$state.reload(this.$state.current); } } catch (err) { this.Notifications.error('Failure', err, `Unable to update Secret`); } 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 Secret?`, (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 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; 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 secret'); } 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); } 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, isDockerConfig: false, secretWarningMessage: '', }; 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); } // after loading the configuration, check if it is a docker config secret type if ( this.formValues.Kind === this.KubernetesConfigurationKinds.SECRET && (this.formValues.Type === this.KubernetesSecretTypeOptions.DOCKERCONFIGJSON.value || this.formValues.Type === this.KubernetesSecretTypeOptions.DOCKERCFG.value) ) { this.state.isDockerConfig = true; } // convert the secret type to a human readable value if (this.formValues.Type) { const secretTypeValues = Object.values(this.KubernetesSecretTypeOptions); const secretType = secretTypeValues.find((secretType) => secretType.value === this.formValues.Type); this.secretTypeName = secretType ? secretType.name : this.formValues.Type; } else { this.secretTypeName = ''; } if (this.formValues.Type === this.KubernetesSecretTypeOptions.SERVICEACCOUNTTOKEN.value) { this.formValues.ServiceAccountName = configuration.ServiceAccountName; } 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 KubernetesSecretController; angular.module('portainer.kubernetes').controller('KubernetesSecretController', KubernetesSecretController);