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

Cross stack reference caused "cyclic reference" error #3087

Closed
1 of 5 tasks
darren-dazh opened this issue Jun 26, 2019 · 10 comments · Fixed by #20149
Closed
1 of 5 tasks

Cross stack reference caused "cyclic reference" error #3087

darren-dazh opened this issue Jun 26, 2019 · 10 comments · Fixed by #20149
Labels
@aws-cdk/aws-codecommit Related to AWS CodeCommit @aws-cdk/aws-codepipeline Related to AWS CodePipeline bug This issue is a bug. cross-stack Related to cross-stack resource sharing effort/large Large work item – several weeks of effort in-progress This issue is being actively worked on. p1

Comments

@darren-dazh
Copy link

darren-dazh commented Jun 26, 2019

Note: for support questions, please first reference our documentation, then use Stackoverflow. This repository's issues are intended for feature requests and bug reports.

  • I'm submitting a ...

    • 🪲 bug report
    • 🚀 feature request
    • 📚 construct library gap
    • ☎️ security issue or vulnerability => Please see policy
    • ❓ support request => Please see note at the top of this template.
  • What is the current behavior?
    If the current behavior is a 🪲bug🪲: Please provide the steps to reproduce

I created 2 stacks - one for CodeCommit repository, and the other for CodePipeline which takes the previous one as source stage.

class CodeRepo(core.Stack):
......
    self.repo = codecommit.Repository(
            self,
            id='CodeCommitRepo',
            repository_name=repo_name
        )

class MyPipeline(core.Stack):
......
        def __init__(self, scope: core.Construct, id: str, code_repo: codecommit.IRepository) -> None:
            ......
            # create CodePipeline and its stages.
            pipeline = codepipeline.Pipeline(self, id='CodePipeline', pipeline_name='pipeline', restart_execution_on_update=False)

            source_stage = pipeline.add_stage(stage_name='Source')
            source_stage.add_action(
                codepipeline_actions.CodeCommitSourceAction(
                    action_name='action',
                    repository=code_repo,
                    branch='master',
                    output=codepipeline.Artifact(artifact_name='output'),
                    trigger=codepipeline_actions.CodeCommitTrigger.EVENTS
                )
            )

app = core.App() 
repo_stack = CodeRepo(app, 'repo_stack')
pipeline_stack = MyPipeline(app, 'pipeline_stack', repo_stack.repo)

app.synth()

Then I got following error:

File "./venv/lib/python3.6/site-packages/aws_cdk/core/__init__.py", line 2905, in synth
    return jsii.invoke(self, "synth", [])
  File "./venv/lib/python3.6/site-packages/jsii/_kernel/__init__.py", line 104, in wrapped
    return _recursize_dereference(kernel, fn(kernel, *args, **kwargs))
  File "./venv/lib/python3.6/site-packages/jsii/_kernel/__init__.py", line 258, in invoke
    args=_make_reference_for_native(self, args),
  File "./venv/lib/python3.6/site-packages/jsii/_kernel/providers/process.py", line 346, in invoke
    return self._process.send(request, InvokeResponse)
  File "./venv/lib/python3.6/site-packages/jsii/_kernel/providers/process.py", line 316, in send
    raise JSIIError(resp.error) from JavaScriptError(resp.stack)
jsii.errors.JSIIError: 'pipeline_stack' depends on 'repo_stack' (pipeline_stack/CodePipeline/Role/DefaultPolicy/Resource -> repo_stack/CodeCommitRepo/Resource.Arn). Adding this dependency (repo_stack/CodeCommitRepo/pipelinestackCodePipelineA06C8E1EEventRule/Resource -> pipeline_stack/CodePipeline/Resource.Ref) would create a cyclic reference.

An interesting observation is, if i put codecommit.Repository(...) in the pipeline_stack and refer the repo resource as usual, everything works without cyclic dependency.

  • What is the expected behavior (or behavior of feature suggested)?
    CDK is expected to handle the dependency issue, as described on aws doc for "Passing Resources from a Different Stack".

  • What is the motivation / use case for changing the behavior or adding this feature?

  • Please tell us about your environment:

    • CDK CLI Version: 0.36.0
    • Module Version: 0.36.0
    • OS: [ OSX Mojave ]
    • Language: [ Python ]
  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. associated pull-request, stackoverflow, gitter, etc)

