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(codepipeline): add support for a StepFunctions invoke action #8931

Merged
merged 9 commits into from
Jul 18, 2020
52 changes: 51 additions & 1 deletion packages/@aws-cdk/aws-codepipeline-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ const buildAction = new codepipeline_actions.CodeBuildAction({
build: {
commands: 'export MY_VAR="some value"',
},
},
},
}),
}),
variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you
Expand Down Expand Up @@ -794,3 +794,53 @@ new codepipeline_actions.CodeBuildAction({

See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html)
on how to write a Lambda function invoked from CodePipeline.

### AWS Step Functions

This module contains an Action that allows you to invoke a Step Function in a Pipeline:

```ts
import * as stepfunction from '@aws-cdk/aws-stepfunctions';

const pipeline = new codepipeline.Pipeline(this, 'MyPipeline');
const startState = new stepfunction.Pass(stack, 'StartState');
const simpleStateMachine = new stepfunction.StateMachine(stack, 'SimpleStateMachine', {
definition: startState,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 spaces instead of 4:

Suggested change
definition: startState,
definition: startState,

});
const stepFunctionAction = new codepipeline_actions.StepFunctionsInvokeAction({
actionName: 'Invoke',
stateMachine: simpleStateMachine,
stateMachineInput: codepipeline_actions.StateMachineInput.literal({IsHelloWorldExample: true}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stateMachineInput: codepipeline_actions.StateMachineInput.literal({IsHelloWorldExample: true}),
stateMachineInput: codepipeline_actions.StateMachineInput.literal({ IsHelloWorldExample: true }),

});
pipeline.addStage({
stageName: 'StepFunctions',
actions: [stepFunctionAction],
});
```

The StateMachineInputType can be a Literal or FilePath.
Input artifact and StateMachineInputField are required when
the StateMachineInputType is set as FilePath.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The StateMachineInputType can be a Literal or FilePath.
Input artifact and StateMachineInputField are required when
the StateMachineInputType is set as FilePath.
The `StateMachineInput` can be created with one of 2 static factory methods:
`literal`, which takes an arbitrary map as its only argument, or `filePath`:


```ts
import * as stepfunction from '@aws-cdk/aws-stepfunctions';

const pipeline = new codepipeline.Pipeline(this, 'MyPipeline');
const inputArtifact = new codepipeline.Artifact();
const startState = new stepfunction.Pass(stack, 'StartState');
const simpleStateMachine = new stepfunction.StateMachine(stack, 'SimpleStateMachine', {
definition: startState,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
definition: startState,
definition: startState,

});
const stepFunctionAction = new codepipeline_actions.StepFunctionsInvokeAction({
actionName: 'Invoke',
stateMachine: simpleStateMachine,
stateMachineInput: codepipeline_actions.StateMachineInput.filePath('assets/input.json', inputArtifact),
});
pipeline.addStage({
stageName: 'StepFunctions',
actions: [stepFunctionAction],
});
```

See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-StepFunctions.html)
for information on Action structure reference.
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export * from './lambda/invoke-action';
export * from './manual-approval-action';
export * from './s3/deploy-action';
export * from './s3/source-action';
export * from './stepfunctions/invoke-action';
export * from './action'; // for some reason, JSII fails building the module without exporting this class
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as iam from '@aws-cdk/aws-iam';
import * as stepfunction from '@aws-cdk/aws-stepfunctions';
import { Construct } from '@aws-cdk/core';
import { Action } from '../action';

/**
* Represents the input for the StateMachine.
*/
export class StateMachineInput {
/**
* When the input type is FilePath, input artifact and
* filepath must be specified.
*/
public static filePath(filePath: string, inputArtifact: codepipeline.Artifact): StateMachineInput {
Copy link
Contributor

@skinny85 skinny85 Jul 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies @madhumitranrl , but I came up with an even better API :). We have a class in the CodePipeline library exactly for this (file in an artifact) purpose: ArtifactPath. So, you can change this function to:

  public static filePath(inuputFile: codepipeline.ArtifactPath): StateMachineInput {
    return new StateMachineInput(inuputFile.location, inuputFile.artifact, 'FilePath');
  }

And the customer uses it like this:

StateMachineInput .filePath(buildArtifact.atPath('path/to/my/file.json'));

This will be perfect!

Now, with this design, what do you think about getting rid of StateMachineInput class? We can change StepFunctionsInvokeActionProps to be just:

export interface StepFunctionInvokeActionProps {
  // these 3 same as now
  readonly output?: codepipeline.Artifact;
  readonly stateMachine: stepfunction.IStateMachine;
  readonly executionNamePrefix?: string;
 
  // only one of inputFile and inputObject can be provided - we check that in the  StepFunctionsInvokeAction class
  readonly inputFile?: codepipeline.ArtifactPath;
  readonly inputObject?: object;
}

Let me know which one you like better!

return new StateMachineInput(filePath, inputArtifact, 'FilePath');
}

/**
* When the input type is Literal, input value is passed
* directly to the state machine input.
*/
public static literal(object: object): StateMachineInput {
return new StateMachineInput(JSON.stringify(object), undefined, 'Literal');
}

