diff --git a/app/scripts/modules/cloudfoundry/src/cf.module.ts b/app/scripts/modules/cloudfoundry/src/cf.module.ts index 90f7923a2c6..98ea145d576 100644 --- a/app/scripts/modules/cloudfoundry/src/cf.module.ts +++ b/app/scripts/modules/cloudfoundry/src/cf.module.ts @@ -21,6 +21,7 @@ import { CloudFoundryNoLoadBalancerModal } from './loadBalancer/configure/cloudF import 'cloudfoundry/pipeline/config/validation/instanceSize.validator'; import 'cloudfoundry/pipeline/config/validation/cfTargetImpedance.validator'; import 'cloudfoundry/pipeline/config/validation/validServiceParameterJson.validator'; +import 'cloudfoundry/pipeline/config/validation/validateManifestRequiredField.validator.ts'; import { CLOUD_FOUNDRY_DEPLOY_SERVICE_STAGE } from './pipeline/stages/deployService/cloudfoundryDeployServiceStage.module'; import { CLOUD_FOUNDRY_DESTROY_ASG_STAGE } from './pipeline/stages/destroyAsg/cloudfoundryDestroyAsgStage.module'; import { CLOUD_FOUNDRY_DESTROY_SERVICE_STAGE } from './pipeline/stages/destroyService/cloudfoundryDestroyServiceStage.module'; diff --git a/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/ManifestConfigValidator.ts b/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/ManifestConfigValidator.ts new file mode 100644 index 00000000000..41153265830 --- /dev/null +++ b/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/ManifestConfigValidator.ts @@ -0,0 +1,5 @@ +import { IValidatorConfig } from '@spinnaker/core'; + +export interface IManifestFieldValidatorConfig extends IValidatorConfig { + manifestType: string; +} diff --git a/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/validServiceParameterJson.validator.ts b/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/validServiceParameterJson.validator.ts index 227f0cbf99d..59edbd41aa6 100644 --- a/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/validServiceParameterJson.validator.ts +++ b/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/validServiceParameterJson.validator.ts @@ -1,66 +1,51 @@ -import { get, has, upperFirst } from 'lodash'; +import { get, upperFirst } from 'lodash'; -import { - IPipeline, - IStage, - IStageOrTriggerValidator, - ITrigger, - IValidatorConfig, - PipelineConfigValidator, -} from '@spinnaker/core'; - -export interface IServiceParameterJsonValidationConfig extends IValidatorConfig { - fieldName: string; - fieldLabel?: string; - message?: string; -} +import { IPipeline, IStage, IStageOrTriggerValidator, ITrigger, PipelineConfigValidator } from '@spinnaker/core'; +import { IManifestFieldValidatorConfig } from 'cloudfoundry/pipeline/config/validation/ManifestConfigValidator'; export class ServiceParameterJsonFieldValidator implements IStageOrTriggerValidator { - public validate( - pipeline: IPipeline, - stage: IStage | ITrigger, - validationConfig: IServiceParameterJsonValidationConfig, - ): string { - if (!this.passesValidation(stage, validationConfig)) { - return this.validationMessage(validationConfig, pipeline); - } - return null; - } - - protected passesValidation( - stage: IStage | ITrigger, - validationConfig: IServiceParameterJsonValidationConfig, - ): boolean { - return this.fieldIsValid(stage, validationConfig); - } - - protected validationMessage(validationConfig: IServiceParameterJsonValidationConfig, pipeline: IPipeline): string { - const fieldLabel: string = this.printableFieldLabel(validationConfig); - return ( - validationConfig.message || `${fieldLabel} should be a valid JSON string in ${pipeline.name}` - ); + private static validationMessage(validationConfig: IManifestFieldValidatorConfig): string { + const fieldLabel: string = ServiceParameterJsonFieldValidator.printableFieldLabel(validationConfig); + return validationConfig.message || `${fieldLabel} should be a valid JSON string.`; } - protected printableFieldLabel(config: IServiceParameterJsonValidationConfig): string { + private static printableFieldLabel(config: IManifestFieldValidatorConfig): string { const fieldLabel: string = config.fieldLabel || config.fieldName; return upperFirst(fieldLabel); } - protected fieldIsValid(stage: IStage | ITrigger, config: IServiceParameterJsonValidationConfig): boolean { - const fieldExists = has(stage, config.fieldName); - const field: any = get(stage, config.fieldName); + private static fieldIsValid(stage: IStage | ITrigger, config: IManifestFieldValidatorConfig): boolean { + const manifest: any = get(stage, 'manifest'); + const content: any = get(manifest, config.fieldName); - if (!fieldExists || !field.trim()) { + if (!content) { return true; } try { - JSON.parse(field); + JSON.parse(content); return true; } catch (e) { return false; } } + + public validate( + _pipeline: IPipeline, + stage: IStage | ITrigger, + validationConfig: IManifestFieldValidatorConfig, + ): string { + const manifest: any = get(stage, 'manifest'); + + if (manifest.type !== validationConfig.manifestType) { + return null; + } + + if (!ServiceParameterJsonFieldValidator.fieldIsValid(stage, validationConfig)) { + return ServiceParameterJsonFieldValidator.validationMessage(validationConfig); + } + return null; + } } PipelineConfigValidator.registerValidator('validServiceParameterJson', new ServiceParameterJsonFieldValidator()); diff --git a/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/validateManifestRequiredField.validator.ts b/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/validateManifestRequiredField.validator.ts new file mode 100644 index 00000000000..80d660c861d --- /dev/null +++ b/app/scripts/modules/cloudfoundry/src/pipeline/config/validation/validateManifestRequiredField.validator.ts @@ -0,0 +1,24 @@ +import { get, upperFirst } from 'lodash'; + +import { IPipeline, IStage, IStageOrTriggerValidator, ITrigger, PipelineConfigValidator } from '@spinnaker/core'; +import { IManifestFieldValidatorConfig } from 'cloudfoundry/pipeline/config/validation/ManifestConfigValidator'; + +export class RequiredManifestFieldValidator implements IStageOrTriggerValidator { + public validate( + _pipeline: IPipeline, + stage: IStage | ITrigger, + validationConfig: IManifestFieldValidatorConfig, + ): string { + const manifest: any = get(stage, 'manifest'); + + if (manifest.type !== validationConfig.manifestType) { + return null; + } + + const content: any = get(manifest, validationConfig.fieldName); + const fieldLabel = upperFirst(validationConfig.fieldName); + return content ? null : `${fieldLabel} is a required field for Deploy Service stages.`; + } +} + +PipelineConfigValidator.registerValidator('requiredManifestField', new RequiredManifestFieldValidator()); diff --git a/app/scripts/modules/cloudfoundry/src/pipeline/stages/deployService/CloudfoundryDeployServiceStageConfig.tsx b/app/scripts/modules/cloudfoundry/src/pipeline/stages/deployService/CloudfoundryDeployServiceStageConfig.tsx index 10451e216e9..46889ff2365 100644 --- a/app/scripts/modules/cloudfoundry/src/pipeline/stages/deployService/CloudfoundryDeployServiceStageConfig.tsx +++ b/app/scripts/modules/cloudfoundry/src/pipeline/stages/deployService/CloudfoundryDeployServiceStageConfig.tsx @@ -4,6 +4,7 @@ import { AccountSelectField, AccountService, IAccount, + IArtifactAccount, IRegion, IService, IServicePlan, @@ -17,12 +18,36 @@ import { includes } from 'lodash'; import './cloudfoundryDeployServiceStage.less'; -export interface ICloudfoundryDeployServiceStageConfigState { +interface ICloudfoundryServiceManifestDirectSource { + parameters?: string; + service: string; + serviceName: string; + servicePlan: string; + tags?: string[]; +} + +interface ICloudfoundryServiceManifestArtifactSource { + account: string; + reference: string; +} + +type ICloudFoundryServiceManifestSource = { type: string } & ( + | ICloudfoundryServiceManifestDirectSource + | ICloudfoundryServiceManifestArtifactSource); + +interface ICloudfoundryServiceStageConfigProps extends IStageConfigProps { + manifest: ICloudFoundryServiceManifestSource; +} + +interface ICloudfoundryDeployServiceStageConfigState { accounts: IAccount[]; + artifactAccount: string; + artifactAccounts: IArtifactAccount[]; cloudProvider: string; credentials: string; newTag: string; parameters: string; + reference: string; region: string; regions: IRegion[]; service: string; @@ -32,37 +57,76 @@ export interface ICloudfoundryDeployServiceStageConfigState { servicePlan: string; servicePlans: string[]; tags: string[]; + timeout: string; + type: string; } export class CloudfoundryDeployServiceStageConfig extends React.Component< - IStageConfigProps, + ICloudfoundryServiceStageConfigProps, ICloudfoundryDeployServiceStageConfigState > { - constructor(props: IStageConfigProps) { + constructor(props: ICloudfoundryServiceStageConfigProps) { super(props); props.stage.cloudProvider = 'cloudfoundry'; + props.stage.manifest = props.stage.manifest || { + service: '', + serviceName: '', + servicePlan: '', + type: 'direct', + }; + this.state = { accounts: [], + artifactAccount: props.stage.manifest.account, + artifactAccounts: [], cloudProvider: 'cloudfoundry', credentials: props.stage.credentials, newTag: '', - parameters: props.stage.parameters, + parameters: props.stage.manifest.parameters, + reference: props.stage.manifest.reference, region: props.stage.region, regions: [], - service: props.stage.service, + service: props.stage.manifest.service, services: [], - serviceName: props.stage.serviceName, + serviceName: props.stage.manifest.serviceName, serviceNamesAndPlans: [], - servicePlan: props.stage.servicePlan, + servicePlan: props.stage.manifest.servicePlan, servicePlans: [], - tags: props.stage.tags || [], + tags: props.stage.manifest.tags || [], + timeout: props.stage.timeout, + type: props.stage.manifest.type, }; } + private manifestTypeUpdated = (type: string): void => { + switch (type) { + case 'direct': + this.props.stage.manifest = { + service: '', + serviceName: '', + servicePlan: '', + type: 'direct', + }; + this.setState({ type: 'direct' }); + break; + case 'artifact': + this.props.stage.manifest = { + account: '', + reference: '', + type: 'artifact', + }; + this.setState({ type: 'artifact' }); + break; + } + }; + public componentDidMount = (): void => { AccountService.listAccounts('cloudfoundry').then(accounts => { this.setState({ accounts: accounts }); }); + AccountService.getArtifactAccounts().then(artifactAccounts => { + this.setState({ artifactAccounts: artifactAccounts }); + }); const { credentials, region } = this.props.stage; if (credentials) { this.clearAndReloadRegions(); @@ -93,7 +157,7 @@ export class CloudfoundryDeployServiceStageConfig extends React.Component< }); const { credentials, region } = this.props.stage; ServicesReader.getServices(credentials, region).then(services => { - const service = services.find(it => it.name === this.props.stage.service); + const service = services.find(it => it.name === this.props.stage.manifest.service); this.setState({ serviceNamesAndPlans: services, servicePlans: service ? service.servicePlans.map((it: IServicePlan) => it.name) : [], @@ -107,8 +171,9 @@ export class CloudfoundryDeployServiceStageConfig extends React.Component< this.setState({ credentials: credentials, region: '' }); this.props.stage.credentials = credentials; this.props.stage.region = ''; - this.props.stage.service = ''; - this.props.stage.servicePlan = ''; + this.props.stage.manifest.service = ''; + this.props.stage.manifest.serviceName = ''; + this.props.stage.manifest.servicePlan = ''; this.props.stageFieldUpdated(); if (credentials) { this.clearAndReloadRegions(); @@ -118,8 +183,9 @@ export class CloudfoundryDeployServiceStageConfig extends React.Component< private regionUpdated = (region: string): void => { this.setState({ region: region, service: '', servicePlan: '' }); this.props.stage.region = region; - this.props.stage.service = ''; - this.props.stage.servicePlan = ''; + this.props.stage.manifest.service = ''; + this.props.stage.manifest.serviceName = ''; + this.props.stage.manifest.servicePlan = ''; this.props.stageFieldUpdated(); this.clearAndReloadServices(); }; @@ -133,29 +199,29 @@ export class CloudfoundryDeployServiceStageConfig extends React.Component< servicePlan: '', servicePlans, }); - this.props.stage.service = service; - this.props.stage.servicePlan = ''; + this.props.stage.manifest.service = service; + this.props.stage.manifest.servicePlan = ''; this.props.stageFieldUpdated(); }; private servicePlanUpdated = (event: React.ChangeEvent): void => { const servicePlan = event.target.value; this.setState({ servicePlan }); - this.props.stage.servicePlan = servicePlan; + this.props.stage.manifest.servicePlan = servicePlan; this.props.stageFieldUpdated(); }; private serviceNameUpdated = (event: React.ChangeEvent): void => { const serviceName = event.target.value; this.setState({ serviceName }); - this.props.stage.serviceName = serviceName; + this.props.stage.manifest.serviceName = serviceName; this.props.stageFieldUpdated(); }; private parametersUpdated = (event: React.ChangeEvent): void => { const parameters = event.target.value; this.setState({ parameters }); - this.props.stage.parameters = parameters; + this.props.stage.manifest.parameters = parameters; this.props.stageFieldUpdated(); }; @@ -163,13 +229,27 @@ export class CloudfoundryDeployServiceStageConfig extends React.Component< this.setState({ newTag: event.target.value }); }; + private artifactAccountUpdated = (event: React.ChangeEvent): void => { + const artifactAccount = event.target.value; + this.setState({ artifactAccount }); + this.props.stage.manifest.account = artifactAccount; + this.props.stageFieldUpdated(); + }; + + private referenceUpdated = (event: React.ChangeEvent): void => { + const reference = event.target.value; + this.setState({ reference }); + this.props.stage.manifest.reference = reference; + this.props.stageFieldUpdated(); + }; + private addTag = (): void => { const { newTag } = this.state; - const tags = this.props.stage.tags || []; + const tags = this.props.stage.manifest.tags || []; if (!includes(tags, newTag.trim())) { const newTags = [...tags, newTag.trim()].sort((a, b) => a.localeCompare(b)); - this.props.stage.tags = newTags; + this.props.stage.manifest.tags = newTags; this.setState({ tags: newTags, newTag: '', @@ -179,42 +259,37 @@ export class CloudfoundryDeployServiceStageConfig extends React.Component< }; private deleteTag = (index: number): void => { - const { tags } = this.props.stage; + const { tags } = this.props.stage.manifest; tags.splice(index, 1); - this.props.stage.tags = tags; + this.props.stage.manifest.tags = tags; this.setState({ tags }); this.props.stageFieldUpdated(); }; private timeoutUpdated = (event: React.ChangeEvent): void => { - this.props.stage.timeout = event.target.value; + const timeout = event.target.value; + this.setState({ timeout }); + this.props.stage.timeout = timeout; this.props.stageFieldUpdated(); }; - public render() { - const { stage } = this.props; - const { credentials, parameters, service, serviceName, servicePlan, tags, timeout } = stage; - const { accounts, newTag, regions, servicePlans, services } = this.state; + private directManifestInput = ( + manifest: ICloudfoundryServiceManifestDirectSource, + state: ICloudfoundryDeployServiceStageConfigState, + ): JSX.Element => { + const { parameters, service, serviceName, servicePlan, tags } = manifest; + const { newTag, servicePlans, services } = state; return ( -
- - + + - - - -
@@ -294,6 +360,111 @@ export class CloudfoundryDeployServiceStageConfig extends React.Component<