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
+
+
+
+
+ this.onConfigChange('enabled', e.target.checked)}
+ type="checkbox"
+ />
+ Spinnaker manages traffic based on your selected strategy
+
+
+
+ {config.enabled && (
+ <>
+
+ ) => this.onConfigChange('options.namespace', option.value)}
+ options={this.getNamespaceOptions()}
+ value={config.options.namespace}
+ />
+
+
+ this.onConfigChange('options.services', map(options, 'value'))}
+ options={map(this.state.services, s => ({ label: split(s, ' ')[1], value: s }))}
+ value={config.options.services}
+ />
+
+
+
+
+ this.onConfigChange('options.enableTraffic', e.target.checked)}
+ type="checkbox"
+ />
+ Send client requests to new pods
+
+
+
+ >
+ )}
+ >
+ );
+ }
+}
+
+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 @@
-
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,