Skip to content

Commit

Permalink
feat(provider/appengine): add deploy global configuration stage (#8599)
Browse files Browse the repository at this point in the history
* fix(provider/cf): fix env variables section

* feat(provider/appengine): add deploy global configuration stage

* fix conflict

* fix conflict

* fix conflicts

* adjust name to app engine

* adjust name to app engine
  • Loading branch information
zachsmith1 committed Sep 28, 2020
1 parent c16d3ff commit d36922b
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/appengine/src/appengine.module.ts
Expand Up @@ -17,6 +17,7 @@ import { APPENGINE_SERVER_GROUP_TRANSFORMER } from './serverGroup/transformer';
import { APPENGINE_SERVER_GROUP_WRITER } from './serverGroup/writer/serverGroup.write.service';
import './validation/ApplicationNameValidator';
import { CONFIG_FILE_ARTIFACT_LIST } from './serverGroup/configure/wizard/configFileArtifactList.module';
import './pipeline/stages/deployAppengineConfig/deployAppengineConfigStage';

import './logo/appengine.logo.less';

Expand Down
@@ -0,0 +1,117 @@
import React from 'react';

import { Observable, Subject } from 'rxjs';

import { get } from 'lodash';

import {
Application,
AppListExtractor,
FormikFormField,
IAccount,
IServerGroup,
IServerGroupFilter,
ReactSelectInput,
IStage,
} from '@spinnaker/core';

import { FormikProps } from 'formik';

export interface IFormikAccountRegionSelectorProps {
accounts: IAccount[];
application: Application;
cloudProvider: string;
componentName?: string;
credentialsField?: string;
formik: FormikProps<IStage>;
}

export interface IFormikAccountRegionSelectorState {
availableRegions: string[];
cloudProvider: string;
componentName: string;
credentialsField: string;
}

export class FormikAccountRegionSelector extends React.Component<
IFormikAccountRegionSelectorProps,
IFormikAccountRegionSelectorState
> {
private destroy$ = new Subject();

constructor(props: IFormikAccountRegionSelectorProps) {
super(props);
const credentialsField = props.credentialsField || 'credentials';
this.state = {
availableRegions: [],
cloudProvider: props.cloudProvider,
componentName: props.componentName || '',
credentialsField,
};
}

public componentDidMount(): void {
const { componentName, formik } = this.props;
const { credentialsField } = this.state;
const credentials = get(
formik.values,
componentName ? `${componentName}.${credentialsField}` : `${credentialsField}`,
undefined,
);
this.setRegionList(credentials);
}

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

private setRegionList = (credentials: string): void => {
const { application } = this.props;
const accountFilter: IServerGroupFilter = (serverGroup: IServerGroup) =>
serverGroup ? serverGroup.account === credentials : true;
Observable.fromPromise(application.ready())
.takeUntil(this.destroy$)
.subscribe(() => {
const availableRegions = AppListExtractor.getRegions([application], accountFilter);
availableRegions.sort();
this.setState({ availableRegions });
});
};

public accountChanged = (credentials: string): void => {
this.setRegionList(credentials);
};

public render() {
const { accounts } = this.props;
const { credentialsField, availableRegions, componentName } = this.state;
return (
<div className="col-md-9">
<div className="sp-margin-m-bottom">
<FormikFormField
name={componentName ? `${componentName}.${credentialsField}` : `${credentialsField}`}
label="Account"
input={props => (
<ReactSelectInput
{...props}
stringOptions={accounts && accounts.map((acc: IAccount) => acc.name)}
clearable={false}
/>
)}
onChange={this.accountChanged}
required={true}
/>
</div>

<div className="sp-margin-m-bottom">
<FormikFormField
name={componentName ? `${componentName}.region` : 'region'}
label="Region"
input={props => <ReactSelectInput {...props} stringOptions={availableRegions} clearable={false} />}
required={true}
/>
</div>
</div>
);
}
}
@@ -0,0 +1,204 @@
import React from 'react';

import {
IArtifact,
IExpectedArtifact,
excludeAllTypesExcept,
ArtifactTypePatterns,
StageArtifactSelectorDelegate,
IFormikStageConfigInjectedProps,
IAccount,
AccountService,
} from '@spinnaker/core';
import { Subject, Observable } from 'rxjs';
import { FormikAccountRegionSelector } from 'appengine/common/FormikAccountRegionSelector';

export interface IAppEngineDeployConfigSettingsState {
accounts: IAccount[];
}

export class DeployAppengineConfigForm extends React.Component<
IFormikStageConfigInjectedProps,
IAppEngineDeployConfigSettingsState
> {
private static readonly excludedArtifactTypes = excludeAllTypesExcept(
ArtifactTypePatterns.BITBUCKET_FILE,
ArtifactTypePatterns.CUSTOM_OBJECT,
ArtifactTypePatterns.EMBEDDED_BASE64,
ArtifactTypePatterns.GCS_OBJECT,
ArtifactTypePatterns.GITHUB_FILE,
ArtifactTypePatterns.GITLAB_FILE,
ArtifactTypePatterns.S3_OBJECT,
ArtifactTypePatterns.HTTP_FILE,
);

private destroy$ = new Subject();
public state: IAppEngineDeployConfigSettingsState = {
accounts: [],
};

public componentDidMount() {
Observable.fromPromise(AccountService.listAccounts('appengine'))
.takeUntil(this.destroy$)
.subscribe(accounts => this.setState({ accounts }));
}

private onTemplateArtifactEdited = (artifact: IArtifact, name: string) => {
this.props.formik.setFieldValue(`${name}.id`, null);
this.props.formik.setFieldValue(`${name}.artifact`, artifact);
this.props.formik.setFieldValue(`${name}.account`, artifact.artifactAccount);
};

private onTemplateArtifactSelected = (id: string, name: string) => {
this.props.formik.setFieldValue(`${name}.id`, id);
this.props.formik.setFieldValue(`${name}.artifact`, null);
};

private removeInputArtifact = (name: string) => {
this.props.formik.setFieldValue(name, null);
};

private getInputArtifact = (stage: any, name: string) => {
if (!stage[name]) {
return {
account: '',
id: '',
};
} else {
return stage[name];
}
};

public render() {
const stage = this.props.formik.values;
const accounts = this.state.accounts;
return (
<div>
<div className="col-md-offset-0 col-md-9">
<h4>Basic Settings</h4>
</div>
<div>
<FormikAccountRegionSelector
componentName={''}
accounts={accounts}
application={this.props.application}
cloudProvider={'appengine'}
credentialsField={'account'}
formik={this.props.formik}
/>
</div>
<div className="col-md-offset-0 col-md-9">
<h4>Configuration Settings</h4>
</div>
<div>
<div className="col-md-offset-1 col-md-9">
<StageArtifactSelectorDelegate
artifact={this.getInputArtifact(stage, 'cronArtifact').artifact}
excludedArtifactTypePatterns={DeployAppengineConfigForm.excludedArtifactTypes}
expectedArtifactId={this.getInputArtifact(stage, 'cronArtifact').id}
label="Cron Artifact"
onArtifactEdited={artifact => {
this.onTemplateArtifactEdited(artifact, 'cronArtifact');
}}
helpKey={''}
onExpectedArtifactSelected={(artifact: IExpectedArtifact) =>
this.onTemplateArtifactSelected(artifact.id, 'cronArtifact')
}
pipeline={this.props.pipeline}
stage={stage}
/>
</div>
<div className="col-md-1">
<div className="form-control-static">
<button onClick={() => this.removeInputArtifact('cronArtifact')}>
<span className="glyphicon glyphicon-trash" />
<span className="sr-only">Remove field</span>
</button>
</div>
</div>
</div>
<div>
<div className="col-md-offset-1 col-md-9">
<StageArtifactSelectorDelegate
artifact={this.getInputArtifact(stage, 'dispatchArtifact').artifact}
excludedArtifactTypePatterns={DeployAppengineConfigForm.excludedArtifactTypes}
expectedArtifactId={this.getInputArtifact(stage, 'dispatchArtifact').id}
label="Dispatch Artifact"
onArtifactEdited={artifact => {
this.onTemplateArtifactEdited(artifact, 'dispatchArtifact');
}}
helpKey={''}
onExpectedArtifactSelected={(artifact: IExpectedArtifact) =>
this.onTemplateArtifactSelected(artifact.id, 'dispatchArtifact')
}
pipeline={this.props.pipeline}
stage={stage}
/>
</div>
<div className="col-md-1">
<div className="form-control-static">
<button onClick={() => this.removeInputArtifact('dispatchArtifact')}>
<span className="glyphicon glyphicon-trash" />
<span className="sr-only">Remove field</span>
</button>
</div>
</div>
</div>
<div>
<div className="col-md-offset-1 col-md-9">
<StageArtifactSelectorDelegate
artifact={this.getInputArtifact(stage, 'indexArtifact').artifact}
excludedArtifactTypePatterns={DeployAppengineConfigForm.excludedArtifactTypes}
expectedArtifactId={this.getInputArtifact(stage, 'indexArtifact').id}
label="Index Artifact"
onArtifactEdited={artifact => {
this.onTemplateArtifactEdited(artifact, 'indexArtifact');
}}
helpKey={''}
onExpectedArtifactSelected={(artifact: IExpectedArtifact) =>
this.onTemplateArtifactSelected(artifact.id, 'indexArtifact')
}
pipeline={this.props.pipeline}
stage={stage}
/>
</div>
<div className="col-md-1">
<div className="form-control-static">
<button onClick={() => this.removeInputArtifact('indexArtifact')}>
<span className="glyphicon glyphicon-trash" />
<span className="sr-only">Remove field</span>
</button>
</div>
</div>
</div>
<div>
<div className="col-md-offset-1 col-md-9">
<StageArtifactSelectorDelegate
artifact={this.getInputArtifact(stage, 'queueArtifact').artifact}
excludedArtifactTypePatterns={DeployAppengineConfigForm.excludedArtifactTypes}
expectedArtifactId={this.getInputArtifact(stage, 'queueArtifact').id}
label="Queue Artifact"
onArtifactEdited={artifact => {
this.onTemplateArtifactEdited(artifact, 'queueArtifact');
}}
helpKey={''}
onExpectedArtifactSelected={(artifact: IExpectedArtifact) =>
this.onTemplateArtifactSelected(artifact.id, 'queueArtifact')
}
pipeline={this.props.pipeline}
stage={stage}
/>
</div>
<div className="col-md-1">
<div className="form-control-static">
<button onClick={() => this.removeInputArtifact('queueArtifact')}>
<span className="glyphicon glyphicon-trash" />
<span className="sr-only">Remove field</span>
</button>
</div>
</div>
</div>
</div>
);
}
}
@@ -0,0 +1,33 @@
import React from 'react';
import { cloneDeep } from 'lodash';
import { FormikErrors } from 'formik';

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

import { DeployAppengineConfigForm } from './DeployAppengineConfigForm';

export function DeployAppengineConfigurationConfig({ application, pipeline, stage, updateStage }: IStageConfigProps) {
const stageWithDefaults = React.useMemo(() => {
return {
...cloneDeep(stage),
};
}, []);

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

export function validateDeployAppengineConfigurationStage(stage: IStage): FormikErrors<IStage> {
const formValidator = new FormValidator(stage);
formValidator.field('account').required();
formValidator.field('region').required();
return formValidator.validateForm();
}
@@ -0,0 +1,19 @@
import { ExecutionArtifactTab, ExecutionDetailsTasks, Registry } from '@spinnaker/core';

import {
validateDeployAppengineConfigurationStage,
DeployAppengineConfigurationConfig,
} from './DeployAppengineConfigurationConfig';

export const DEPLOY_APPENGINE_CONFIG_STAGE_KEY = 'deployAppEngineConfiguration';

Registry.pipeline.registerStage({
label: 'Deploy App Engine Configuration',
description: 'Deploy index, dispatch, cron, and queue configuration to App Engine.',
key: DEPLOY_APPENGINE_CONFIG_STAGE_KEY,
component: DeployAppengineConfigurationConfig,
producesArtifacts: false,
cloudProvider: 'appengine',
executionDetailsSections: [ExecutionDetailsTasks, ExecutionArtifactTab],
validateFn: validateDeployAppengineConfigurationStage,
});

0 comments on commit d36922b

Please sign in to comment.