@NGL321 NGL321 added bug This issue is a bug. @aws-cdk/aws-codecommit Related to AWS CodeCommit @aws-cdk/aws-codepipeline Related to AWS CodePipeline needs-reproduction This issue needs reproduction. language/python Related to Python bindings labels Jun 27, 2019
@skinny85 skinny85 self-assigned this Aug 12, 2019
@garnaat garnaat self-assigned this Aug 12, 2019
@skinny85 skinny85 added the p0 label Aug 12, 2019
@josjaf
Copy link

josjaf commented Aug 15, 2019

Is this an error with the codepipeline module?

@skinny85
Copy link
Contributor

It could be related to #3300

@SomayaB SomayaB added cross-stack Related to cross-stack resource sharing and removed needs-reproduction This issue needs reproduction. labels Oct 22, 2019
@skinny85 skinny85 added p1 and removed language/python Related to Python bindings p0 labels Oct 31, 2019
@skinny85 skinny85 added the effort/large Large work item – several weeks of effort label Sep 5, 2020
@hornetmadness
Copy link

I am also having this issue at of v1.98. Whats strange to me, is the IAM stack there is no direct dependency on any other stacks.
If I comment out the "codebuild.PipelineProject.role" it all works fine as its falling back to creating its own iam roles and not the prescribed one. There seems to be some black magic going on here that's causing this cyclic reference

    iam_name=f"alc-ops-IAM-{account_name}"
    iam_stack=IAM(
        scope=app,
        id=iam_name,
        description=iam_name,
        config=cfg.iam,
        env=cdk_env
    )
    pipebuild_stack = PipeBuild(
        scope=app,
        id=f"cdk-{cfg.account_name}-pipebuild-{region}",
        description=f"Created codepipeline and codebuild in {region}",
        config=cfg_ref.pipebuild,
        vpc_stack=vpc_stack,
        codecommit=codecommit_stack,
        iam_stack=iam_stack,
        env=cdk_env,
    )
class PipeBuild(core.Stack):
    def __init__(self,
        scope: core.Construct,
        id: str,
        config,
        vpc_stack,
        codecommit,
        iam_stack,
        **kwargs
    ) -> None:
        super().__init__(scope, id, **kwargs)

        sg=project.vpc.sg_name
        build = codebuild.PipelineProject(
            self, 
            f"build-{project.name}",
            project_name=f"{project.name}-Docker-Build",
            environment=image_switch[project.build_image](),
            environment_variables=project.envs,
            description=project.description,
            timeout=core.Duration.minutes(60),
            vpc=vpc_stack.vpc[project.vpc.vpc_name],
            security_groups=[ vpc_stack.sg[project.vpc.vpc_name][sg] ],
            role=iam_stack.roles[project.role]
        )

'alc-ops-IAM-devops' depends on 'cdk-devops-pipebuild-us-east-1' (alc-ops-IAM-devops -> cdk-devops-pipebuild-us-east-1/build-cdk-customers/Resource.Ref). Adding this dependency (cdk-devops-pipebuild-us-east-1 -> alc-ops-IAM-devops/pipebuild-cdk-customers-build/Resource.Arn) would create a cyclic reference.

@skinny85
Copy link
Contributor

Thanks for reporting @hornetmadness, but I'm having trouble replicating your setup. In this code:

