Skip to content

Commit

Permalink
feat(codepipeline): introduce the Action abstract class (#14009)
Browse files Browse the repository at this point in the history
Add an officially supported class to the API of the CodePipeline module
that contains some logic common to all implementations of the `IAction` interface,
like dealing with variable namespaces, or methods like `onStateChange()`.

There was previously a class like that in the codepipeline-actions module,
but it was marked experimental, and had some strong opinions on the construction way of the subclasses
(required passing the `actionProperties` through a `super()` call in the constructor,
which is not very flexible).

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
skinny85 authored Apr 15, 2021
1 parent 3107d03 commit 4b6a6cc
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 142 deletions.
122 changes: 6 additions & 116 deletions packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,15 @@
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as events from '@aws-cdk/aws-events';
import { Lazy } from '@aws-cdk/core';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

/**
* Low-level class for generic CodePipeline Actions.
*
* WARNING: this class should not be externally exposed, but is currently visible
* because of a limitation of jsii (https://github.com/aws/jsii/issues/524).
*
* This class will disappear in a future release and should not be used.
*
* @experimental
* If you're implementing your own IAction,
* prefer to use the Action class from the codepipeline module.
*/
export abstract class Action implements codepipeline.IAction {
public readonly actionProperties: codepipeline.ActionProperties;
private _pipeline?: codepipeline.IPipeline;
private _stage?: codepipeline.IStage;
private _scope?: Construct;
private readonly customerProvidedNamespace?: string;
private readonly namespaceOrToken: string;
private actualNamespace?: string;
private variableReferenced = false;
export abstract class Action extends codepipeline.Action {
protected readonly providedActionProperties: codepipeline.ActionProperties;

protected constructor(actionProperties: codepipeline.ActionProperties) {
this.customerProvidedNamespace = actionProperties.variablesNamespace;
this.namespaceOrToken = Lazy.string({
produce: () => {
// make sure the action was bound (= added to a pipeline)
if (this.actualNamespace !== undefined) {
return this.customerProvidedNamespace !== undefined
// if a customer passed a namespace explicitly, always use that
? this.customerProvidedNamespace
// otherwise, only return a namespace if any variable was referenced
: (this.variableReferenced ? this.actualNamespace : undefined);
} else {
throw new Error(`Cannot reference variables of action '${this.actionProperties.actionName}', ` +
'as that action was never added to a pipeline');
}
},
});
this.actionProperties = {
...actionProperties,
variablesNamespace: this.namespaceOrToken,
};
}

public bind(scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
this._pipeline = stage.pipeline;
this._stage = stage;
this._scope = scope;

this.actualNamespace = this.customerProvidedNamespace === undefined
// default a namespace name, based on the stage and action names
? `${stage.stageName}_${this.actionProperties.actionName}_NS`
: this.customerProvidedNamespace;

return this.bound(scope, stage, options);
}

public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) {
const rule = new events.Rule(this.scope, name, options);
rule.addTarget(target);
rule.addEventPattern({
detailType: ['CodePipeline Action Execution State Change'],
source: ['aws.codepipeline'],
resources: [this.pipeline.pipelineArn],
detail: {
stage: [this.stage.stageName],
action: [this.actionProperties.actionName],
},
});
return rule;
}

protected variableExpression(variableName: string): string {
this.variableReferenced = true;
return `#{${this.namespaceOrToken}.${variableName}}`;
}

/**
* The method called when an Action is attached to a Pipeline.
* This method is guaranteed to be called only once for each Action instance.
*
* @param options an instance of the {@link ActionBindOptions} class,
* that contains the necessary information for the Action
* to configure itself, like a reference to the Role, etc.
*/
protected abstract bound(scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig;

private get pipeline(): codepipeline.IPipeline {
if (this._pipeline) {
return this._pipeline;
} else {
throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange');
}
}

private get stage(): codepipeline.IStage {
if (this._stage) {
return this._stage;
} else {
throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange');
}
}

/**
* Retrieves the Construct scope of this Action.
* Only available after the Action has been added to a Stage,
* and that Stage to a Pipeline.
*/
private get scope(): Construct {
if (this._scope) {
return this._scope;
} else {
throw new Error('Action must be added to a stage that is part of a pipeline first');
}
super();
this.providedActionProperties = actionProperties;
}
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ export * from './s3/deploy-action';
export * from './s3/source-action';
export * from './stepfunctions/invoke-action';
export * from './servicecatalog/deploy-action';
export * from './action'; // for some reason, JSII fails building the module without exporting this class
export * from './action';
1 change: 0 additions & 1 deletion packages/@aws-cdk/aws-codepipeline-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@
"props-default-doc:@aws-cdk/aws-codepipeline-actions.CloudFormationCreateUpdateStackActionProps.extraInputs",
"props-default-doc:@aws-cdk/aws-codepipeline-actions.CloudFormationDeleteStackActionProps.extraInputs",
"props-default-doc:@aws-cdk/aws-codepipeline-actions.CodeBuildActionProps.extraInputs",
"docs-public-apis:@aws-cdk/aws-codepipeline-actions.Action.bind",
"docs-public-apis:@aws-cdk/aws-codepipeline-actions.CodeCommitSourceActionProps.branch",
"docs-public-apis:@aws-cdk/aws-codepipeline-actions.EcrSourceActionProps.output",
"docs-public-apis:@aws-cdk/aws-codepipeline-actions.GitHubSourceActionProps.output",
Expand Down
129 changes: 127 additions & 2 deletions packages/@aws-cdk/aws-codepipeline/lib/action.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import { IResource } from '@aws-cdk/core';
import { IResource, Lazy } from '@aws-cdk/core';
import { Artifact } from './artifact';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down Expand Up @@ -119,7 +119,10 @@ export interface ActionConfig {
}

/**
* A Pipeline Action
* A Pipeline Action.
* If you want to implement this interface,
* consider extending the {@link Action} class,
* which contains some common logic.
*/
export interface IAction {
/**
Expand Down Expand Up @@ -254,3 +257,125 @@ export interface CommonAwsActionProps extends CommonActionProps {
*/
readonly role?: iam.IRole;
}

/**
* Low-level class for generic CodePipeline Actions implementing the {@link IAction} interface.
* Contains some common logic that can be re-used by all {@link IAction} implementations.
* If you're writing your own Action class,
* feel free to extend this class.
*/
export abstract class Action implements IAction {
/**
* This is a renamed version of the {@link IAction.actionProperties} property.
*/
protected abstract readonly providedActionProperties: ActionProperties;

private __actionProperties?: ActionProperties;
private __pipeline?: IPipeline;
private __stage?: IStage;
private __scope?: Construct;
private readonly _namespaceToken: string;
private _customerProvidedNamespace?: string;
private _actualNamespace?: string;

private _variableReferenced = false;

protected constructor() {
this._namespaceToken = Lazy.string({
produce: () => {
// make sure the action was bound (= added to a pipeline)
if (this._actualNamespace === undefined) {
throw new Error(`Cannot reference variables of action '${this.actionProperties.actionName}', ` +
'as that action was never added to a pipeline');
} else {
return this._customerProvidedNamespace !== undefined
// if a customer passed a namespace explicitly, always use that
? this._customerProvidedNamespace
// otherwise, only return a namespace if any variable was referenced
: (this._variableReferenced ? this._actualNamespace : undefined);
}
},
});
}

public get actionProperties(): ActionProperties {
if (this.__actionProperties === undefined) {
const actionProperties = this.providedActionProperties;
this._customerProvidedNamespace = actionProperties.variablesNamespace;
this.__actionProperties = {
...actionProperties,
variablesNamespace: this._customerProvidedNamespace === undefined
? this._namespaceToken
: this._customerProvidedNamespace,
};
}
return this.__actionProperties;
}

public bind(scope: Construct, stage: IStage, options: ActionBindOptions): ActionConfig {
this.__pipeline = stage.pipeline;
this.__stage = stage;
this.__scope = scope;

this._actualNamespace = this._customerProvidedNamespace === undefined
// default a namespace name, based on the stage and action names
? `${stage.stageName}_${this.actionProperties.actionName}_NS`
: this._customerProvidedNamespace;

return this.bound(scope, stage, options);
}

public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) {
const rule = new events.Rule(this._scope, name, options);
rule.addTarget(target);
rule.addEventPattern({
detailType: ['CodePipeline Action Execution State Change'],
source: ['aws.codepipeline'],
resources: [this._pipeline.pipelineArn],
detail: {
stage: [this._stage.stageName],
action: [this.actionProperties.actionName],
},
});
return rule;
}

protected variableExpression(variableName: string): string {
this._variableReferenced = true;
return `#{${this._namespaceToken}.${variableName}}`;
}

/**
* This is a renamed version of the {@link IAction.bind} method.
*/
protected abstract bound(scope: Construct, stage: IStage, options: ActionBindOptions): ActionConfig;

private get _pipeline(): IPipeline {
if (this.__pipeline) {
return this.__pipeline;
} else {
throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange');
}
}

private get _stage(): IStage {
if (this.__stage) {
return this.__stage;
} else {
throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange');
}
}

/**
* Retrieves the Construct scope of this Action.
* Only available after the Action has been added to a Stage,
* and that Stage to a Pipeline.
*/
private get _scope(): Construct {
if (this.__scope) {
return this.__scope;
} else {
throw new Error('Action must be added to a stage that is part of a pipeline first');
}
}
}
3 changes: 0 additions & 3 deletions packages/@aws-cdk/aws-codepipeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,6 @@
"props-default-doc:@aws-cdk/aws-codepipeline.ActionProperties.runOrder",
"docs-public-apis:@aws-cdk/aws-codepipeline.ActionProperties.version",
"props-default-doc:@aws-cdk/aws-codepipeline.ActionProperties.version",
"docs-public-apis:@aws-cdk/aws-codepipeline.IAction.actionProperties",
"docs-public-apis:@aws-cdk/aws-codepipeline.IAction.bind",
"docs-public-apis:@aws-cdk/aws-codepipeline.IAction.onStateChange",
"docs-public-apis:@aws-cdk/aws-codepipeline.IStage.pipeline",
"docs-public-apis:@aws-cdk/aws-codepipeline.IStage.addAction",
"docs-public-apis:@aws-cdk/aws-codepipeline.IStage.onStateChange",
Expand Down
14 changes: 5 additions & 9 deletions packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import { IResource } from '@aws-cdk/core';
import { Construct } from 'constructs';
Expand All @@ -24,12 +23,13 @@ export interface FakeBuildActionProps extends codepipeline.CommonActionProps {
resource?: IResource;
}

export class FakeBuildAction implements codepipeline.IAction {
public readonly actionProperties: codepipeline.ActionProperties;
export class FakeBuildAction extends codepipeline.Action {
protected readonly providedActionProperties: codepipeline.ActionProperties;
private readonly customConfigKey: string | undefined;

constructor(props: FakeBuildActionProps) {
this.actionProperties = {
super();
this.providedActionProperties = {
...props,
category: codepipeline.ActionCategory.BUILD,
provider: 'Fake',
Expand All @@ -40,16 +40,12 @@ export class FakeBuildAction implements codepipeline.IAction {
this.customConfigKey = props.customConfigKey;
}

public bind(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions):
public bound(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
return {
configuration: {
CustomConfigKey: this.customConfigKey,
},
};
}

public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule {
throw new Error('onStateChange() is not available on FakeBuildAction');
}
}
15 changes: 5 additions & 10 deletions packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as events from '@aws-cdk/aws-events';
import { Lazy } from '@aws-cdk/core';
import { Construct } from 'constructs';
import * as codepipeline from '../lib';
Expand All @@ -15,15 +14,15 @@ export interface FakeSourceActionProps extends codepipeline.CommonActionProps {
readonly region?: string;
}

export class FakeSourceAction implements codepipeline.IAction {
export class FakeSourceAction extends codepipeline.Action {
public readonly inputs?: codepipeline.Artifact[];
public readonly outputs?: codepipeline.Artifact[];
public readonly variables: IFakeSourceActionVariables;

public readonly actionProperties: codepipeline.ActionProperties;
protected readonly providedActionProperties: codepipeline.ActionProperties;

constructor(props: FakeSourceActionProps) {
this.actionProperties = {
super();
this.providedActionProperties = {
...props,
category: codepipeline.ActionCategory.SOURCE,
provider: 'Fake',
Expand All @@ -35,12 +34,8 @@ export class FakeSourceAction implements codepipeline.IAction {
};
}

public bind(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions):
public bound(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
return {};
}

public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule {
throw new Error('onStateChange() is not available on FakeSourceAction');
}
}

0 comments on commit 4b6a6cc

Please sign in to comment.