Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kubernetes): add rollout strategies to deploy manifest stage #6841

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
};