Skip to content

Commit

Permalink
feat(cloudfoundry/unbind): Delete Service Bindings stage (#9384)
Browse files Browse the repository at this point in the history
* fix(core): fix state update for pipeline tags

* fix(core): make metadata page content overridable

* feat(cloudfoundry/unbind): Add Stage in order to Unbind Services

* feat(cloudfoundry/unbind): remove restaga and restart from test

* feat(cloudfoundry/unbind): fix test

* feat(cloudfoundry/unbind): change typeof Observable by observableFrom

Co-authored-by: Fernando Freire <fernando.freire@armory.io>
Co-authored-by: Zach Smith <33258732+zachsmith1@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 6, 2021
1 parent 0524550 commit 45e8b9f
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/cloudfoundry/src/cf.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import './pipeline/stages/bakeCloudFoundryManifest/bakeCloudFoundryManifestStage
import './pipeline/stages/cloneServerGroup/cloudfoundryCloneServerGroupStage.module';
import './pipeline/stages/createServiceBindings/cloudFoundryCreateServiceBindingsStage';
import './pipeline/stages/createServiceKey/cloudfoundryCreateServiceKeyStage.module';
import './pipeline/stages/deleteServiceBindings/cloudFoundryDeleteServiceBindingsStage';
import './pipeline/stages/deleteServiceKey/cloudfoundryDeleteServiceKeyStage.module';
import './pipeline/stages/deployService/cloudfoundryDeployServiceStage.module';
import './pipeline/stages/destroyAsg/cloudfoundryDestroyAsgStage.module';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { FormikErrors } from 'formik';
import { cloneDeep } from 'lodash';
import React from 'react';

import { FormikStageConfig, FormValidator, IStage, IStageConfigProps } from '@spinnaker/core';

import { CloudFoundryDeleteServiceBindingsStageConfigForm } from './CloudFoundryDeleteServiceBindingsStageConfigForm';

interface ServiceUnbindingRequests {
serviceInstanceName: String;
}

export function CloudFoundryDeleteServiceBindingsConfig({
application,
pipeline,
stage,
updateStage,
}: IStageConfigProps) {
const stageWithDefaults = React.useMemo(() => {
return {
serviceUnbindingRequests: [],
restageRequired: true,
restartRequired: false,
credentials: '',
region: '',
...cloneDeep(stage),
};
}, []);

return (
<FormikStageConfig
application={application}
onChange={updateStage}
pipeline={pipeline}
stage={stageWithDefaults}
validate={validateCloudFoundryDeleteServiceBindingsStage}
render={(props) => <CloudFoundryDeleteServiceBindingsStageConfigForm {...props} />}
/>
);
}

export function validateCloudFoundryDeleteServiceBindingsStage(stage: IStage): FormikErrors<IStage> {
const formValidator = new FormValidator(stage);

formValidator.field('credentials', 'Account').required();
formValidator.field('region', 'Region').required();
formValidator.field('cluster', 'Cluster').required();
formValidator.field('target', 'Target').required();

formValidator
.field('serviceUnbindingRequests', 'Service Binding Requests')
.required()
.withValidators((serviceUnbindingRequests: ServiceUnbindingRequests[]) => {
if (validateServiceUnbindingRequests(serviceUnbindingRequests)) {
return undefined;
}
return 'There should be at least one service binding request. At a minimum, each request must have a service instance name.';
});

return formValidator.validateForm();
}

export function validateServiceUnbindingRequests(serviceUnbindingRequests: ServiceUnbindingRequests[]): boolean {
if (serviceUnbindingRequests?.length < 1) {
return false;
}
return serviceUnbindingRequests.every((req) => req.serviceInstanceName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React from 'react';
import { from as observableFrom, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import {
AccountService,
Application,
IAccount,
IFormikStageConfigInjectedProps,
IRegion,
NgReact,
StageConfigField,
StageConstants,
TextInput,
} from '@spinnaker/core';

import { AccountRegionClusterSelector } from '../../../presentation/widgets/accountRegionClusterSelector';

interface ICloudfoundryDeleteServiceBindingsStageConfigState {
accounts: IAccount[];
regions: string[];
application: Application;
}

export class CloudFoundryDeleteServiceBindingsStageConfigForm extends React.Component<
IFormikStageConfigInjectedProps,
ICloudfoundryDeleteServiceBindingsStageConfigState
> {
private destroy$ = new Subject();

constructor(props: IFormikStageConfigInjectedProps, context: any) {
super(props, context);
this.state = {
accounts: [],
regions: [],
application: props.application,
};
observableFrom(AccountService.listAccounts('cloudfoundry'))
.pipe(takeUntil(this.destroy$))
.subscribe((rawAccounts: IAccount[]) => this.setState({ accounts: rawAccounts }));
}

public componentDidMount() {
const stage = this.props.formik.values;
this.props.formik.setFieldValue('cloudProvider', 'cloudfoundry');
if (stage.serviceUnbindingRequests && stage.serviceUnbindingRequests.length === 0) {
this.props.formik.setFieldValue('serviceUnbindingRequests', [
{
serviceInstanceName: '',
updatable: false,
},
]);
}
if (stage.credentials) {
this.loadRegions(stage.credentials);
}
}

public componentWillUnmount(): void {
this.destroy$.next();
}

private loadRegions = (creds: string) => {
this.setState({ regions: [] });
observableFrom(AccountService.getRegionsForAccount(creds))
.pipe(takeUntil(this.destroy$))
.subscribe((regionList: IRegion[]) => {
const regions = regionList.map((r) => r.name);
regions.sort((a, b) => a.localeCompare(b));
this.setState({ regions: regions });
});
};

private addInputArtifact = () => {
const stage = this.props.formik.values;
const newServiceUnbindingRequests = [
...stage.serviceUnbindingRequests,
{
serviceInstanceName: '',
updatable: false,
},
];

this.props.formik.setFieldValue('serviceUnbindingRequests', newServiceUnbindingRequests);
};

private removeInputArtifact = (index: number) => {
const stage = this.props.formik.values;
const newServiceUnbindingRequests = [...stage.serviceUnbindingRequests];
newServiceUnbindingRequests.splice(index, 1);
this.props.formik.setFieldValue('serviceUnbindingRequests', newServiceUnbindingRequests);
};

private targetUpdated = (target: string) => {
this.props.formik.setFieldValue('target', target);
};

private accountRegionClusterUpdated = (stage: any): void => {
this.props.formik.setFieldValue(`cluster`, stage.cluster);
this.props.formik.setFieldValue(`credentials`, stage.credentials);
this.props.formik.setFieldValue(`region`, stage.region);
};

public render() {
const stage = this.props.formik.values;
const { accounts, application } = this.state;
const { target } = stage;
const { TargetSelect } = NgReact;

return (
<>
<h4>Basic Settings</h4>
<div className="form-horizontal">
<AccountRegionClusterSelector
accounts={accounts}
application={application}
cloudProvider={'cloudfoundry'}
isSingleRegion={true}
onComponentUpdate={this.accountRegionClusterUpdated}
component={stage}
/>
</div>
<StageConfigField label="Target">
<TargetSelect model={{ target }} options={StageConstants.TARGET_LIST} onChange={this.targetUpdated} />
</StageConfigField>
<h4>Service Unbindings</h4>
{stage.serviceUnbindingRequests && stage.serviceUnbindingRequests.length > 0 && (
<div className="row form-group">
{stage.serviceUnbindingRequests.map((a: any, index: number) => {
return (
<div key={index}>
<div className="col-md-offset-1 col-md-8">
<StageConfigField label="Service Instance Name">
<TextInput
onChange={(e: React.ChangeEvent<any>) => {
this.props.formik.setFieldValue(
`serviceUnbindingRequests[${index}].serviceInstanceName`,
e.target.value,
);
}}
value={a.serviceInstanceName}
/>
</StageConfigField>
</div>
<div className="col-md-1">
<div className="form-control-static">
<button onClick={() => this.removeInputArtifact(index)}>
<span className="glyphicon glyphicon-trash" />
<span className="sr-only">Remove field</span>
</button>
</div>
</div>
</div>
);
})}
</div>
)}
<StageConfigField fieldColumns={8} label={''}>
<button className="btn btn-block btn-sm add-new" onClick={() => this.addInputArtifact()}>
<span className="glyphicon glyphicon-plus-sign" />
Add Service Unbinding
</button>
</StageConfigField>
</>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ExecutionDetailsTasks, Registry } from '@spinnaker/core';

import {
CloudFoundryDeleteServiceBindingsConfig,
validateCloudFoundryDeleteServiceBindingsStage,
} from './CloudFoundryDeleteServiceBindingsConfig';

export const CF_DELETE_SERVICE_BINDINGS_STAGE_KEY = 'cloudFoundryDeleteServiceBindings';

Registry.pipeline.registerStage({
label: 'Delete Service Bindings',
description: 'Delete CF service bindings with optional parameters.',
key: CF_DELETE_SERVICE_BINDINGS_STAGE_KEY,
component: CloudFoundryDeleteServiceBindingsConfig,
producesArtifacts: false,
cloudProvider: 'cloudfoundry',
executionDetailsSections: [ExecutionDetailsTasks],
validateFn: validateCloudFoundryDeleteServiceBindingsStage,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { mock } from 'angular';
import { mount } from 'enzyme';

import { ApplicationModelBuilder, IStage, REACT_MODULE, SpinFormik, StageConfigField } from '@spinnaker/core';
import { CloudFoundryDeleteServiceBindingsStageConfigForm } from './CloudFoundryDeleteServiceBindingsStageConfigForm';

describe('<CloudFoundryDeleteServiceBindingsStageConfigForm/>', function () {
beforeEach(mock.module(REACT_MODULE));
beforeEach(mock.inject());

const getProps = () => {
return {
application: ApplicationModelBuilder.createApplicationForTests('my-application'),
pipeline: {
application: 'my-application',
id: 'pipeline-id',
limitConcurrent: true,
keepWaitingPipelines: true,
name: 'My Pipeline',
parameterConfig: [],
stages: [],
triggers: [],
},
} as any;
};

it('loads component correctly with 2 serviceUnbindingRequests', function () {
const stage = ({
serviceUnbindingRequests: [{ serviceInstanceName: 'service1' }, { serviceInstanceName: 'service2' }],
} as unknown) as IStage;

const props = getProps();

const component = mount(
<SpinFormik
initialValues={stage}
onSubmit={() => null}
validate={() => null}
render={(formik) => <CloudFoundryDeleteServiceBindingsStageConfigForm {...props} formik={formik} />}
/>,
);
expect(component.find(StageConfigField).findWhere((x) => x.text() === 'Target').length).toBe(1);
expect(component.find(StageConfigField).findWhere((x) => x.text() === 'Service Instance Name').length).toBe(2);
});
});

0 comments on commit 45e8b9f

Please sign in to comment.