/**
* The optional input Artifact of the Action.
* If InputType is set to FilePath, this artifact is required
* and is used to source the input for the state machine execution.
*
* @default - the Action will not have any inputs
* @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-StepFunctions.html#action-reference-StepFunctions-example
*/
public readonly inputArtifact?: codepipeline.Artifact;

/**
* Optional StateMachine InputType
* InputType can be Literal or FilePath
*
* @default - Literal
*/
public readonly inputType?: string;

/**
* When InputType is set to Literal (default), the Input field is used
* directly as the input for the state machine execution.
* Otherwise, the state machine is invoked with an empty JSON object {}.
*
* When InputType is set to FilePath, this field is required.
* An input artifact is also required when InputType is set to FilePath.
*
* @default - none
*/
public readonly input: any;

private constructor(input: any, inputArtifact: codepipeline.Artifact | undefined, inputType: string) {
this.input = input;
this.inputArtifact = inputArtifact;
this.inputType = inputType;
}
}

/**
* Construction properties of the {@link StepFunctionsInvokeAction StepFunction Invoke Action}.
*/
export interface StepFunctionsInvokeActionProps extends codepipeline.CommonAwsActionProps {
/**
* The optional output Artifact of the Action.
*
* @default the Action will not have any outputs
*/
readonly output?: codepipeline.Artifact;

/**
* The state machine to invoke.
*/
readonly stateMachine: stepfunction.IStateMachine;

/**
* Represents the input to the StateMachine.
* This includes input artifact, input type and the statemachine input.
*
* @default - none
*/
readonly stateMachineInput?: StateMachineInput;

/**
* Prefix (optional)
*
* By default, the action execution ID is used as the state machine execution name.
* If a prefix is provided, it is prepended to the action execution ID with a hyphen and
* together used as the state machine execution name.
*
* @default - action execution ID
*/
readonly executionNamePrefix?: string;
}

/**
* StepFunction invoke Action that is provided by an AWS CodePipeline.
*/
export class StepFunctionsInvokeAction extends Action {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of calling it StepFunctionInvokeAction (not Functions)? It does invoke a single StepFunction...

private readonly props: StepFunctionsInvokeActionProps;

constructor(props: StepFunctionsInvokeActionProps) {
super({
...props,
resource: props.stateMachine,
category: codepipeline.ActionCategory.INVOKE,
provider: 'StepFunctions',
artifactBounds: {
minInputs: 0,
maxInputs: 1,
minOutputs: 0,
maxOutputs: 1,
},
inputs: (props.stateMachineInput && props.stateMachineInput.inputArtifact) ? [props.stateMachineInput.inputArtifact] : [],
outputs: (props.output) ? [props.output] : [],
});
this.props = props;
}

protected bound(_scope: Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
// allow pipeline to invoke this step function
options.role.addToPolicy(new iam.PolicyStatement({
actions: ['states:StartExecution', 'states:DescribeStateMachine'],
resources: [this.props.stateMachine.stateMachineArn],
}));

// allow state machine executions to be inspected
options.role.addToPolicy(new iam.PolicyStatement({
actions: ['states:DescribeExecution'],
resources: [`arn:aws:states:*:*:execution:${this.props.stateMachine.stateMachineArn}:${this.props.executionNamePrefix ?? ''}*`],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually have a method for that in our standard library. This should be:

      resources: [Stack.of(this.props.stateMachine).formatArn({
        service: 'states',
        resource: 'execution',
        resourceName: `${this.props.stateMachine.stateMachineArn}:${this.props.executionNamePrefix ?? ''}*`,
        sep: ':',
      })],

}));

// allow the Role access to the Bucket, if there are any inputs/outputs
if ((this.actionProperties.inputs ?? []).length > 0) {
options.bucket.grantRead(options.role);
}
if ((this.actionProperties.outputs ?? []).length > 0) {
options.bucket.grantWrite(options.role);
}

return {
configuration: {
StateMachineArn: this.props.stateMachine.stateMachineArn,
Input: (this.props.stateMachineInput) ? this.props.stateMachineInput.input : [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to fill this out if stateMachineInput is undefined? We can't make it undefined as well?

InputType: (this.props.stateMachineInput) ? this.props.stateMachineInput.inputType : undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks to TypeScript's safe navigation operator, this can be simplified to:

Suggested change
InputType: (this.props.stateMachineInput) ? this.props.stateMachineInput.inputType : undefined,
InputType: this.props.stateMachineInput?.inputType,

ExecutionNamePrefix: this.props.executionNamePrefix,
},
};
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@aws-cdk/aws-s3": "0.0.0",
"@aws-cdk/aws-sns": "0.0.0",
"@aws-cdk/aws-sns-subscriptions": "0.0.0",
"@aws-cdk/aws-stepfunctions": "0.0.0",
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
"@aws-cdk/core": "0.0.0",
"case": "1.6.3",
"constructs": "^3.0.2"
Expand All @@ -109,6 +110,7 @@
"@aws-cdk/aws-s3": "0.0.0",
"@aws-cdk/aws-sns": "0.0.0",
"@aws-cdk/aws-sns-subscriptions": "0.0.0",
"@aws-cdk/aws-stepfunctions": "0.0.0",
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
"@aws-cdk/core": "0.0.0",
"constructs": "^3.0.2"
},
Expand Down
Loading