2021-07-27 13:50:24 +00:00
|
|
|
# Copyright 2016-2021, Pulumi Corporation.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
2021-07-28 13:49:07 +00:00
|
|
|
from typing import Optional, TypeVar, Awaitable, List, Any
|
|
|
|
import asyncio
|
[sdk/python] Adds a default exception when dependency cycles are created (#14597)
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Currently, when we detect that we've created a cycle in the dependency
graph, we early-exit. This works well enough for simple cycles, but when
early exiting is not sufficient (as when providing a resource output as
an argument to another resource's inputs), we will still fail to resolve
the full dependency graph. This PR introduces an exception-by-default,
as any cycles represent an unsafe/invalid dependency graph and should be
resolved manually. We also provide an escape hatch to fall back to
current behavior, in case users would prefer to retain the ability to
create unsafe dependency graphs (potentially introducing infinite hangs
when resolving those graphs).
Fixes https://github.com/pulumi/pulumi/issues/13551
## Checklist
- [x] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-21 16:26:02 +00:00
|
|
|
import os
|
2021-07-28 13:49:07 +00:00
|
|
|
import pytest
|
2021-07-27 13:50:24 +00:00
|
|
|
import unittest
|
|
|
|
|
|
|
|
from pulumi.resource import DependencyProviderResource
|
2021-07-28 13:49:07 +00:00
|
|
|
from pulumi.runtime import settings, mocks
|
|
|
|
from pulumi.runtime.proto import resource_pb2
|
2022-11-10 21:14:52 +00:00
|
|
|
from pulumi import ResourceOptions
|
[sdk/python] Adds a default exception when dependency cycles are created (#14597)
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Currently, when we detect that we've created a cycle in the dependency
graph, we early-exit. This works well enough for simple cycles, but when
early exiting is not sufficient (as when providing a resource output as
an argument to another resource's inputs), we will still fail to resolve
the full dependency graph. This PR introduces an exception-by-default,
as any cycles represent an unsafe/invalid dependency graph and should be
resolved manually. We also provide an escape hatch to fall back to
current behavior, in case users would prefer to retain the ability to
create unsafe dependency graphs (potentially introducing infinite hangs
when resolving those graphs).
Fixes https://github.com/pulumi/pulumi/issues/13551
## Checklist
- [x] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-21 16:26:02 +00:00
|
|
|
from pulumi.runtime.rpc import ERROR_ON_DEPENDENCY_CYCLES_VAR
|
2021-07-28 13:49:07 +00:00
|
|
|
import pulumi
|
|
|
|
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
T = TypeVar("T")
|
2021-07-27 13:50:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
class DependencyProviderResourceTests(unittest.TestCase):
|
|
|
|
def test_get_package(self):
|
2024-04-23 08:29:58 +00:00
|
|
|
res = DependencyProviderResource(
|
|
|
|
"urn:pulumi:stack::project::pulumi:providers:aws::default_4_13_0"
|
|
|
|
)
|
2021-07-27 13:50:24 +00:00
|
|
|
self.assertEqual("aws", res.package)
|
2021-07-28 13:49:07 +00:00
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
|
[sdk/python] Adds a default exception when dependency cycles are created (#14597)
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Currently, when we detect that we've created a cycle in the dependency
graph, we early-exit. This works well enough for simple cycles, but when
early exiting is not sufficient (as when providing a resource output as
an argument to another resource's inputs), we will still fail to resolve
the full dependency graph. This PR introduces an exception-by-default,
as any cycles represent an unsafe/invalid dependency graph and should be
resolved manually. We also provide an escape hatch to fall back to
current behavior, in case users would prefer to retain the ability to
create unsafe dependency graphs (potentially introducing infinite hangs
when resolving those graphs).
Fixes https://github.com/pulumi/pulumi/issues/13551
## Checklist
- [x] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-21 16:26:02 +00:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def clean_up_env_vars():
|
|
|
|
try:
|
|
|
|
del os.environ[ERROR_ON_DEPENDENCY_CYCLES_VAR]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2021-07-28 13:49:07 +00:00
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
|
2021-07-28 13:49:07 +00:00
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_depends_on_accepts_outputs(dep_tracker):
|
2024-04-23 08:29:58 +00:00
|
|
|
dep1 = MockResource(name="dep1")
|
|
|
|
dep2 = MockResource(name="dep2")
|
2021-07-28 13:49:07 +00:00
|
|
|
out = output_depending_on_resource(dep1, isKnown=True).apply(lambda _: dep2)
|
2024-04-23 08:29:58 +00:00
|
|
|
res = MockResource(name="res", opts=pulumi.ResourceOptions(depends_on=[out]))
|
2021-07-28 13:49:07 +00:00
|
|
|
|
|
|
|
def check(urns):
|
|
|
|
(dep1_urn, dep2_urn, res_urn) = urns
|
|
|
|
res_deps = dep_tracker.dependencies[res_urn]
|
2024-04-23 08:29:58 +00:00
|
|
|
assert (
|
|
|
|
dep1_urn in res_deps
|
|
|
|
), "Failed to propagate indirect dependencies via depends_on"
|
|
|
|
assert (
|
|
|
|
dep2_urn in res_deps
|
|
|
|
), "Failed to propagate direct dependencies via depends_on"
|
2021-07-28 13:49:07 +00:00
|
|
|
|
|
|
|
return pulumi.Output.all(dep1.urn, dep2.urn, res.urn).apply(check)
|
|
|
|
|
|
|
|
|
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_depends_on_outputs_works_in_presence_of_unknowns(dep_tracker_preview):
|
2024-04-23 08:29:58 +00:00
|
|
|
dep1 = MockResource(name="dep1")
|
|
|
|
dep2 = MockResource(name="dep2")
|
|
|
|
dep3 = MockResource(name="dep3")
|
2021-07-28 13:49:07 +00:00
|
|
|
known = output_depending_on_resource(dep1, isKnown=True).apply(lambda _: dep2)
|
|
|
|
unknown = output_depending_on_resource(dep2, isKnown=False).apply(lambda _: dep2)
|
2024-04-23 08:29:58 +00:00
|
|
|
res = MockResource(
|
|
|
|
name="res", opts=pulumi.ResourceOptions(depends_on=[known, unknown])
|
|
|
|
)
|
2021-07-28 13:49:07 +00:00
|
|
|
|
|
|
|
def check(urns):
|
|
|
|
(dep1_urn, res_urn) = urns
|
|
|
|
assert dep1_urn in dep_tracker_preview.dependencies[res_urn]
|
|
|
|
|
|
|
|
return pulumi.Output.all(dep1.urn, res.urn).apply(check)
|
|
|
|
|
|
|
|
|
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_depends_on_respects_top_level_implicit_dependencies(dep_tracker):
|
2024-04-23 08:29:58 +00:00
|
|
|
dep1 = MockResource(name="dep1")
|
|
|
|
dep2 = MockResource(name="dep2")
|
2021-07-28 13:49:07 +00:00
|
|
|
out = output_depending_on_resource(dep1, isKnown=True).apply(lambda _: [dep2])
|
2024-04-23 08:29:58 +00:00
|
|
|
res = MockResource(name="res", opts=pulumi.ResourceOptions(depends_on=out))
|
2021-07-28 13:49:07 +00:00
|
|
|
|
|
|
|
def check(urns):
|
|
|
|
(dep1_urn, dep2_urn, res_urn) = urns
|
|
|
|
assert set(dep_tracker.dependencies[res_urn]) == set([dep1_urn, dep2_urn])
|
|
|
|
|
|
|
|
return pulumi.Output.all(dep1.urn, dep2.urn, res.urn).apply(check)
|
|
|
|
|
|
|
|
|
|
|
|
def promise(x: T) -> Awaitable[T]:
|
|
|
|
fut: asyncio.Future[T] = asyncio.Future()
|
|
|
|
fut.set_result(x)
|
|
|
|
return fut
|
|
|
|
|
|
|
|
|
|
|
|
def out(x: T) -> pulumi.Output[T]:
|
|
|
|
return pulumi.Output.from_input(x)
|
|
|
|
|
|
|
|
|
|
|
|
def depends_on_variations(dep: pulumi.Resource) -> List[pulumi.ResourceOptions]:
|
|
|
|
return [
|
|
|
|
pulumi.ResourceOptions(depends_on=None),
|
|
|
|
pulumi.ResourceOptions(depends_on=dep),
|
|
|
|
pulumi.ResourceOptions(depends_on=[dep]),
|
|
|
|
pulumi.ResourceOptions(depends_on=promise(dep)),
|
|
|
|
pulumi.ResourceOptions(depends_on=out(dep)),
|
|
|
|
pulumi.ResourceOptions(depends_on=promise([dep])),
|
|
|
|
pulumi.ResourceOptions(depends_on=out([dep])),
|
|
|
|
pulumi.ResourceOptions(depends_on=promise([promise(dep)])),
|
|
|
|
pulumi.ResourceOptions(depends_on=promise([out(dep)])),
|
|
|
|
pulumi.ResourceOptions(depends_on=out([promise(dep)])),
|
|
|
|
pulumi.ResourceOptions(depends_on=out([out(dep)])),
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_depends_on_typing_variations(dep_tracker) -> None:
|
2024-04-23 08:29:58 +00:00
|
|
|
dep: pulumi.Resource = MockResource(name="dep1")
|
2021-07-28 13:49:07 +00:00
|
|
|
|
|
|
|
def check(i, urns):
|
|
|
|
(dep_urn, res_urn) = urns
|
|
|
|
|
|
|
|
if i == 0:
|
|
|
|
assert dep_tracker.dependencies[res_urn] == set([])
|
|
|
|
else:
|
|
|
|
assert dep_urn in dep_tracker.dependencies[res_urn]
|
|
|
|
|
|
|
|
def check_opts(i, name, opts):
|
|
|
|
res = MockResource(name, opts)
|
|
|
|
return pulumi.Output.all(dep.urn, res.urn).apply(lambda urns: check(i, urns))
|
|
|
|
|
2024-04-30 12:54:13 +00:00
|
|
|
pulumi.Output.all(
|
2024-04-23 08:29:58 +00:00
|
|
|
[
|
|
|
|
check_opts(i, f"res{i}", opts)
|
|
|
|
for i, opts in enumerate(depends_on_variations(dep))
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
2021-07-28 13:49:07 +00:00
|
|
|
|
2024-03-25 18:51:21 +00:00
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_depends_on_typechecks_sync():
|
|
|
|
# https://github.com/pulumi/pulumi/issues/13917
|
|
|
|
try:
|
2024-04-23 08:29:58 +00:00
|
|
|
res = MockResource(
|
|
|
|
name="res", opts=pulumi.ResourceOptions(depends_on=["hello"])
|
|
|
|
)
|
2024-03-25 18:51:21 +00:00
|
|
|
assert False, "should of failed"
|
|
|
|
except TypeError as e:
|
2024-04-23 08:29:58 +00:00
|
|
|
assert (
|
|
|
|
str(e) == "'depends_on' was passed a value hello that was not a Resource."
|
|
|
|
)
|
2024-03-25 18:51:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_depends_on_typechecks_async():
|
2024-04-23 08:29:58 +00:00
|
|
|
if not hasattr(asyncio, "to_thread"):
|
2024-03-25 18:51:21 +00:00
|
|
|
# Old versions of Python don't have asyncio.to_thread, just skip the test in that case.
|
|
|
|
return
|
|
|
|
|
|
|
|
@pulumi.runtime.test
|
|
|
|
def test():
|
|
|
|
# https://github.com/pulumi/pulumi/issues/13917
|
|
|
|
dep = asyncio.to_thread(lambda: "goodbye")
|
2024-04-23 08:29:58 +00:00
|
|
|
res = MockResource(name="res", opts=pulumi.ResourceOptions(depends_on=[dep]))
|
2024-03-25 18:51:21 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
test()
|
|
|
|
assert False, "should of failed"
|
|
|
|
except TypeError as e:
|
2024-04-23 08:29:58 +00:00
|
|
|
assert (
|
|
|
|
str(e) == "'depends_on' was passed a value goodbye that was not a Resource."
|
|
|
|
)
|
|
|
|
|
2021-07-28 13:49:07 +00:00
|
|
|
|
2023-04-03 22:28:19 +00:00
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_component_resource_propagates_provider() -> None:
|
|
|
|
mocks.set_mocks(MinimalMocks())
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
provider = pulumi.ProviderResource("test", "prov", {})
|
2023-04-03 22:28:19 +00:00
|
|
|
component = pulumi.ComponentResource(
|
|
|
|
"custom:foo:Component",
|
|
|
|
"comp",
|
|
|
|
opts=pulumi.ResourceOptions(provider=provider),
|
|
|
|
)
|
|
|
|
custom = pulumi.CustomResource(
|
|
|
|
"test:index:Resource",
|
|
|
|
"res",
|
|
|
|
opts=pulumi.ResourceOptions(parent=component),
|
|
|
|
)
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
assert (
|
|
|
|
provider == custom._provider
|
|
|
|
), "Failed to propagate provider to child resource"
|
2023-04-03 22:28:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_component_resource_propagates_providers_list() -> None:
|
|
|
|
mocks.set_mocks(MinimalMocks())
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
provider = pulumi.ProviderResource("test", "prov", {})
|
2023-04-03 22:28:19 +00:00
|
|
|
component = pulumi.ComponentResource(
|
|
|
|
"custom:foo:Component",
|
|
|
|
"comp",
|
|
|
|
opts=pulumi.ResourceOptions(providers=[provider]),
|
|
|
|
)
|
|
|
|
custom = pulumi.CustomResource(
|
|
|
|
"test:index:Resource",
|
|
|
|
"res",
|
|
|
|
opts=pulumi.ResourceOptions(parent=component),
|
|
|
|
)
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
assert (
|
|
|
|
provider == custom._provider
|
|
|
|
), "Failed to propagate provider to child resource"
|
2023-04-03 22:28:19 +00:00
|
|
|
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
def output_depending_on_resource(
|
|
|
|
r: pulumi.Resource, isKnown: bool
|
|
|
|
) -> pulumi.Output[None]:
|
2021-07-28 13:49:07 +00:00
|
|
|
"""Returns an output that depends on the given resource."""
|
|
|
|
o = pulumi.Output.from_input(None)
|
|
|
|
is_known_fut: asyncio.Future[bool] = asyncio.Future()
|
|
|
|
is_known_fut.set_result(isKnown)
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
return pulumi.Output(resources=set([r]), is_known=is_known_fut, future=o.future())
|
2021-07-28 13:49:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def dep_tracker():
|
|
|
|
for dt in build_dep_tracker():
|
|
|
|
yield dt
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def dep_tracker_preview():
|
|
|
|
for dt in build_dep_tracker(preview=True):
|
|
|
|
yield dt
|
|
|
|
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
def build_dep_tracker(preview: bool = False):
|
2021-07-28 13:49:07 +00:00
|
|
|
old_settings = settings.SETTINGS
|
|
|
|
mm = MinimalMocks()
|
|
|
|
mocks.set_mocks(mm, preview=preview)
|
|
|
|
dt = DependencyTrackingMonitorWrapper(settings.SETTINGS.monitor)
|
|
|
|
settings.SETTINGS.monitor = dt
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield dt
|
|
|
|
finally:
|
|
|
|
settings.configure(old_settings)
|
|
|
|
|
|
|
|
|
|
|
|
class MinimalMocks(pulumi.runtime.Mocks):
|
|
|
|
|
|
|
|
def new_resource(self, args: pulumi.runtime.MockResourceArgs):
|
2024-04-23 08:29:58 +00:00
|
|
|
return [args.name + "_id", args.inputs]
|
2021-07-28 13:49:07 +00:00
|
|
|
|
|
|
|
def call(self, args: pulumi.runtime.MockCallArgs):
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
class DependencyTrackingMonitorWrapper:
|
|
|
|
|
|
|
|
def __init__(self, inner):
|
|
|
|
self.inner = inner
|
|
|
|
self.dependencies = {}
|
|
|
|
|
|
|
|
def RegisterResource(self, req: Any):
|
|
|
|
resp = self.inner.RegisterResource(req)
|
2024-04-23 08:29:58 +00:00
|
|
|
self.dependencies[resp.urn] = self.dependencies.get(resp.urn, set()) | set(
|
|
|
|
req.dependencies
|
|
|
|
)
|
2021-07-28 13:49:07 +00:00
|
|
|
return resp
|
|
|
|
|
|
|
|
def __getattr__(self, attr):
|
|
|
|
return getattr(self.inner, attr)
|
|
|
|
|
|
|
|
|
|
|
|
class MockResource(pulumi.CustomResource):
|
|
|
|
def __init__(self, name: str, opts: Optional[pulumi.ResourceOptions] = None):
|
2022-11-10 21:14:52 +00:00
|
|
|
super().__init__("python:test:MockResource", name, {}, opts)
|
|
|
|
|
|
|
|
|
|
|
|
class MergeResourceOptions(unittest.TestCase):
|
|
|
|
def test_parent(self):
|
|
|
|
opts1 = ResourceOptions()
|
|
|
|
assert opts1.protect is None
|
|
|
|
opts2 = ResourceOptions.merge(opts1, ResourceOptions(protect=True))
|
|
|
|
assert opts2.protect is True
|
2023-02-27 20:00:53 +00:00
|
|
|
opts3 = ResourceOptions.merge(opts2, ResourceOptions())
|
2022-11-10 21:14:52 +00:00
|
|
|
assert opts3.protect is True
|
[sdk/python] Fix hang due to component children cycles
When a resource depends on a local component resource, rather than setting the component resource itself as a dependency, each of the component's descendants is added as a dependency. This can lead to hangs when cycles are introduced.
For example, consider the following parent/child hierarchy, where `ComponentA` is the parent of `ComponentB` and `ComponentB` is the parent of `CustomC`:
ComponentA
|
ComponentB
|
CustomC
If `ComponentB` specifies it has a dependency on `ComponentA`, the following takes place as part determining the full set of transitive dependencies:
1. `ComponentA` is a component resource so it isn't added as a dependency, its children are.
2. `ComponentA` has one child: `ComponentB`
3. `ComponentB` is a component resource so it isn't added as a dependency, its children are.
4. `ComponentB` has one child: `CustomC`, a custom resource.
5. Since `CustomC` is a custom resource, it is added to the set of dependencies.
6. We try to await its URN, but we'll never get it because `RegisterResource` hasn't yet been called for it. And we hang waiting.
To address this, skip looking at a component's children if it is the component from which the dependency is being added.
In the example, with the fix, at step 3 the dependency expansion will stop: we won't look at `ComponentB`'s children because we're adding the dependency from `ComponentB`.
PR feedback
2023-03-21 00:30:40 +00:00
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
|
[sdk/python] Fix hang due to component children cycles
When a resource depends on a local component resource, rather than setting the component resource itself as a dependency, each of the component's descendants is added as a dependency. This can lead to hangs when cycles are introduced.
For example, consider the following parent/child hierarchy, where `ComponentA` is the parent of `ComponentB` and `ComponentB` is the parent of `CustomC`:
ComponentA
|
ComponentB
|
CustomC
If `ComponentB` specifies it has a dependency on `ComponentA`, the following takes place as part determining the full set of transitive dependencies:
1. `ComponentA` is a component resource so it isn't added as a dependency, its children are.
2. `ComponentA` has one child: `ComponentB`
3. `ComponentB` is a component resource so it isn't added as a dependency, its children are.
4. `ComponentB` has one child: `CustomC`, a custom resource.
5. Since `CustomC` is a custom resource, it is added to the set of dependencies.
6. We try to await its URN, but we'll never get it because `RegisterResource` hasn't yet been called for it. And we hang waiting.
To address this, skip looking at a component's children if it is the component from which the dependency is being added.
In the example, with the fix, at step 3 the dependency expansion will stop: we won't look at `ComponentB`'s children because we're adding the dependency from `ComponentB`.
PR feedback
2023-03-21 00:30:40 +00:00
|
|
|
# Regression test for https://github.com/pulumi/pulumi/issues/12032
|
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_parent_and_depends_on_are_the_same_12032():
|
[sdk/python] Adds a default exception when dependency cycles are created (#14597)
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Currently, when we detect that we've created a cycle in the dependency
graph, we early-exit. This works well enough for simple cycles, but when
early exiting is not sufficient (as when providing a resource output as
an argument to another resource's inputs), we will still fail to resolve
the full dependency graph. This PR introduces an exception-by-default,
as any cycles represent an unsafe/invalid dependency graph and should be
resolved manually. We also provide an escape hatch to fall back to
current behavior, in case users would prefer to retain the ability to
create unsafe dependency graphs (potentially introducing infinite hangs
when resolving those graphs).
Fixes https://github.com/pulumi/pulumi/issues/13551
## Checklist
- [x] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-21 16:26:02 +00:00
|
|
|
os.environ[ERROR_ON_DEPENDENCY_CYCLES_VAR] = "false"
|
[sdk/python] Fix hang due to component children cycles
When a resource depends on a local component resource, rather than setting the component resource itself as a dependency, each of the component's descendants is added as a dependency. This can lead to hangs when cycles are introduced.
For example, consider the following parent/child hierarchy, where `ComponentA` is the parent of `ComponentB` and `ComponentB` is the parent of `CustomC`:
ComponentA
|
ComponentB
|
CustomC
If `ComponentB` specifies it has a dependency on `ComponentA`, the following takes place as part determining the full set of transitive dependencies:
1. `ComponentA` is a component resource so it isn't added as a dependency, its children are.
2. `ComponentA` has one child: `ComponentB`
3. `ComponentB` is a component resource so it isn't added as a dependency, its children are.
4. `ComponentB` has one child: `CustomC`, a custom resource.
5. Since `CustomC` is a custom resource, it is added to the set of dependencies.
6. We try to await its URN, but we'll never get it because `RegisterResource` hasn't yet been called for it. And we hang waiting.
To address this, skip looking at a component's children if it is the component from which the dependency is being added.
In the example, with the fix, at step 3 the dependency expansion will stop: we won't look at `ComponentB`'s children because we're adding the dependency from `ComponentB`.
PR feedback
2023-03-21 00:30:40 +00:00
|
|
|
mocks.set_mocks(MinimalMocks())
|
|
|
|
|
|
|
|
parent = pulumi.ComponentResource("pkg:index:first", "first")
|
|
|
|
child = pulumi.ComponentResource(
|
|
|
|
"pkg:index:second",
|
|
|
|
"second",
|
|
|
|
opts=pulumi.ResourceOptions(parent=parent, depends_on=[parent]),
|
|
|
|
)
|
|
|
|
|
|
|
|
# This would freeze before the fix.
|
|
|
|
pulumi.CustomResource(
|
|
|
|
"foo:bar:baz",
|
|
|
|
"myresource",
|
|
|
|
opts=pulumi.ResourceOptions(parent=child),
|
|
|
|
)
|
2023-05-10 13:06:50 +00:00
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
|
2023-05-10 13:06:50 +00:00
|
|
|
# Regression test for https://github.com/pulumi/pulumi/issues/12736
|
|
|
|
@pulumi.runtime.test
|
|
|
|
def test_complex_parent_child_dependencies():
|
[sdk/python] Adds a default exception when dependency cycles are created (#14597)
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Currently, when we detect that we've created a cycle in the dependency
graph, we early-exit. This works well enough for simple cycles, but when
early exiting is not sufficient (as when providing a resource output as
an argument to another resource's inputs), we will still fail to resolve
the full dependency graph. This PR introduces an exception-by-default,
as any cycles represent an unsafe/invalid dependency graph and should be
resolved manually. We also provide an escape hatch to fall back to
current behavior, in case users would prefer to retain the ability to
create unsafe dependency graphs (potentially introducing infinite hangs
when resolving those graphs).
Fixes https://github.com/pulumi/pulumi/issues/13551
## Checklist
- [x] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-21 16:26:02 +00:00
|
|
|
os.environ[ERROR_ON_DEPENDENCY_CYCLES_VAR] = "false"
|
2023-05-10 13:06:50 +00:00
|
|
|
mocks.set_mocks(MinimalMocks())
|
|
|
|
|
|
|
|
class A(pulumi.ComponentResource):
|
|
|
|
def __init__(self, name: str, opts=None):
|
|
|
|
super().__init__("my:modules:A", name, {}, opts)
|
|
|
|
self.b = B("a-b", opts=ResourceOptions(parent=self))
|
|
|
|
self.c = C("a-c", opts=ResourceOptions(parent=self.b, depends_on=[self.b]))
|
|
|
|
|
|
|
|
class B(pulumi.ComponentResource):
|
|
|
|
def __init__(self, name: str, opts=None):
|
|
|
|
super().__init__("my:modules:B", name, {}, opts)
|
2024-04-23 08:29:58 +00:00
|
|
|
pulumi.CustomResource(
|
|
|
|
"my:module:Child", "b-child", opts=ResourceOptions(parent=self)
|
|
|
|
)
|
2023-05-10 13:06:50 +00:00
|
|
|
|
|
|
|
class C(pulumi.ComponentResource):
|
|
|
|
def __init__(self, name: str, opts=None):
|
|
|
|
super().__init__("my:modules:C", name, {}, opts)
|
2024-04-23 08:29:58 +00:00
|
|
|
pulumi.CustomResource(
|
|
|
|
"my:module:Child", "c-child", opts=ResourceOptions(parent=self)
|
|
|
|
)
|
2023-05-10 13:06:50 +00:00
|
|
|
|
|
|
|
class D(pulumi.ComponentResource):
|
|
|
|
def __init__(self, name: str, opts=None):
|
|
|
|
super().__init__("my:modules:D", name, {}, opts)
|
2024-04-23 08:29:58 +00:00
|
|
|
pulumi.CustomResource(
|
|
|
|
"my:module:Child", "d-child", opts=ResourceOptions(parent=self)
|
|
|
|
)
|
2023-05-10 13:06:50 +00:00
|
|
|
|
|
|
|
a = A("a")
|
|
|
|
|
2024-04-23 08:29:58 +00:00
|
|
|
D("d", opts=ResourceOptions(parent=a.b, depends_on=[a.b]))
|
2023-09-25 14:54:28 +00:00
|
|
|
|
[sdk/python] Adds a default exception when dependency cycles are created (#14597)
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Currently, when we detect that we've created a cycle in the dependency
graph, we early-exit. This works well enough for simple cycles, but when
early exiting is not sufficient (as when providing a resource output as
an argument to another resource's inputs), we will still fail to resolve
the full dependency graph. This PR introduces an exception-by-default,
as any cycles represent an unsafe/invalid dependency graph and should be
resolved manually. We also provide an escape hatch to fall back to
current behavior, in case users would prefer to retain the ability to
create unsafe dependency graphs (potentially introducing infinite hangs
when resolving those graphs).
Fixes https://github.com/pulumi/pulumi/issues/13551
## Checklist
- [x] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-21 16:26:02 +00:00
|
|
|
|
2023-09-25 14:54:28 +00:00
|
|
|
# Regression test for https://github.com/pulumi/pulumi/issues/13997
|
|
|
|
def test_bad_component_super_call():
|
|
|
|
class C(pulumi.ComponentResource):
|
|
|
|
def __init__(self, name: str, arg: int, opts=None):
|
|
|
|
super().__init__("my:module:C", name, arg, opts)
|
|
|
|
|
2024-03-25 18:51:21 +00:00
|
|
|
@pulumi.runtime.test
|
|
|
|
def test():
|
2023-09-25 14:54:28 +00:00
|
|
|
C("test", 4, None)
|
2024-03-25 18:51:21 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
test()
|
|
|
|
assert False, "should of failed"
|
|
|
|
except TypeError as e:
|
|
|
|
assert str(e) == "Expected resource properties to be a mapping"
|