aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.65k stars 3.91k forks source link

aws codepipeline: Stage does not implement IStage in Python #25661

Open jonodrew opened 1 year ago

jonodrew commented 1 year ago

Describe the bug

I'm trying to follow the documentation here: https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_codepipeline_actions/README.html#manual-approval-action. My aim is to add an action at the end of a testing stage to destroy the resources I don't need any more.

However, when I try to call add_action on a StageDeployent object I get the error 'StageDeployment' object has no attribute 'add_action'. When I try to call it on a Stage object, I get the same error: Stage object has no attribute add_action

Expected Behavior

I expect the code to synthesize as written in the docs

Current Behavior

Error as above

Reproduction Steps

class MentorMatchPipeline(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs):
        super().__init__(scope, construct_id, **kwargs)
        pipeline = CodePipeline(self, "MentorMatchPipeline",
                                pipeline_name="Pipeline",
                                synth=ShellStep("Synth",
                                                input=CodePipelineSource.git_hub("mentor-matching-online/mentor-match", "main"),
                                                commands=["npm install -g aws-cdk",
                                                          "cd mentor-match-infra",
                                                          "python -m pip install -r requirements.txt",
                                                          "cdk synth"],
                                                primary_output_directory="mentor-match-infra/cdk.out"
                                                )
                                )
        testing_stage: Stage = MentorMatchAppStage(self, "testing")
        pipeline.add_stage(testing_stage)

        production_stage: StageDeployment = pipeline.add_stage(
            MentorMatchAppStage(self)
        )
        production_stage.add_pre(
            ManualApprovalStep('approval')
        )
        delete_action = CloudFormationDeleteStackAction(
            admin_permissions=True,
            stack_name="MentorMatchStack",
            action_name="delete test stack"
        )
        production_stage.add_action(delete_action) # this should work, but throws an AttributeError

Adding this to an existing pipeline will cause the synth to fail with the error Stage object has no attribute 'add_action'

Possible Solution

No response

Additional Information/Context

No response

CDK CLI Version

2.79.1

Framework Version

2.79.1

Node.js Version

20.1.0

OS

Linux

Language

Python

Language Version

3.9.10

Other information

aws-cdk-lib==2.79.1 jsii==1.81.0

ETA: expanded example

peterwoodworth commented 1 year ago

You're using a cdk.Stage. The examples we have that I found, and what the API reference shows is that the stage class with the addAction method is a pipelines.StageDeployment. Please let me know if there are any docs you found that use a cdk.Stage. Thanks

jonodrew commented 1 year ago

Thanks for responding. As I wrote in the report, the StageDeployment object also fails cdk synth with the error AttributeError: 'StageDeployment' object has no attribute 'add_action'

peterwoodworth commented 1 year ago

Can you please provide a snippet for this?

jonodrew commented 1 year ago

I've expanded the existing snippet. You can also see the code I'm working on, if it's helpful to see the larger context: https://github.com/mentor-matching-online/mentor-match/blob/main/mentor-match-infra/mentor_match_infra/mentor_match_pipeline.py

In that example, neither testing_stage nor production_stage will accept a add_action method

peterwoodworth commented 1 year ago

You still aren't creating a StageDeployment. You're importing Stage from the core library and trying to directly call add_action on this Stage. This shouldn't ever work.

stage_deployment = pipeline.add_stage(s) stage_deployment.add_action # fails with object has no attribute 'add_action'

This should work I think, except I don't think you created the CodePipeline correctly. You've created a Codepipeline, which I'm not sure where that comes from, and the CodePipeline construct requires a synth prop at the least as well.

peterwoodworth commented 1 year ago

Checking source code now

jonodrew commented 1 year ago

Thanks - you make a good point. My snippet isn't reproducible, sorry. The source code is clearer I hope

HannesOberreiter commented 1 year ago

Hi, can I hijack this discussion. As I struggle with a more or less similar problem and tried various version and workarounds to add an action to my pipeline without success.

My code is in TypeScript but as far as I can see and also in the linked API documentation there is no method addAction or in the Python case add_action (https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_pipelines.StageDeployment.html). Or am I missing something?

The reference code I got as answer for my discussion thread is also not reproducible / working: https://github.com/aws/aws-cdk/discussions/25650

Curious if @peterwoodworth can guide us what we did wrong and how to add correctly an action to a pipeline.

Thanks already for your hard work!

peterwoodworth commented 1 year ago

Hey @jonodrew, I took the code in your repository, refactored it a little bit, and didn't run into any errors with the stages. Here's my refactored code:

from aws_cdk import Stack, Environment, Stage
from aws_cdk.pipelines import CodePipeline, ShellStep, CodePipelineSource, ManualApprovalStep
from aws_cdk import aws_lambda as _lambda
from constructs import Construct

class MentorMatchPipeline(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs):
        super().__init__(scope, construct_id, **kwargs)
        pipeline = CodePipeline(self, "MentorMatchPipeline",
                                pipeline_name="Pipeline",
                                cross_account_keys=True,
                                synth=ShellStep("Synth",
                                                input=CodePipelineSource.git_hub("mentor-matching-online/mentor-match", "main"),
                                                commands=["npm install -g aws-cdk",
                                                          "cd mentor-match-infra",
                                                          "python -m pip install -r requirements.txt",
                                                          "cdk synth"],
                                                primary_output_directory="mentor-match-infra/cdk.out"
                                                )
                                )
        testing_stage = MentorMatchAppStage(self, "testing", env=Environment(account="712310211354", region="eu-west-2"))
        pipeline.add_stage(testing_stage)

        production_stage = pipeline.add_stage(
            MentorMatchAppStage(self, "production", env=Environment(account="712310211354", region="eu-west-2"))
        )
        production_stage.add_pre(
            ManualApprovalStep('approval')
        )

