Skip to content

Commit

Permalink
feat(kubernetes): add rollout strategies to deploy manifest stage
Browse files Browse the repository at this point in the history
  • Loading branch information
maggieneterval committed Apr 11, 2019
1 parent 6072ee5 commit a69241b
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 4 deletions.
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 @@ -205,6 +205,9 @@ const helpContents: { [key: string]: string } = {
'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.rolloutStrategy': `
<p>The rollout strategy tells Spinnaker what to do with the previous version(s) of the ReplicaSet in the cluster.</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
Expand Up @@ -29,10 +29,10 @@ describe('<ManifestDeploymentOptions />', () => {
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', () => {
it('renders config fields for `namespace`, `services`, `enableTraffic`, and `strategy` when config is enabled', () => {
props.config.enabled = true;
wrapper = shallow(<ManifestDeploymentOptions {...props} />);
expect(wrapper.find(StageConfigField).length).toEqual(4);
expect(wrapper.find(StageConfigField).length).toEqual(5);
});
});

Expand All @@ -47,5 +47,15 @@ describe('<ManifestDeploymentOptions />', () => {
enabled: true,
});
});
it('disables the traffic checkbox when a non-None rollout strategy is selected', () => {
props.config.options.strategy = 'redblack';
wrapper = shallow(<ManifestDeploymentOptions {...props} />);
expect(
wrapper
.find('input[type="checkbox"]')
.at(1)
.props().disabled,
).toEqual(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { module } from 'angular';
import * as DOMPurify from 'dompurify';
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 { IAccountDetails, IDeploymentStrategy, StageConfigField } from '@spinnaker/core';

import { ManifestKindSearchService } from 'kubernetes/v2/manifest/ManifestKindSearch';
import { rolloutStrategies } from 'kubernetes/v2/rolloutStrategy';

export interface ITrafficManagementConfig {
enabled: boolean;
Expand Down Expand Up @@ -73,6 +75,19 @@ export class ManifestDeploymentOptions extends React.Component<
return map(namespaces, n => ({ label: n, value: n }));
};

private strategyOptionRenderer = (option: IDeploymentStrategy) => {
return (
<div className="body-regular">
<strong>
<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(option.label) }} />
</strong>
<div>
<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(option.description) }} />
</div>
</div>
);
};

public componentDidMount() {
this.fetchServices();
}
Expand All @@ -86,6 +101,10 @@ export class ManifestDeploymentOptions extends React.Component<
this.onConfigChange('options.services', null);
this.fetchServices();
}

if (!this.props.config.options.enableTraffic && !!this.props.config.options.strategy) {
this.onConfigChange('options.enableTraffic', true);
}
}

public render() {
Expand Down Expand Up @@ -130,13 +149,26 @@ export class ManifestDeploymentOptions extends React.Component<
<label>
<input
checked={config.options.enableTraffic}
disabled={!!config.options.strategy}
onChange={e => this.onConfigChange('options.enableTraffic', e.target.checked)}
type="checkbox"
/>
Send client requests to new pods
</label>
</div>
</StageConfigField>
<StageConfigField fieldColumns={8} helpKey="kubernetes.manifest.rolloutStrategy" label="Strategy">
<Select
clearable={false}
onChange={(option: Option<IDeploymentStrategy>) => this.onConfigChange('options.strategy', option.key)}
options={rolloutStrategies}
optionRenderer={this.strategyOptionRenderer}
placeholder="None"
value={config.options.strategy}
valueKey="key"
valueRenderer={o => <>{o.label}</>}
/>
</StageConfigField>
</>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { isEmpty } from 'lodash';

import { IPipeline, IStage, IValidatorConfig, ICustomValidator } from '@spinnaker/core';

export const deployManifestValidators = (): IValidatorConfig[] => {
return [
{
type: 'custom',
validate: (_pipeline: IPipeline, stage: IStage) => {
const { enabled = false, options = {} } = stage.trafficManagement;
if (enabled && isEmpty(options.services)) {
return `Select at least one <strong>Service</strong> to enable Spinnaker-managed rollout strategy options.`;
}
return null;
},
} as ICustomValidator,
];
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { KubernetesV2DeployManifestConfigCtrl } from './deployManifestConfig.con
import { MANIFEST_BIND_ARTIFACTS_SELECTOR_REACT } from './ManifestBindArtifactsSelector';
import { MANIFEST_DEPLOYMENT_OPTIONS } from './ManifestDeploymentOptions';
import { DeployStatus } from './react/DeployStatus';
import { deployManifestValidators } from './deployManifest.validator';

export const KUBERNETES_DEPLOY_MANIFEST_STAGE = 'spinnaker.kubernetes.v2.pipeline.stage.deployManifestStage';

Expand All @@ -37,7 +38,7 @@ module(KUBERNETES_DEPLOY_MANIFEST_STAGE, [
executionDetailsSections: [DeployStatus, ExecutionDetailsTasks, ExecutionArtifactTab],
producesArtifacts: true,
defaultTimeoutMs: 30 * 60 * 1000, // 30 minutes
validators: [],
validators: deployManifestValidators(),
accountExtractor: (stage: IStage): string => (stage.account ? stage.account : ''),
configAccountExtractor: (stage: any): string[] => (stage.account ? [stage.account] : []),
artifactExtractor: ExpectedArtifactService.accumulateArtifacts(['manifestArtifactId', 'requiredArtifactIds']),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IDeploymentStrategy } from 'core/deploymentStrategy';

export const strategyHighlander: IDeploymentStrategy = {
label: 'Highlander',
description: 'Destroys <i>all</i> previous ReplicaSets in the cluster as soon as the new ReplicaSet is ready',
key: 'highlander',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { strategyHighlander } from 'kubernetes/v2/rolloutStrategy/highlander.strategy';
import { strategyNone } from 'kubernetes/v2/rolloutStrategy/none.strategy';
import { strategyRedBlack } from 'kubernetes/v2/rolloutStrategy/redblack.strategy';

export const rolloutStrategies = [strategyNone, strategyRedBlack, strategyHighlander];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IDeploymentStrategy } from 'core/deploymentStrategy';

export const strategyNone: IDeploymentStrategy = {
label: 'None',
description: 'Creates the new ReplicaSet with no impact on existing ReplicaSets in the cluster',
key: null,
providerRestricted: false,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IDeploymentStrategy } from 'core/deploymentStrategy';

export const strategyRedBlack: IDeploymentStrategy = {
label: 'Red/Black',
description: 'Disables <i>all</i> previous ReplicaSets in the cluster as soon as the new ReplicaSet is ready',
key: 'redblack',
};

0 comments on commit a69241b

Please sign in to comment.