class PipeBuild(core.Stack):
    def __init__(self, scope: core.Construct, id: str,
            config, vpc_stack, codecommit, iam_stack, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        sg=project.vpc.sg_name
        build = codebuild.PipelineProject(
            self,
            f"build-{project.name}",
            project_name=f"{project.name}-Docker-Build",
            environment=image_switch[project.build_image](),
            environment_variables=project.envs,
            description=project.description,
            timeout=core.Duration.minutes(60),
            vpc=vpc_stack.vpc[project.vpc.vpc_name],
            security_groups=[ vpc_stack.sg[project.vpc.vpc_name][sg] ],
            role=iam_stack.roles[project.role]
        )

What is project?

@hornetmadness
Copy link

Its just a bit of json

            "pipebuild": [
                {
                    "name": "cdk-customers",
                    "description": "Build docker container for cdk-customer",
                    "build_image": "STANDARD_5_0",
                    "envs":{},
                    "vpc": {
                        "vpc_name": "alc-ops",
                        "subnet_name": "applications",
                        "sg_name": "app_sg"
                    },
                    "timeout": 30,
                    "source_repo": "cdk-customers",
                    "watch_branch": "master",
                    "s3_bucket_name": "pipebuild-cdk-customers",
                    "role": "pipebuild-cdk-customers-build"
                }
            ],

@skinny85
Copy link
Contributor

I still need more information to diagnose this, like what's in IAM, and how are all of these Stacks wired in the entrypoint (otherwise the error message doesn't really make much sense to me).

Any chance you could push a minimal reproduction of this problem to some public GitHub repo @hornetmadness?

@skinny85
Copy link
Contributor

Ah, I probably get it. It's because you've put the Role of the Project in a different Stack. So, the Role needs to know the ARN of the Project (as it has a bunch of permissions that use that), and also the Project needs to know the ARN of the Role (to set it correctly on the Project). Hence, cycle.

This is enough to demonstrate it:

import * as codebuild from '@aws-cdk/aws-codebuild';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

class IamStack extends cdk.Stack {
    public readonly projectRole: iam.IRole;

    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        this.projectRole = new iam.Role(this, 'Role', {
            assumedBy: new iam.AnyPrincipal(),
        });
    }
}

interface ProjectStackProps extends cdk.StackProps {
    readonly projectRole: iam.IRole;
}

class ProjectStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props: ProjectStackProps) {
        super(scope, id, props);

        new codebuild.PipelineProject(this, 'Project', {
            role: props.projectRole,
        });
    }
}

const app = new cdk.App();
const iamStack = new IamStack(app, 'IamStack');
new ProjectStack(app, 'ProjectStack', {
    projectRole: iamStack.projectRole,
});
app.synth();

I'm afraid you'll have to structure your Stacks a little differently @hornetmadness.

@skinny85
Copy link
Contributor

Simplest solution - move the IAM Role of the CodeBuild Project to the same Stack as that Project.

@stevenlafl
Copy link

It appears any time you create a Role in one stack and reference it in another, it will break entirely. Setting roles in another stack for larger application is a good practice if you have the -same exact role- that would otherwise be created multiple times.