class MentorMatchAppStage(Stage):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        service = TestStack(self, "MentorMatchStack")

class TestStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        self.fn = _lambda.Function(
            self,
            "LambdaFunction",
            runtime=_lambda.Runtime.PYTHON_3_9,
            code=_lambda.InlineCode("def handler(event, context): print 'hello'"),
            handler="index.handler",
        )

And here's where I create the stack:

app = cdk.App()

pipeline_stack = MentorMatchPipeline(app, 'MentorMatchPipeline',
    env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')),
)

This synthesized properly for me, and the stages worked the way they should.

So I wonder if something might be going wrong in your environment? The code you posted looks good to me and also worked for me

jonodrew commented 1 year ago

Thanks @HannesOberreiter ! I also note in the Python CDK library, the add_action method has no code under it:


class StageDeployment(
    metaclass=jsii.JSIIMeta,
    jsii_type="aws-cdk-lib.pipelines.StageDeployment",
):
    '''Deployment of a single ``Stage``.

    A ``Stage`` consists of one or more ``Stacks``, which will be
    deployed in dependency order.

    :exampleMetadata: fixture=_generated

    Example::

        # The code below shows an example of how to instantiate this type.
        # The values are placeholders you should change.
        import aws_cdk as cdk
        from aws_cdk import pipelines

        # stack: cdk.Stack
        # stage: cdk.Stage
        # step: pipelines.Step

        stage_deployment = pipelines.StageDeployment.from_stage(stage,
            post=[step],
            pre=[step],
            stack_steps=[pipelines.StackSteps(
                stack=stack,

                # the properties below are optional
                change_set=[step],
                post=[step],
                pre=[step]
            )],
            stage_name="stageName"
        )
    '''

    @jsii.member(jsii_name="fromStage")
    @builtins.classmethod
    def from_stage(
        cls,
        stage: _Stage_7df8511b,
        *,
        post: typing.Optional[typing.Sequence["Step"]] = None,
        pre: typing.Optional[typing.Sequence["Step"]] = None,
        stack_steps: typing.Optional[typing.Sequence[typing.Union[StackSteps, typing.Dict[builtins.str, typing.Any]]]] = None,
        stage_name: typing.Optional[builtins.str] = None,
    ) -> "StageDeployment":
        '''Create a new ``StageDeployment`` from a ``Stage``.

        Synthesizes the target stage, and deployes the stacks found inside
        in dependency order.

        :param stage: -
        :param post: Additional steps to run after all of the stacks in the stage. Default: - No additional steps
        :param pre: Additional steps to run before any of the stacks in the stage. Default: - No additional steps
        :param stack_steps: Instructions for additional steps that are run at the stack level. Default: - No additional instructions
        :param stage_name: Stage name to use in the pipeline. Default: - Use Stage's construct ID
        '''
        if __debug__:
            type_hints = typing.get_type_hints(_typecheckingstub__a37efd4695086f03c4f48796707d94ae7caf71cecf52f15940e3a454ddce14cf)
            check_type(argname="argument stage", value=stage, expected_type=type_hints["stage"])
        props = StageDeploymentProps(
            post=post, pre=pre, stack_steps=stack_steps, stage_name=stage_name
        )

        return typing.cast("StageDeployment", jsii.sinvoke(cls, "fromStage", [stage, props]))

    # rest of class removed for space

    def add_action(self):
        pass
jonodrew commented 1 year ago

@peterwoodworth thank you very much for all your work so far. I've updated my example. As you saw in the code, I don't actually call the add_action method anywhere because it doesn't work.

You'll note above that I've added the code from the StageDeployment class. The lack of code under add_action feels like it might be linked, but I know I might be wrong about that!

jonodrew commented 1 year ago

You're using a cdk.Stage. The examples we have that I found, and what the API reference shows is that the stage class with the addAction method is a pipelines.StageDeployment. Please let me know if there are any docs you found that use a cdk.Stage. Thanks

Following up on the documentation question - in the documentation for Step (https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.pipelines/Step.html#step), users are invited to call add_action on a variable called stage. There's no type on that variable, but I think it's fair to assume it should be of type Stage ?

peterwoodworth commented 1 year ago

Oh, I think I see where the confusion is now.

codepipeline.Pipeline.addStage() returns a codepipeline.IStage which does have this method. pipelines.CodePipeline.addStage() returns a StageDeployment - which isn't documented as having addAction().

Here's our documentation on how to add an arbitrary action in our pipelines.CodePipeline. This is the same documentation snippet you just linked, but it's our TypeScript docs which specifies that the Stage is actually a codepipeline.IStage, not a pipelines.StageDeployment

jonodrew commented 1 year ago

Thank you so much! I'm off to try to understand the difference between Pipeline and CodePipeline 😆

peterwoodworth commented 1 year ago

Seems like the way we generate the python docs makes this very easily confusing, so please let us know if you have any ideas on how to make this more clear in the docs

HannesOberreiter commented 1 year ago

Thank you, was really caught there in a limbo

jonodrew commented 1 year ago

I think further clarity about when one might use Pipeline over CodePipeline, or vice versa, might be helpful. I also don't love that we have aws_cdk.aws_codepipeline.Pipeline and aws_cdk.pipelines.CodePipeline 😅

peterwoodworth commented 1 year ago

Yeah it's not the clearest naming system. We'll see what we can do

jonodrew commented 1 year ago

Thanks @peterwoodworth . Happy for this to be closed