Skip to content

Commit

Permalink
feat(kubernetes): feature-flagged support for kubernetes traffic mana…
Browse files Browse the repository at this point in the history
…gement strategies (spinnaker#6816)
  • Loading branch information
maggieneterval authored Apr 8, 2019
1 parent 36074c2 commit 2d7f388
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 3 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions app/scripts/modules/kubernetes/src/help/kubernetes.help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ const helpContents: { [key: string]: string } = {
<b>artifact:</b> The manifest is read from an artifact supplied/created upstream. The expected artifact must be referenced here, and will be bound at runtime.
</p>
`,
'kubernetes.manifest.rolloutStrategyOptions': `
<p>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.</p>
`,
'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':
Expand Down
Original file line number Diff line number Diff line change
@@ -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('<ManifestDeploymentOptions />', () => {
const onConfigChangeSpy = jasmine.createSpy('onConfigChangeSpy');
let wrapper: any;
let props: IManifestDeploymentOptionsProps;

beforeEach(() => {
props = {
accounts: [],
config: defaultTrafficManagementConfig,
onConfigChange: onConfigChangeSpy,
selectedAccount: null,
};
wrapper = shallow(<ManifestDeploymentOptions {...props} />);
});

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(<ManifestDeploymentOptions {...props} />);
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,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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<Option<string>> => {
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 (
<>
<h4>Rollout Strategy Options</h4>
<hr />
<StageConfigField helpKey="kubernetes.manifest.rolloutStrategyOptions" fieldColumns={8} label="Enable">
<div className="checkbox">
<label>
<input
checked={config.enabled}
onChange={e => this.onConfigChange('enabled', e.target.checked)}
type="checkbox"
/>
Spinnaker manages traffic based on your selected strategy
</label>
</div>
</StageConfigField>
{config.enabled && (
<>
<StageConfigField fieldColumns={8} label="Service(s) Namespace">
<Select
clearable={false}
onChange={(option: Option<string>) => this.onConfigChange('options.namespace', option.value)}
options={this.getNamespaceOptions()}
value={config.options.namespace}
/>
</StageConfigField>
<StageConfigField fieldColumns={8} label="Service(s)">
<Select
clearable={false}
multi={true}
onChange={options => this.onConfigChange('options.services', map(options, 'value'))}
options={map(this.state.services, s => ({ label: split(s, ' ')[1], value: s }))}
value={config.options.services}
/>
</StageConfigField>
<StageConfigField fieldColumns={8} label="Traffic">
<div className="checkbox">
<label>
<input
checked={config.options.enableTraffic}
onChange={e => this.onConfigChange('options.enableTraffic', e.target.checked)}
type="checkbox"
/>
Send client requests to new pods
</label>
</div>
</StageConfigField>
</>
)}
</>
);
}
}

export const MANIFEST_DEPLOYMENT_OPTIONS = 'spinnaker.kubernetes.v2.pipelines.deployManifest.manifestDeploymentOptions';
module(MANIFEST_DEPLOYMENT_OPTIONS, []).component(
'manifestDeploymentOptions',
react2angular(ManifestDeploymentOptions, ['accounts', 'config', 'onConfigChange', 'selectedAccount']),
);
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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'], []));
Expand Down Expand Up @@ -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();
};
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<div ng-if="ctrl.state.loaded" class="clearfix">
<div class="container-fluid form-horizontal">
<h4>Basic Settings</h4>
<hr />
<kubernetes-manifest-basic-settings command="ctrl.$scope.stage" metadata="ctrl.metadata">
</kubernetes-manifest-basic-settings>

<h4>Manifest Configuration</h4>
<hr />
<ng-form name="kubernetesManifestSource">
<stage-config-field label="Manifest Source" help-key="kubernetes.manifest.source">
<label class="sm-label-right">
Expand All @@ -28,6 +30,7 @@ <h4>Manifest Configuration</h4>
on-change="ctrl.handleRawManifestChange"
>
</yaml-editor>
<hr ng-if="stage.source === ctrl.textSource" />
<div ng-if="ctrl.checkFeatureFlag('artifactsRewrite')">
<stage-config-field
label="Manifest Artifact"
Expand Down Expand Up @@ -57,7 +60,6 @@ <h4>Manifest Configuration</h4>
</label>
</div>
</stage-config-field>
<hr />
<stage-config-field
label="Req. Artifacts to Bind"
help-field-key="kubernetes.manifest.requiredArtifactsToBind"
Expand Down Expand Up @@ -117,7 +119,6 @@ <h4>Manifest Configuration</h4>
</label>
</div>
</stage-config-field>
<hr />
<expected-artifact-multi-selector
command="ctrl.$scope.stage"
ids-field="requiredArtifactIds"
Expand All @@ -128,5 +129,12 @@ <h4>Manifest Configuration</h4>
>
</expected-artifact-multi-selector>
</div>
<manifest-deployment-options
ng-if="ctrl.checkFeatureFlag('kubernetesRolloutStrategies')"
accounts="ctrl.metadata.backingData.accounts"
config="ctrl.$scope.stage.trafficManagement"
on-config-change="ctrl.handleTrafficManagementConfigChange"
selected-account="ctrl.$scope.stage.account"
/>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 2d7f388

Please sign in to comment.