@mergify mergify bot closed this as completed in #20149 Aug 9, 2022
mergify bot pushed a commit that referenced this issue Aug 9, 2022
…n sources that use CloudWatch Events (#20149)

When using a newly-created, CDK-managed resource, such as an S3 Bucket or a CodeCommit Repository,
as the source of a CodePipeline, when it's in a different Stack than the pipeline
(but in the same environment as it),
and we use CloudWatch Events to trigger the pipeline from the source,
that would result in a cycle:

1. Because the Event Rule is created in the source Stack, that Stack needs the name of the pipeline to trigger from the Rule, and also the Role to use for the trigger, which is created in the target (in this case, the pipeline) Stack. So, the source Stack depends on the pipeline Stack.
2. The pipeline Stack needs the name of the source resource. So, the pipeline Stack depends on the source Stack.

The only way to break this cycle is to move the Event Rule to the target Stack.
This PR adds an API to the Events module to make it possible for event-creating constructs to make that choice,
and uses that capability in the CodePipeline `CodeCommitSourceAction` and `S3SourceAction`.

Fixes #3087
Fixes #8042
Fixes #10896

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@github-actions
Copy link

github-actions bot commented Aug 9, 2022

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

josephedward pushed a commit to josephedward/aws-cdk that referenced this issue Aug 30, 2022
…n sources that use CloudWatch Events (aws#20149)

When using a newly-created, CDK-managed resource, such as an S3 Bucket or a CodeCommit Repository,
as the source of a CodePipeline, when it's in a different Stack than the pipeline
(but in the same environment as it),
and we use CloudWatch Events to trigger the pipeline from the source,
that would result in a cycle:

1. Because the Event Rule is created in the source Stack, that Stack needs the name of the pipeline to trigger from the Rule, and also the Role to use for the trigger, which is created in the target (in this case, the pipeline) Stack. So, the source Stack depends on the pipeline Stack.
2. The pipeline Stack needs the name of the source resource. So, the pipeline Stack depends on the source Stack.

The only way to break this cycle is to move the Event Rule to the target Stack.
This PR adds an API to the Events module to make it possible for event-creating constructs to make that choice,
and uses that capability in the CodePipeline `CodeCommitSourceAction` and `S3SourceAction`.

Fixes aws#3087
Fixes aws#8042
Fixes aws#10896

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
mergify bot pushed a commit that referenced this issue Oct 3, 2023
…ws-lambda-python-alpha/test/lambda-handler-poetry (#27381)

Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.3 to 2.0.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/urllib3/urllib3/releases">urllib3's releases</a>.</em></p>
<blockquote>
<h2>2.0.6</h2>
<ul>
<li>Added the <code>Cookie</code> header to the list of headers to strip from requests when redirecting to a different host. As before, different headers can be set via <code>Retry.remove_headers_on_redirect</code>. (GHSA-v845-jxx5-vc9f)</li>
</ul>
<h2>2.0.5</h2>
<ul>
<li>Allowed pyOpenSSL third-party module without any deprecation warning. <a href="https://redirect.github.com/urllib3/urllib3/issues/3126">#3126</a></li>
<li>Fixed default <code>blocksize</code> of <code>HTTPConnection</code> classes to match high-level classes. Previously was 8KiB, now 16KiB. <a href="https://redirect.github.com/urllib3/urllib3/issues/3066%3E">#3066</a></li>
</ul>
<h2>2.0.4</h2>
<ul>
<li>Added support for union operators to <code>HTTPHeaderDict</code> (<a href="https://redirect.github.com/urllib3/urllib3/issues/2254">#2254</a>)</li>
<li>Added <code>BaseHTTPResponse</code> to <code>urllib3.__all__</code> (<a href="https://redirect.github.com/urllib3/urllib3/issues/3078">#3078</a>)</li>
<li>Fixed <code>urllib3.connection.HTTPConnection</code> to raise the <code>http.client.connect</code> audit event to have the same behavior as the standard library HTTP client (<a href="https://redirect.github.com/urllib3/urllib3/issues/2757">#2757</a>)</li>
<li>Relied on the standard library for checking hostnames in supported PyPy releases (<a href="https://redirect.github.com/urllib3/urllib3/issues/3087">#3087</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's changelog</a>.</em></p>
<blockquote>
<h1>2.0.6 (2023-10-02)</h1>
<ul>
<li>Added the <code>Cookie</code> header to the list of headers to strip from requests when redirecting to a different host. As before, different headers can be set via <code>Retry.remove_headers_on_redirect</code>.</li>
</ul>
<h1>2.0.5 (2023-09-20)</h1>
<ul>
<li>Allowed pyOpenSSL third-party module without any deprecation warning. (<code>[#3126](urllib3/urllib3#3126) &lt;https://github.com/urllib3/urllib3/issues/3126&gt;</code>__)</li>
<li>Fixed default <code>blocksize</code> of <code>HTTPConnection</code> classes to match high-level classes. Previously was 8KiB, now 16KiB. (<code>[#3066](urllib3/urllib3#3066) &lt;https://github.com/urllib3/urllib3/issues/3066&gt;</code>__)</li>
</ul>
<h1>2.0.4 (2023-07-19)</h1>
<ul>
<li>Added support for union operators to <code>HTTPHeaderDict</code> (<code>[#2254](urllib3/urllib3#2254) &lt;https://github.com/urllib3/urllib3/issues/2254&gt;</code>__)</li>
<li>Added <code>BaseHTTPResponse</code> to <code>urllib3.__all__</code> (<code>[#3078](urllib3/urllib3#3078) &lt;https://github.com/urllib3/urllib3/issues/3078&gt;</code>__)</li>
<li>Fixed <code>urllib3.connection.HTTPConnection</code> to raise the <code>http.client.connect</code> audit event to have the same behavior as the standard library HTTP client (<code>[#2757](urllib3/urllib3#2757) &lt;https://github.com/urllib3/urllib3/issues/2757&gt;</code>__)</li>
<li>Relied on the standard library for checking hostnames in supported PyPy releases (<code>[#3087](urllib3/urllib3#3087) &lt;https://github.com/urllib3/urllib3/issues/3087&gt;</code>__)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/urllib3/urllib3/commit/262e3e332209ee93ff70e2b13502c8f20c105ac8"><code>262e3e3</code></a> Release 2.0.6</li>
<li><a href="https://github.com/urllib3/urllib3/commit/644124ecd0b6e417c527191f866daa05a5a2056d"><code>644124e</code></a> Merge pull request from GHSA-v845-jxx5-vc9f</li>
<li><a href="https://github.com/urllib3/urllib3/commit/740380c59ca2a7c2dceca19e5dba99f6b7060e62"><code>740380c</code></a> Bump cryptography from 41.0.3 to 41.0.4 (<a href="https://redirect.github.com/urllib3/urllib3/issues/3131">#3131</a>)</li>
<li><a href="https://github.com/urllib3/urllib3/commit/d9f85a749488188c286cd50606d159874db94d5f"><code>d9f85a7</code></a> Release 2.0.5</li>
<li><a href="https://github.com/urllib3/urllib3/commit/d41f4122966f7f4f5f92001ad518e5d9dafcc886"><code>d41f412</code></a> Undeprecate pyOpenSSL module (<a href="https://redirect.github.com/urllib3/urllib3/issues/3127">#3127</a>)</li>
<li><a href="https://github.com/urllib3/urllib3/commit/b6c04cb3e62ef5a0e4947d037c12fb3ca79e024a"><code>b6c04cb</code></a> Fix a link to &quot;absolute URI&quot; definition (<a href="https://redirect.github.com/urllib3/urllib3/issues/3128">#3128</a>)</li>
<li><a href="https://github.com/urllib3/urllib3/commit/af7c78fa30f5a4e265911371d0c59b6baeddca0f"><code>af7c78f</code></a> refactor: change double conditional to one (<a href="https://redirect.github.com/urllib3/urllib3/issues/3118">#3118</a>)</li>
<li><a href="https://github.com/urllib3/urllib3/commit/34c13c8e68df6f89890ba08b9fc4fbf87ed21669"><code>34c13c8</code></a> Refer to current internet standards in docs on proxies (<a href="https://redirect.github.com/urllib3/urllib3/issues/3124">#3124</a>)</li>
<li><a href="https://github.com/urllib3/urllib3/commit/a3e94f218cd8297db73302eadae235f0c832a809"><code>a3e94f2</code></a> Fix a name of an attribute in docs (<a href="https://redirect.github.com/urllib3/urllib3/issues/3125">#3125</a>)</li>
<li><a href="https://github.com/urllib3/urllib3/commit/da69d4f4f95bc7ef9307fc8e0499c2121f1e4791"><code>da69d4f</code></a> Fix docs build (<a href="https://redirect.github.com/urllib3/urllib3/issues/3123">#3123</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/urllib3/urllib3/compare/2.0.3...2.0.6">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=2.0.3&new-version=2.0.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/aws/aws-cdk/network/alerts).

</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-codecommit Related to AWS CodeCommit @aws-cdk/aws-codepipeline Related to AWS CodePipeline bug This issue is a bug. cross-stack Related to cross-stack resource sharing effort/large Large work item – several weeks of effort in-progress This issue is being actively worked on. p1
Projects
None yet
8 participants