From 2d7f3885248e71b930df101b942bb0998e16b635 Mon Sep 17 00:00:00 2001 From: Maggie Neterval Date: Mon, 8 Apr 2019 11:52:51 -0400 Subject: [PATCH] feat(kubernetes): feature-flagged support for kubernetes traffic management strategies (#6816) --- .../modules/core/src/config/settings.ts | 1 + .../kubernetes/src/help/kubernetes.help.ts | 3 + .../ManifestDeploymentOptions.spec.tsx | 51 ++++++ .../ManifestDeploymentOptions.tsx | 151 ++++++++++++++++++ .../deployManifestConfig.controller.ts | 10 ++ .../deployManifest/deployManifestConfig.html | 12 +- .../deployManifest/deployManifestStage.ts | 7 +- settings.js | 2 + 8 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.spec.tsx create mode 100644 app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.tsx diff --git a/app/scripts/modules/core/src/config/settings.ts b/app/scripts/modules/core/src/config/settings.ts index 81644329abb..b6308fe3a9f 100644 --- a/app/scripts/modules/core/src/config/settings.ts +++ b/app/scripts/modules/core/src/config/settings.ts @@ -45,6 +45,7 @@ export interface IFeatures { // whether stages affecting infrastructure (like "Create Load Balancer") should be enabled or not infrastructureStages?: boolean; jobs?: boolean; + kubernetesRolloutStrategies?: boolean; managedPipelineTemplatesV2UI?: boolean; managedServiceAccounts?: boolean; notifications?: boolean; diff --git a/app/scripts/modules/kubernetes/src/help/kubernetes.help.ts b/app/scripts/modules/kubernetes/src/help/kubernetes.help.ts index 30b2df707e3..1fe2830eea4 100644 --- a/app/scripts/modules/kubernetes/src/help/kubernetes.help.ts +++ b/app/scripts/modules/kubernetes/src/help/kubernetes.help.ts @@ -202,6 +202,9 @@ const helpContents: { [key: string]: string } = { artifact: The manifest is read from an artifact supplied/created upstream. The expected artifact must be referenced here, and will be bound at runtime.

`, + 'kubernetes.manifest.rolloutStrategyOptions': ` +

Allow Spinnaker to associate your workload with one or more Services and manage traffic based on your selected rollout strategy options. Valid for ReplicaSets only.

+ `, 'kubernetes.manifest.expectedArtifact': 'The artifact that is to be applied to the Kubernetes account for this stage. The artifact should represent a valid Kubernetes manifest.', 'kubernetes.manifest.requiredArtifactsToBind': diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.spec.tsx b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.spec.tsx new file mode 100644 index 00000000000..ad128b5d83b --- /dev/null +++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.spec.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; + +import { StageConfigField } from '@spinnaker/core'; + +import { + IManifestDeploymentOptionsProps, + ManifestDeploymentOptions, + defaultTrafficManagementConfig, +} from './ManifestDeploymentOptions'; + +describe('', () => { + const onConfigChangeSpy = jasmine.createSpy('onConfigChangeSpy'); + let wrapper: any; + let props: IManifestDeploymentOptionsProps; + + beforeEach(() => { + props = { + accounts: [], + config: defaultTrafficManagementConfig, + onConfigChange: onConfigChangeSpy, + selectedAccount: null, + }; + wrapper = shallow(); + }); + + describe('view', () => { + it('renders only the enable checkbox when config is disabled', () => { + expect(wrapper.find(StageConfigField).length).toEqual(1); + expect(wrapper.find('input[type="checkbox"]').length).toEqual(1); + }); + it('renders config fields for `namespace`, `services`, and `enableTraffic` when config is enabled', () => { + props.config.enabled = true; + wrapper = shallow(); + expect(wrapper.find(StageConfigField).length).toEqual(4); + }); + }); + + describe('functionality', () => { + it('updates `config.enabled` when enable checkbox is toggled', () => { + wrapper + .find('input[type="checkbox"]') + .at(0) + .simulate('change', { target: { checked: true } }); + expect(onConfigChangeSpy).toHaveBeenCalledWith({ + ...defaultTrafficManagementConfig, + enabled: true, + }); + }); + }); +}); diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.tsx b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.tsx new file mode 100644 index 00000000000..96bf03503fc --- /dev/null +++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestDeploymentOptions.tsx @@ -0,0 +1,151 @@ +import { module } from 'angular'; +import * as React from 'react'; +import { react2angular } from 'react2angular'; +import { cloneDeep, find, get, map, set, split } from 'lodash'; +import Select, { Option } from 'react-select'; + +import { IAccountDetails, StageConfigField } from '@spinnaker/core'; + +import { ManifestKindSearchService } from 'kubernetes/v2/manifest/ManifestKindSearch'; + +export interface ITrafficManagementConfig { + enabled: boolean; + options: ITrafficManagementOptions; +} + +export interface ITrafficManagementOptions { + namespace: string; + services: string[]; + enableTraffic: boolean; + strategy: string; +} + +export const defaultTrafficManagementConfig: ITrafficManagementConfig = { + enabled: false, + options: { + namespace: null, + services: [], + enableTraffic: false, + strategy: null, + }, +}; + +export interface IManifestDeploymentOptionsProps { + accounts: IAccountDetails[]; + config: ITrafficManagementConfig; + onConfigChange: (config: ITrafficManagementConfig) => void; + selectedAccount: string; +} + +export interface IManifestDeploymentOptionsState { + services: string[]; +} + +export class ManifestDeploymentOptions extends React.Component< + IManifestDeploymentOptionsProps, + IManifestDeploymentOptionsState +> { + public state: IManifestDeploymentOptionsState = { services: [] }; + + private onConfigChange = (key: string, value: any): void => { + const updatedConfig = cloneDeep(this.props.config); + set(updatedConfig, key, value); + this.props.onConfigChange(updatedConfig); + }; + + private fetchServices = (): void => { + const namespace = this.props.config.options.namespace; + const account = this.props.selectedAccount; + if (!namespace || !account) { + this.setState({ + services: [], + }); + } + ManifestKindSearchService.search('service', namespace, account).then(services => { + this.setState({ services: map(services, 'name') }); + }); + }; + + private getNamespaceOptions = (): Array> => { + const { accounts, selectedAccount } = this.props; + const selectedAccountDetails = find(accounts, a => a.name === selectedAccount); + const namespaces = get(selectedAccountDetails, 'namespaces', []); + return map(namespaces, n => ({ label: n, value: n })); + }; + + public componentDidMount() { + this.fetchServices(); + } + + public componentDidUpdate(prevProps: IManifestDeploymentOptionsProps) { + if (prevProps.selectedAccount !== this.props.selectedAccount) { + this.onConfigChange('options.namespace', null); + } + + if (prevProps.config.options.namespace !== this.props.config.options.namespace) { + this.onConfigChange('options.services', null); + this.fetchServices(); + } + } + + public render() { + const { config } = this.props; + return ( + <> +

Rollout Strategy Options

+
+ +
+ +
+
+ {config.enabled && ( + <> + + this.onConfigChange('options.services', map(options, 'value'))} + options={map(this.state.services, s => ({ label: split(s, ' ')[1], value: s }))} + value={config.options.services} + /> + + +
+ +
+
+ + )} + + ); + } +} + +export const MANIFEST_DEPLOYMENT_OPTIONS = 'spinnaker.kubernetes.v2.pipelines.deployManifest.manifestDeploymentOptions'; +module(MANIFEST_DEPLOYMENT_OPTIONS, []).component( + 'manifestDeploymentOptions', + react2angular(ManifestDeploymentOptions, ['accounts', 'config', 'onConfigChange', 'selectedAccount']), +); diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts index ca3c0acb7d9..9e5eaf35bca 100644 --- a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts +++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts @@ -18,6 +18,7 @@ import { } from 'kubernetes/v2/manifest/manifestCommandBuilder.service'; import { IManifestBindArtifact } from './ManifestBindArtifactsSelector'; +import { ITrafficManagementConfig, defaultTrafficManagementConfig } from './ManifestDeploymentOptions'; export class KubernetesV2DeployManifestConfigCtrl implements IController { public state = { @@ -61,6 +62,9 @@ export class KubernetesV2DeployManifestConfigCtrl implements IController { skipExpressionEvaluation: false, }); } + if (!stage.trafficManagement) { + stage.trafficManagement = defaultTrafficManagementConfig; + } this.metadata = builtCommand.metadata; this.state.loaded = true; this.manifestArtifactDelegate.setAccounts(get(this, ['metadata', 'backingData', 'artifactAccounts'], [])); @@ -124,4 +128,10 @@ export class KubernetesV2DeployManifestConfigCtrl implements IController { // This method is called from a React component. this.$scope.$applyAsync(); }; + + public handleTrafficManagementConfigChange = (trafficManagementConfig: ITrafficManagementConfig): void => { + this.$scope.stage.trafficManagement = trafficManagementConfig; + // This method is called from a React component. + this.$scope.$applyAsync(); + }; } diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html index 96b5ab67ed0..9f439c826e0 100644 --- a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html +++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html @@ -1,10 +1,12 @@

Basic Settings

+

Manifest Configuration

+
-
Manifest Configuration
-
Manifest Configuration >
+ diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestStage.ts b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestStage.ts index 02d121e34cd..cad1fac9738 100644 --- a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestStage.ts +++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestStage.ts @@ -13,11 +13,16 @@ import { import { KubernetesV2DeployManifestConfigCtrl } from './deployManifestConfig.controller'; import { MANIFEST_BIND_ARTIFACTS_SELECTOR_REACT } from './ManifestBindArtifactsSelector'; +import { MANIFEST_DEPLOYMENT_OPTIONS } from './ManifestDeploymentOptions'; import { DeployStatus } from './react/DeployStatus'; export const KUBERNETES_DEPLOY_MANIFEST_STAGE = 'spinnaker.kubernetes.v2.pipeline.stage.deployManifestStage'; -module(KUBERNETES_DEPLOY_MANIFEST_STAGE, [EXECUTION_ARTIFACT_TAB, MANIFEST_BIND_ARTIFACTS_SELECTOR_REACT]) +module(KUBERNETES_DEPLOY_MANIFEST_STAGE, [ + EXECUTION_ARTIFACT_TAB, + MANIFEST_BIND_ARTIFACTS_SELECTOR_REACT, + MANIFEST_DEPLOYMENT_OPTIONS, +]) .config(() => { // Todo: replace feature flag with proper versioned provider mechanism once available. if (SETTINGS.feature.versionedProviders) { diff --git a/settings.js b/settings.js index 22e09f150eb..b85a49003a4 100644 --- a/settings.js +++ b/settings.js @@ -22,6 +22,7 @@ var fiatEnabled = process.env.FIAT_ENABLED === 'true' ? true : false; var gremlinEnabled = process.env.GREMLIN_ENABLED === 'false' ? false : true; var iapRefresherEnabled = process.env.IAP_REFRESHER_ENABLED === 'true' ? true : false; var infrastructureEnabled = process.env.INFRA_ENABLED === 'true' ? true : false; +var kubernetesRolloutStrategiesEnabled = process.env.KUBERNETES_ROLLOUT_STRATEGIES === 'true'; var managedPipelineTemplatesV2UIEnabled = process.env.MANAGED_PIPELINE_TEMPLATES_V2_UI_ENABLED === 'true'; var managedServiceAccountsEnabled = process.env.MANAGED_SERVICE_ACCOUNTS_ENABLED === 'true'; var onDemandClusterThreshold = process.env.ON_DEMAND_CLUSTER_THRESHOLD || '350'; @@ -77,6 +78,7 @@ window.spinnakerSettings = { // whether stages affecting infrastructure (like "Create Load Balancer") should be enabled or not infrastructureStages: infrastructureEnabled, jobs: false, + kubernetesRolloutStrategies: kubernetesRolloutStrategiesEnabled, managedPipelineTemplatesV2UI: managedPipelineTemplatesV2UIEnabled, managedServiceAccounts: managedServiceAccountsEnabled, notifications: false,