Add python aliases support. (#2974)
This commit is contained in:
parent
c6916051f0
commit
237f8d2222
|
@ -9,6 +9,11 @@ CHANGELOG
|
|||
- Fix unexpected provider replacements when upgrading from older CLIs and older providers
|
||||
[pulumi/pulumi-kubernetes#645](https://github.com/pulumi/pulumi-kubernetes/issues/645)
|
||||
|
||||
- Add *Python* support for renaming resources via the `aliases` resource option. Adding aliases
|
||||
allows new resources to match resources from previous deployments which used different names,
|
||||
maintaining the identity of the resource and avoiding replacements or re-creation of the resource.
|
||||
This was previously added to the *JavaScript* sdk in 0.17.15.
|
||||
|
||||
## 0.17.25 (2019-07-19)
|
||||
|
||||
- Support for Dynamic Providers in Python [#2900](https://github.com/pulumi/pulumi/pull/2900)
|
||||
|
|
|
@ -53,12 +53,15 @@ from .metadata import (
|
|||
)
|
||||
|
||||
from .resource import (
|
||||
Alias,
|
||||
Resource,
|
||||
CustomResource,
|
||||
ComponentResource,
|
||||
ProviderResource,
|
||||
ResourceOptions,
|
||||
create_urn,
|
||||
export,
|
||||
ROOT_STACK_RESOURCE,
|
||||
)
|
||||
|
||||
from .output import (
|
||||
|
|
|
@ -15,12 +15,18 @@
|
|||
"""The Resource module, containing all resource-related definitions."""
|
||||
from typing import Optional, List, Any, Mapping, Union, TYPE_CHECKING
|
||||
|
||||
import copy
|
||||
|
||||
from .runtime import known_types
|
||||
from .runtime.resource import register_resource, register_resource_outputs, read_resource
|
||||
from .runtime.settings import get_root_resource
|
||||
|
||||
from .metadata import get_project, get_stack
|
||||
|
||||
from .output import Output
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .output import Output, Inputs
|
||||
from .output import Input, Inputs
|
||||
|
||||
|
||||
class CustomTimeouts:
|
||||
|
@ -49,6 +55,152 @@ class CustomTimeouts:
|
|||
self.delete = delete
|
||||
|
||||
|
||||
def inherited_child_alias(
|
||||
child_name: str,
|
||||
parent_name: str,
|
||||
parent_alias: 'Input[str]',
|
||||
child_type: str) -> 'Output[str]':
|
||||
"""
|
||||
inherited_child_alias computes the alias that should be applied to a child based on an alias
|
||||
applied to it's parent. This may involve changing the name of the resource in cases where the
|
||||
resource has a named derived from the name of the parent, and the parent name changed.
|
||||
"""
|
||||
|
||||
# If the child name has the parent name as a prefix, then we make the assumption that it was
|
||||
# constructed from the convention of using `{name}-details` as the name of the child resource. To
|
||||
# ensure this is aliased correctly, we must then also replace the parent aliases name in the prefix of
|
||||
# the child resource name.
|
||||
#
|
||||
# For example:
|
||||
# * name: "newapp-function"
|
||||
# * opts.parent.__name: "newapp"
|
||||
# * parentAlias: "urn:pulumi:stackname::projectname::awsx:ec2:Vpc::app"
|
||||
# * parentAliasName: "app"
|
||||
# * aliasName: "app-function"
|
||||
# * childAlias: "urn:pulumi:stackname::projectname::aws:s3/bucket:Bucket::app-function"
|
||||
alias_name = Output.from_input(child_name)
|
||||
if child_name.startswith(parent_name):
|
||||
alias_name = Output.from_input(parent_alias).apply(
|
||||
lambda u: u[u.rfind("::") + 2:] + child_name[len(parent_name):])
|
||||
|
||||
return create_urn(alias_name, child_type, parent_alias)
|
||||
|
||||
|
||||
ROOT_STACK_RESOURCE = None
|
||||
"""
|
||||
Constant to represent the 'root stack' resource for a Pulumi application. The purpose of this is
|
||||
solely to make it easy to write an [Alias] like so:
|
||||
|
||||
`aliases=[Alias(parent=pulumi.ROOT_STACK_RESOURCE)]`.
|
||||
|
||||
This indicates that the prior name for a resource was created based on it being parented directly by
|
||||
the stack itself and no other resources. Note: this is equivalent to:
|
||||
|
||||
`aliases=[Alias(parent=None)]`
|
||||
|
||||
However, the former form is preferable as it is more self-descriptive, while the latter may look a
|
||||
bit confusing and may incorrectly look like something that could be removed without changing
|
||||
semantics.
|
||||
"""
|
||||
|
||||
class Alias:
|
||||
"""
|
||||
Alias is a partial description of prior named used for a resource. It can be processed in the
|
||||
context of a resource creation to determine what the full aliased URN would be.
|
||||
|
||||
Note there is a semantic difference between attributes being given the `None` value and
|
||||
attributes not being given at all. Specifically, there is a difference between:
|
||||
|
||||
```ts
|
||||
Alias(name="foo", parent=None) # and
|
||||
Alias(name="foo")
|
||||
```
|
||||
|
||||
So the first alias means "the original urn had no parent" while the second alias means "use the
|
||||
current parent".
|
||||
|
||||
Note: to indicate that a resource was previously parented by the root stack, it is recommended
|
||||
that you use:
|
||||
|
||||
`aliases=[Alias(parent=pulumi.ROOT_STACK_RESOURCE)]`
|
||||
|
||||
This form is self-descriptive and makes the intent clearer than using:
|
||||
|
||||
`aliases=[Alias(parent=None)]`
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
"""
|
||||
The previous name of the resource. If not provided, the current name of the resource is used.
|
||||
"""
|
||||
|
||||
type_: Optional[str]
|
||||
"""
|
||||
The previous type of the resource. If not provided, the current type of the resource is used.
|
||||
"""
|
||||
|
||||
parent: Optional[Union['Resource', 'Input[str]']]
|
||||
"""
|
||||
The previous parent of the resource. If not provided (i.e. `Alias(name="foo")`), the current
|
||||
parent of the resource is used (`opts.parent` if provided, else the implicit stack resource
|
||||
parent).
|
||||
|
||||
To specify no original parent, use `Alias(parent=pulumi.rootStackResource)`.
|
||||
"""
|
||||
|
||||
stack: Optional['Input[str]']
|
||||
"""
|
||||
The name of the previous stack of the resource. If not provided, defaults to `pulumi.getStack()`.
|
||||
"""
|
||||
|
||||
project: Optional['Input[str]']
|
||||
"""
|
||||
The previous project of the resource. If not provided, defaults to `pulumi.getProject()`.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
name: Optional[str] = ...,
|
||||
type_: Optional[str] = ...,
|
||||
parent: Optional[Union['Resource', 'Input[str]']] = ...,
|
||||
stack: Optional['Input[str]'] = ...,
|
||||
project: Optional['Input[str]'] = ...) -> None:
|
||||
|
||||
self.name = name
|
||||
self.type_ = type_
|
||||
self.parent = parent
|
||||
self.stack = stack
|
||||
self.project = project
|
||||
|
||||
|
||||
def collapse_alias_to_urn(
|
||||
alias: 'Input[Union[Alias, str]]',
|
||||
defaultName: str,
|
||||
defaultType: str,
|
||||
defaultParent: Optional['Resource']) -> 'Output[str]':
|
||||
"""
|
||||
collapse_alias_to_urn turns an Alias into a URN given a set of default data
|
||||
"""
|
||||
|
||||
def collapse_alias_to_urn_worker(inner: Union[Alias, str]) -> 'Output[str]':
|
||||
if isinstance(inner, str):
|
||||
return Output.from_input(inner)
|
||||
|
||||
name = inner.name if inner.name is not ... else defaultName
|
||||
type_ = inner.type_ if inner.type_ is not ... else defaultType
|
||||
parent = inner.parent if inner.parent is not ... else defaultParent
|
||||
project = inner.project if inner.project is not ... else get_project()
|
||||
stack = inner.stack if inner.stack is not ... else get_stack()
|
||||
|
||||
if name is None:
|
||||
raise Exception("No valid 'name' passed in for alias.")
|
||||
|
||||
if type_ is None:
|
||||
raise Exception("No valid 'type_' passed in for alias.")
|
||||
|
||||
return create_urn(name, type_, parent, project, stack)
|
||||
|
||||
return Output.from_input(alias).apply(collapse_alias_to_urn_worker)
|
||||
|
||||
|
||||
class ResourceOptions:
|
||||
"""
|
||||
|
@ -99,6 +251,11 @@ class ResourceOptions:
|
|||
used.
|
||||
"""
|
||||
|
||||
aliases: Optional[List['Input[Union[str, Alias]]']]
|
||||
"""
|
||||
An optional list of aliases to treat this resource as matching.
|
||||
"""
|
||||
|
||||
additional_secret_outputs: [List[str]]
|
||||
"""
|
||||
The names of outputs for this resource that should be treated as secrets. This augments the list that
|
||||
|
@ -133,6 +290,7 @@ class ResourceOptions:
|
|||
delete_before_replace: Optional[bool] = None,
|
||||
ignore_changes: Optional[List[str]] = None,
|
||||
version: Optional[str] = None,
|
||||
aliases: Optional[List['Input[Union[str, Alias]]']] = None,
|
||||
additional_secret_outputs: Optional[List[str]] = None,
|
||||
id: Optional[str] = None,
|
||||
import_: Optional[str] = None,
|
||||
|
@ -168,6 +326,7 @@ class ResourceOptions:
|
|||
self.delete_before_replace = delete_before_replace
|
||||
self.ignore_changes = ignore_changes
|
||||
self.version = version
|
||||
self.aliases = aliases
|
||||
self.additional_secret_outputs = additional_secret_outputs
|
||||
self.custom_timeouts = custom_timeouts
|
||||
self.id = id
|
||||
|
@ -185,7 +344,8 @@ class Resource:
|
|||
|
||||
urn: 'Output[str]'
|
||||
"""
|
||||
The stable, logical URN used to distinctly address a resource, both before and after deployments.
|
||||
The stable, logical URN used to distinctly address a resource, both before and after
|
||||
deployments.
|
||||
"""
|
||||
|
||||
_providers: Mapping[str, 'ProviderResource']
|
||||
|
@ -198,6 +358,16 @@ class Resource:
|
|||
When set to true, protect ensures this resource cannot be deleted.
|
||||
"""
|
||||
|
||||
_aliases: 'Input[str]'
|
||||
"""
|
||||
A list of aliases applied to this resource.
|
||||
"""
|
||||
|
||||
_name: str
|
||||
"""
|
||||
The name assigned to the resource at construction.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
t: str,
|
||||
name: str,
|
||||
|
@ -225,6 +395,11 @@ class Resource:
|
|||
if opts is None:
|
||||
opts = ResourceOptions()
|
||||
|
||||
self._name = name
|
||||
|
||||
# Make a shallow clone of opts to ensure we don't modify the value passed in.
|
||||
opts = copy.copy(opts)
|
||||
|
||||
self._providers = {}
|
||||
# Check the parent type if one exists and fill in any default options.
|
||||
if opts.parent is not None:
|
||||
|
@ -235,6 +410,15 @@ class Resource:
|
|||
if opts.protect is None:
|
||||
opts.protect = opts.parent._protect
|
||||
|
||||
# Make a copy of the aliases array, and add to it any implicit aliases inherited from
|
||||
# its parent
|
||||
if opts.aliases is None:
|
||||
opts.aliases = []
|
||||
|
||||
opts.aliases = opts.aliases.copy()
|
||||
for parent_alias in opts.parent._aliases:
|
||||
opts.aliases.append(inherited_child_alias(name, opts.parent._name, parent_alias, t))
|
||||
|
||||
# Infer providers and provider maps from parent, if one was provided.
|
||||
self._providers = opts.parent._providers
|
||||
|
||||
|
@ -246,8 +430,8 @@ class Resource:
|
|||
# provider from our parent.
|
||||
opts.provider = opts.parent.get_provider(t)
|
||||
else:
|
||||
# If a provider was specified, add it to the providers map under this type's package so that
|
||||
# any children of this resource inherit its provider.
|
||||
# If a provider was specified, add it to the providers map under this type's package
|
||||
# so that any children of this resource inherit its provider.
|
||||
type_components = t.split(":")
|
||||
if len(type_components) == 3:
|
||||
[pkg, _, _] = type_components
|
||||
|
@ -258,6 +442,14 @@ class Resource:
|
|||
|
||||
self._protect = bool(opts.protect)
|
||||
|
||||
# Collapse any `Alias`es down to URNs. We have to wait until this point to do so because we
|
||||
# do not know the default `name` and `type` to apply until we are inside the resource
|
||||
# constructor.
|
||||
self._aliases = []
|
||||
if opts.aliases is not None:
|
||||
for alias in opts.aliases:
|
||||
self._aliases.append(collapse_alias_to_urn(alias, name, t, opts.parent))
|
||||
|
||||
if opts.id is not None:
|
||||
# If this resource already exists, read its state rather than registering it anow.
|
||||
if not custom:
|
||||
|
@ -266,6 +458,7 @@ class Resource:
|
|||
else:
|
||||
register_resource(self, t, name, custom, props, opts)
|
||||
|
||||
|
||||
def _convert_providers(self, provider: Optional['ProviderResource'], providers: Union[Mapping[str, 'ProviderResource'], List['ProviderResource']]) -> Mapping[str, 'ProviderResource']:
|
||||
if provider is not None:
|
||||
return self._convert_providers(None, [provider])
|
||||
|
@ -435,3 +628,37 @@ def export(name: str, value: Any):
|
|||
stack = get_root_resource()
|
||||
if stack is not None:
|
||||
stack.output(name, value)
|
||||
|
||||
|
||||
def create_urn(
|
||||
name: 'Input[str]',
|
||||
type_: 'Input[str]',
|
||||
parent: Optional[Union['Resource', 'Input[str]']] = None,
|
||||
project: str = None,
|
||||
stack: str = None) -> 'Output[str]':
|
||||
"""
|
||||
create_urn computes a URN from the combination of a resource name, resource type, optional
|
||||
parent, optional project and optional stack.
|
||||
"""
|
||||
|
||||
parent_prefix = None
|
||||
if parent is not None:
|
||||
parent_urn = None
|
||||
if isinstance(parent, Resource):
|
||||
parent_urn = parent.urn
|
||||
else:
|
||||
parent_urn = Output.from_input(parent)
|
||||
|
||||
parent_prefix = parent_urn.apply(
|
||||
lambda u: u[0:u.rfind("::")] + "$")
|
||||
else:
|
||||
if stack is None:
|
||||
stack = get_stack()
|
||||
|
||||
if project is None:
|
||||
project = get_project()
|
||||
|
||||
parent_prefix = "urn:pulumi:" + stack + "::" + project + "::"
|
||||
|
||||
return Output.all(parent_prefix, type_, name).apply(
|
||||
lambda arr: arr[0] + arr[1] + "::" + arr[2])
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
import asyncio
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Optional, Any, Callable, List, NamedTuple, Dict, Set, TYPE_CHECKING
|
||||
|
||||
from typing import Optional, Any, Callable, List, NamedTuple, Dict, Set, Union, TYPE_CHECKING
|
||||
from google.protobuf import struct_pb2
|
||||
import grpc
|
||||
|
||||
|
@ -22,10 +23,13 @@ from . import rpc, settings, known_types
|
|||
from .. import log
|
||||
from ..runtime.proto import resource_pb2
|
||||
from .rpc_manager import RPC_MANAGER
|
||||
from ..metadata import get_project, get_stack
|
||||
|
||||
from ..output import Output
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Resource, ResourceOptions
|
||||
from ..output import Output, Inputs
|
||||
from ..output import Inputs
|
||||
|
||||
|
||||
class ResourceResolverOperations(NamedTuple):
|
||||
|
@ -58,6 +62,11 @@ class ResourceResolverOperations(NamedTuple):
|
|||
A map from property name to the URNs of the resources the property depends on.
|
||||
"""
|
||||
|
||||
aliases: List[str]
|
||||
"""
|
||||
A list of aliases applied to this resource.
|
||||
"""
|
||||
|
||||
|
||||
# Prepares for an RPC that will manufacture a resource, and hence deals with input and output properties.
|
||||
# pylint: disable=too-many-locals
|
||||
|
@ -82,7 +91,8 @@ async def prepare_resource(res: 'Resource',
|
|||
parent_urn = ""
|
||||
if opts is not None and opts.parent is not None:
|
||||
parent_urn = await opts.parent.urn.future()
|
||||
elif ty != "pulumi:pulumi:Stack": # TODO(sean) is it necessary to check the type here?
|
||||
# TODO(sean) is it necessary to check the type here?
|
||||
elif ty != "pulumi:pulumi:Stack":
|
||||
# If no parent was provided, parent to the root resource.
|
||||
parent = settings.get_root_resource()
|
||||
if parent is not None:
|
||||
|
@ -109,19 +119,33 @@ async def prepare_resource(res: 'Resource',
|
|||
dependencies.add(urn)
|
||||
property_dependencies[key] = list(urns)
|
||||
|
||||
# Wait for all aliases. Note that we use `res._aliases` instead of `opts.aliases` as the
|
||||
# former has been processed in the Resource constructor prior to calling
|
||||
# `register_resource` - both adding new inherited aliases and simplifying aliases down
|
||||
# to URNs.
|
||||
aliases = []
|
||||
for alias in res._aliases:
|
||||
alias_val = await Output.from_input(alias).future()
|
||||
if not alias_val in aliases:
|
||||
aliases.append(alias_val)
|
||||
|
||||
log.debug(f"resource {props} prepared")
|
||||
return ResourceResolverOperations(
|
||||
parent_urn,
|
||||
serialized_props,
|
||||
dependencies,
|
||||
provider_ref,
|
||||
property_dependencies
|
||||
property_dependencies,
|
||||
aliases,
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-locals,too-many-statements
|
||||
|
||||
|
||||
def read_resource(res: 'Resource', ty: str, name: str, props: 'Inputs', opts: Optional['ResourceOptions']):
|
||||
if opts.id is None:
|
||||
raise Exception("Cannot read resource whose options are lacking an ID value")
|
||||
raise Exception(
|
||||
"Cannot read resource whose options are lacking an ID value")
|
||||
|
||||
log.debug(f"reading resource: ty={ty}, name={name}, id={opts.id}")
|
||||
monitor = settings.get_monitor()
|
||||
|
@ -148,7 +172,8 @@ def read_resource(res: 'Resource', ty: str, name: str, props: 'Inputs', opts: Op
|
|||
resolve_value = asyncio.Future()
|
||||
resolve_perform_apply = asyncio.Future()
|
||||
resolve_secret = asyncio.Future()
|
||||
res.id = known_types.new_output({res}, resolve_value, resolve_perform_apply, resolve_secret)
|
||||
res.id = known_types.new_output(
|
||||
{res}, resolve_value, resolve_perform_apply, resolve_secret)
|
||||
|
||||
def do_resolve(value: Any, perform_apply: bool, exn: Optional[Exception]):
|
||||
if exn is not None:
|
||||
|
@ -181,7 +206,8 @@ def read_resource(res: 'Resource', ty: str, name: str, props: 'Inputs', opts: Op
|
|||
# here.
|
||||
additional_secret_outputs = opts.additional_secret_outputs
|
||||
if res.translate_input_property is not None and opts.additional_secret_outputs is not None:
|
||||
additional_secret_outputs = map(res.translate_input_property, opts.additional_secret_outputs)
|
||||
additional_secret_outputs = map(
|
||||
res.translate_input_property, opts.additional_secret_outputs)
|
||||
|
||||
req = resource_pb2.ReadResourceRequest(
|
||||
type=ty,
|
||||
|
@ -196,11 +222,13 @@ def read_resource(res: 'Resource', ty: str, name: str, props: 'Inputs', opts: Op
|
|||
additionalSecretOutputs=additional_secret_outputs,
|
||||
)
|
||||
|
||||
from ..resource import create_urn
|
||||
mock_urn = await create_urn(name, ty, resolver.parent_urn).future()
|
||||
|
||||
def do_rpc_call():
|
||||
if monitor is None:
|
||||
# If no monitor is available, we'll need to fake up a response, for testing.
|
||||
test_urn = create_test_urn(ty, name)
|
||||
return RegisterResponse(test_urn, None, resolver.serialized_props)
|
||||
return RegisterResponse(mock_urn, None, resolver.serialized_props)
|
||||
|
||||
# If there is a monitor available, make the true RPC request to the engine.
|
||||
try:
|
||||
|
@ -218,7 +246,8 @@ def read_resource(res: 'Resource', ty: str, name: str, props: 'Inputs', opts: Op
|
|||
resp = await asyncio.get_event_loop().run_in_executor(None, do_rpc_call)
|
||||
|
||||
except Exception as exn:
|
||||
log.debug(f"exception when preparing or executing rpc: {traceback.format_exc()}")
|
||||
log.debug(
|
||||
f"exception when preparing or executing rpc: {traceback.format_exc()}")
|
||||
rpc.resolve_outputs_due_to_exception(resolvers, exn)
|
||||
resolve_urn_exn(exn)
|
||||
resolve_id(None, False, exn)
|
||||
|
@ -226,17 +255,20 @@ def read_resource(res: 'Resource', ty: str, name: str, props: 'Inputs', opts: Op
|
|||
|
||||
log.debug(f"resource read successful: ty={ty}, urn={resp.urn}")
|
||||
resolve_urn(resp.urn)
|
||||
resolve_id(resolved_id, True, None) # Read IDs are always known.
|
||||
resolve_id(resolved_id, True, None) # Read IDs are always known.
|
||||
await rpc.resolve_outputs(res, props, resp.properties, resolvers)
|
||||
|
||||
asyncio.ensure_future(RPC_MANAGER.do_rpc("read resource", do_read)())
|
||||
|
||||
# pylint: disable=too-many-locals,too-many-statements
|
||||
|
||||
|
||||
def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props: 'Inputs', opts: Optional['ResourceOptions']):
|
||||
"""
|
||||
registerResource registers a new resource object with a given type t and name. It returns the auto-generated
|
||||
URN and the ID that will resolve after the deployment has completed. All properties will be initialized to property
|
||||
objects that the registration operation will resolve at the right time (or remain unresolved for deployments).
|
||||
registerResource registers a new resource object with a given type t and name. It returns the
|
||||
auto-generated URN and the ID that will resolve after the deployment has completed. All
|
||||
properties will be initialized to property objects that the registration operation will resolve
|
||||
at the right time (or remain unresolved for deployments).
|
||||
"""
|
||||
log.debug(f"registering resource: ty={ty}, name={name}, custom={custom}")
|
||||
monitor = settings.get_monitor()
|
||||
|
@ -257,12 +289,14 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
|
|||
res.urn = known_types.new_output({res}, urn_future, urn_known, urn_secret)
|
||||
|
||||
# If a custom resource, make room for the ID property.
|
||||
resolve_id: Optional[Callable[[Any, str, Optional[Exception]], None]] = None
|
||||
resolve_id: Optional[Callable[[
|
||||
Any, str, Optional[Exception]], None]] = None
|
||||
if custom:
|
||||
resolve_value = asyncio.Future()
|
||||
resolve_perform_apply = asyncio.Future()
|
||||
resolve_secret = asyncio.Future()
|
||||
res.id = known_types.new_output({res}, resolve_value, resolve_perform_apply, resolve_secret)
|
||||
res.id = known_types.new_output(
|
||||
{res}, resolve_value, resolve_perform_apply, resolve_secret)
|
||||
|
||||
def do_resolve(value: Any, perform_apply: bool, exn: Optional[Exception]):
|
||||
if exn is not None:
|
||||
|
@ -289,18 +323,21 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
|
|||
|
||||
property_dependencies = {}
|
||||
for key, deps in resolver.property_dependencies.items():
|
||||
property_dependencies[key] = resource_pb2.RegisterResourceRequest.PropertyDependencies(urns=deps)
|
||||
property_dependencies[key] = resource_pb2.RegisterResourceRequest.PropertyDependencies(
|
||||
urns=deps)
|
||||
|
||||
ignore_changes = opts.ignore_changes
|
||||
if res.translate_input_property is not None and opts.ignore_changes is not None:
|
||||
ignore_changes = map(res.translate_input_property, opts.ignore_changes)
|
||||
ignore_changes = map(
|
||||
res.translate_input_property, opts.ignore_changes)
|
||||
|
||||
# Note that while `additional_secret_outputs` lists property names that are outputs, we call
|
||||
# `translate_input_property` because it is the method that converts from the language projection
|
||||
# name to the provider name, which is what we want.
|
||||
# Note that while `additional_secret_outputs` lists property names that are outputs, we
|
||||
# call `translate_input_property` because it is the method that converts from the
|
||||
# language projection name to the provider name, which is what we want.
|
||||
additional_secret_outputs = opts.additional_secret_outputs
|
||||
if res.translate_input_property is not None and opts.additional_secret_outputs is not None:
|
||||
additional_secret_outputs = map(res.translate_input_property, opts.additional_secret_outputs)
|
||||
additional_secret_outputs = map(
|
||||
res.translate_input_property, opts.additional_secret_outputs)
|
||||
|
||||
req = resource_pb2.RegisterResourceRequest(
|
||||
type=ty,
|
||||
|
@ -317,16 +354,18 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
|
|||
version=opts.version or "",
|
||||
acceptSecrets=True,
|
||||
additionalSecretOutputs=additional_secret_outputs,
|
||||
aliases=[],
|
||||
importId=opts.import_,
|
||||
customTimeouts=opts.custom_timeouts
|
||||
customTimeouts=opts.custom_timeouts,
|
||||
aliases=resolver.aliases,
|
||||
)
|
||||
|
||||
from ..resource import create_urn
|
||||
mock_urn = await create_urn(name, ty, resolver.parent_urn).future()
|
||||
|
||||
def do_rpc_call():
|
||||
if monitor is None:
|
||||
# If no monitor is available, we'll need to fake up a response, for testing.
|
||||
test_urn = create_test_urn(ty, name)
|
||||
return RegisterResponse(test_urn, None, resolver.serialized_props)
|
||||
return RegisterResponse(mock_urn, None, resolver.serialized_props)
|
||||
|
||||
# If there is a monitor available, make the true RPC request to the engine.
|
||||
try:
|
||||
|
@ -343,7 +382,8 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
|
|||
|
||||
resp = await asyncio.get_event_loop().run_in_executor(None, do_rpc_call)
|
||||
except Exception as exn:
|
||||
log.debug(f"exception when preparing or executing rpc: {traceback.format_exc()}")
|
||||
log.debug(
|
||||
f"exception when preparing or executing rpc: {traceback.format_exc()}")
|
||||
rpc.resolve_outputs_due_to_exception(resolvers, exn)
|
||||
resolve_urn_exn(exn)
|
||||
if resolve_id is not None:
|
||||
|
@ -361,16 +401,19 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
|
|||
|
||||
await rpc.resolve_outputs(res, props, resp.object, resolvers)
|
||||
|
||||
asyncio.ensure_future(RPC_MANAGER.do_rpc("register resource", do_register)())
|
||||
asyncio.ensure_future(RPC_MANAGER.do_rpc(
|
||||
"register resource", do_register)())
|
||||
|
||||
|
||||
def register_resource_outputs(res: 'Resource', outputs: 'Union[Inputs, Awaitable[Inputs], Output[Inputs]]'):
|
||||
async def do_register_resource_outputs():
|
||||
urn = await res.urn.future()
|
||||
serialized_props = await rpc.serialize_properties(outputs, {})
|
||||
log.debug(f"register resource outputs prepared: urn={urn}, props={serialized_props}")
|
||||
log.debug(
|
||||
f"register resource outputs prepared: urn={urn}, props={serialized_props}")
|
||||
monitor = settings.get_monitor()
|
||||
req = resource_pb2.RegisterResourceOutputsRequest(urn=urn, outputs=serialized_props)
|
||||
req = resource_pb2.RegisterResourceOutputsRequest(
|
||||
urn=urn, outputs=serialized_props)
|
||||
|
||||
def do_rpc_call():
|
||||
if monitor is None:
|
||||
|
@ -390,9 +433,11 @@ def register_resource_outputs(res: 'Resource', outputs: 'Union[Inputs, Awaitable
|
|||
raise Exception(details)
|
||||
|
||||
await asyncio.get_event_loop().run_in_executor(None, do_rpc_call)
|
||||
log.debug(f"resource registration successful: urn={urn}, props={serialized_props}")
|
||||
log.debug(
|
||||
f"resource registration successful: urn={urn}, props={serialized_props}")
|
||||
|
||||
asyncio.ensure_future(RPC_MANAGER.do_rpc("register resource outputs", do_register_resource_outputs)())
|
||||
asyncio.ensure_future(RPC_MANAGER.do_rpc(
|
||||
"register resource outputs", do_register_resource_outputs)())
|
||||
|
||||
|
||||
class RegisterResponse:
|
||||
|
@ -405,10 +450,3 @@ class RegisterResponse:
|
|||
self.urn = urn
|
||||
self.id = id
|
||||
self.object = object
|
||||
|
||||
|
||||
def create_test_urn(ty: str, name: str) -> str:
|
||||
"""
|
||||
Creates a test URN for cases where the engine isn't available to give us one (i.e., test mode).
|
||||
"""
|
||||
return 'urn:pulumi:{0}::{1}::{2}::{3}'.format(settings.get_stack(), settings.get_project(), ty, name)
|
||||
|
|
|
@ -4,20 +4,25 @@ package ints
|
|||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/testing/integration"
|
||||
)
|
||||
|
||||
// TestAliases tests a case where a resource's name changes but it provides an `alias` pointing to the old URN to ensure
|
||||
// the resource is preserved across the update.
|
||||
func TestAliases(t *testing.T) {
|
||||
dirs := []string{
|
||||
"rename", "adopt_into_component", "rename_component",
|
||||
"rename_component_and_child", "retype_component",
|
||||
}
|
||||
var dirs = []string{
|
||||
"rename",
|
||||
"adopt_into_component",
|
||||
"rename_component_and_child",
|
||||
"retype_component",
|
||||
"rename_component",
|
||||
}
|
||||
|
||||
// TestNodejsAliases tests a case where a resource's name changes but it provides an `alias`
|
||||
// pointing to the old URN to ensure the resource is preserved across the update.
|
||||
func TestNodejsAliases(t *testing.T) {
|
||||
for _, dir := range dirs {
|
||||
d := dir
|
||||
d := path.Join("nodejs", dir)
|
||||
t.Run(d, func(t *testing.T) {
|
||||
integration.ProgramTest(t, &integration.ProgramTestOptions{
|
||||
Dir: path.Join(d, "step1"),
|
||||
|
@ -34,3 +39,25 @@ func TestAliases(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPythonAliases(t *testing.T) {
|
||||
for _, dir := range dirs {
|
||||
d := path.Join("python", dir)
|
||||
t.Run(d, func(t *testing.T) {
|
||||
integration.ProgramTest(t, &integration.ProgramTestOptions{
|
||||
Dir: path.Join(d, "step1"),
|
||||
Dependencies: []string{
|
||||
filepath.Join("..", "..", "..", "sdk", "python", "env", "src"),
|
||||
},
|
||||
Quick: true,
|
||||
EditDirs: []integration.EditDir{
|
||||
{
|
||||
Dir: path.Join(d, "step2"),
|
||||
Additive: true,
|
||||
ExpectNoChanges: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class ComponentThree extends pulumi.ComponentResource {
|
|||
this.resource2 = new Resource("otherchild", { parent: this });
|
||||
}
|
||||
}
|
||||
// ...but applying an alias to the instance succesfully renames both the component and the children.
|
||||
// ...but applying an alias to the instance successfully renames both the component and the children.
|
||||
const comp3 = new ComponentThree("newcomp3", {
|
||||
aliases: [{ name: "comp3" }],
|
||||
});
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
|
||||
const stackName = pulumi.getStack();
|
||||
const projectName = pulumi.getProject();
|
||||
|
||||
class Resource extends pulumi.ComponentResource {
|
||||
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
|
||||
super("my:module:Resource", name, {}, opts);
|
|
@ -0,0 +1,3 @@
|
|||
name: aliases_adopt_into_component
|
||||
description: A program that replaces a resource with a new name and alias.
|
||||
runtime: python
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import ComponentResource, export, Resource, ResourceOptions
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
|
||||
# Scenario #2 - adopt a resource into a component
|
||||
class Component1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Component", name, None, opts)
|
||||
|
||||
res2 = Resource1("res2")
|
||||
comp2 = Component1("comp2")
|
||||
|
||||
# Scenario 3: adopt this resource into a new parent.
|
||||
class Component2(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Component2", name, None, opts)
|
||||
|
||||
unparented_comp2 = Component2("unparented")
|
||||
|
||||
# Scenario 4: Make a child resource that is parented by opts instead of 'this'. Fix
|
||||
# in the next step to be parented by this. Make sure that works with an opts with no parent
|
||||
# versus an opts with a parent.
|
||||
|
||||
class Component3(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Component3", name, None, opts)
|
||||
mycomp2 = Component2(name + "-child", opts)
|
||||
|
||||
parented_by_stack_comp3 = Component3("parentedbystack")
|
||||
parented_by_component_comp3 = Component3("parentedbycomponent", ResourceOptions(parent=comp2))
|
||||
|
||||
# Scenario 5: Allow multiple aliases to the same resource.
|
||||
class Component4(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Component4", name)
|
||||
|
||||
comp4 = Component4("duplicateAliases", ResourceOptions(parent=comp2))
|
|
@ -0,0 +1,66 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
import copy
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #2 - adopt a resource into a component. The component author is the same as the
|
||||
# component user, and changes the component to be able to adopt the resource that was previously
|
||||
# defined separately...
|
||||
class Component1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Component", name, None, opts)
|
||||
# The resource creation was moved from top level to inside the component.
|
||||
resource = Resource1(name + "-child", ResourceOptions(
|
||||
# With a new parent
|
||||
parent=self,
|
||||
# But with an alias provided based on knowing where the resource existing before - in
|
||||
# this case at top level. We use an absolute URN instead of a relative `Alias` because
|
||||
# we are referencing a fixed resource that was in some arbitrary other location in the
|
||||
# hierarchy prior to being adopted into this component.
|
||||
aliases=[create_urn("res2", "my:module:Resource")]))
|
||||
|
||||
# The creation of the component is unchanged.
|
||||
comp2 = Component1("comp2")
|
||||
|
||||
|
||||
# Scenario 3: adopt this resource into a new parent.
|
||||
class Component2(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Component2", name, None, opts)
|
||||
|
||||
|
||||
# validate that "parent: undefined" means "i didn't have a parent previously"
|
||||
unparented_comp2 = Component2("unparented", ResourceOptions(
|
||||
aliases=[Alias(parent=ROOT_STACK_RESOURCE)],
|
||||
parent=comp2))
|
||||
|
||||
|
||||
# Scenario 4: Make a child resource that is parented by opts instead of 'this'. Fix in the next
|
||||
# step to be parented by this. Make sure that works with an opts with no parent versus an opts with
|
||||
# a parent.
|
||||
|
||||
class Component3(ComponentResource):
|
||||
def __init__(self, name, opts=ResourceOptions()):
|
||||
super().__init__("my:module:Component3", name, None, opts)
|
||||
mycomp2 = Component2(name + "-child", ResourceOptions(
|
||||
aliases=[Alias(parent=opts.parent)],
|
||||
parent=self))
|
||||
|
||||
parented_by_stack_comp3 = Component3("parentedbystack")
|
||||
parented_by_component_comp3 = Component3("parentedbycomponent", ResourceOptions(parent=comp2))
|
||||
|
||||
# Scenario 5: Allow multiple aliases to the same resource.
|
||||
class Component4(ComponentResource):
|
||||
def __init__(self, name, opts=ResourceOptions()):
|
||||
child_opts = copy.copy(opts)
|
||||
if child_opts.aliases is None:
|
||||
child_opts.aliases = [Alias(parent=ROOT_STACK_RESOURCE), Alias(parent=ROOT_STACK_RESOURCE)]
|
||||
|
||||
super().__init__("my:module:Component4", name, None, child_opts)
|
||||
|
||||
comp4 = Component4("duplicateAliases", ResourceOptions(parent=comp2))
|
|
@ -0,0 +1,3 @@
|
|||
name: aliases_rename
|
||||
description: A program that replaces a resource with a new name and alias.
|
||||
runtime: python
|
10
tests/integration/aliases/python/rename/step1/__main__.py
Normal file
10
tests/integration/aliases/python/rename/step1/__main__.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #1 - rename a resource
|
||||
res1 = Resource1("res1")
|
12
tests/integration/aliases/python/rename/step2/__main__.py
Normal file
12
tests/integration/aliases/python/rename/step2/__main__.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #1 - rename a resource
|
||||
# This resource was previously named `res1`, we'll alias to the old name.
|
||||
res1 = Resource1("newres1", ResourceOptions(
|
||||
aliases=[Alias(name="res1")]))
|
|
@ -0,0 +1,3 @@
|
|||
name: aliases_rename_component
|
||||
description: A program that replaces a resource with a new name and alias.
|
||||
runtime: python
|
|
@ -0,0 +1,19 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #3 - rename a component (and all it's children)
|
||||
class ComponentThree(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:ComponentThree", name, None, opts)
|
||||
# Note that both un-prefixed and parent-name-prefixed child names are supported. For the
|
||||
# later, the implicit alias inherited from the parent alias will include replacing the name
|
||||
# prefix to match the parent alias name.
|
||||
resource1 = Resource1(name + "-child", ResourceOptions(parent=self))
|
||||
resource2 = Resource1("otherchild", ResourceOptions(parent=self))
|
||||
|
||||
comp3 = ComponentThree("comp3")
|
|
@ -0,0 +1,22 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #3 - rename a component (and all it's children)
|
||||
# No change to the component...
|
||||
class ComponentThree(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:ComponentThree", name, None, opts)
|
||||
# Note that both un-prefixed and parent-name-prefixed child names are supported. For the
|
||||
# later, the implicit alias inherited from the parent alias will include replacing the name
|
||||
# prefix to match the parent alias name.
|
||||
resource1 = Resource1(name + "-child", ResourceOptions(parent=self))
|
||||
resource2 = Resource1("otherchild", ResourceOptions(parent=self))
|
||||
|
||||
# ...but applying an alias to the instance successfully renames both the component and the children.
|
||||
comp3 = ComponentThree("newcomp3", ResourceOptions(
|
||||
aliases=[Alias(name="comp3")]))
|
|
@ -0,0 +1,3 @@
|
|||
name: aliases_rename_component_and_child
|
||||
description: A program that replaces a resource with a new name and alias.
|
||||
runtime: python
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #5 - composing #1 and #3 and making both changes at the same time
|
||||
class ComponentFive(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:ComponentFive", name, None, opts)
|
||||
res1 = Resource1("otherchild", ResourceOptions(parent=self))
|
||||
|
||||
comp5 = ComponentFive("comp5")
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #5 - composing #1 and #3 and making both changes at the same time
|
||||
class ComponentFive(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:ComponentFive", name, None, opts)
|
||||
res1 = Resource1("otherchildrenamed", ResourceOptions(
|
||||
parent=self,
|
||||
aliases=[Alias(name="otherchild", parent=self)]))
|
||||
|
||||
comp5 = ComponentFive("newcomp5", ResourceOptions(
|
||||
aliases=[Alias(name="comp5")]))
|
|
@ -0,0 +1,3 @@
|
|||
name: aliases_retype_component
|
||||
description: A program that replaces a resource with a new name and alias.
|
||||
runtime: python
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #4 - change the type of a component
|
||||
class ComponentFour(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:ComponentFour", name, None, opts)
|
||||
res1 = Resource1("otherchild", ResourceOptions(parent=self))
|
||||
|
||||
comp4 = ComponentFour("comp4")
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
import copy
|
||||
|
||||
from pulumi import Alias, ComponentResource, export, Resource, ResourceOptions, create_urn, ROOT_STACK_RESOURCE
|
||||
|
||||
class Resource1(ComponentResource):
|
||||
def __init__(self, name, opts=None):
|
||||
super().__init__("my:module:Resource", name, None, opts)
|
||||
|
||||
# Scenario #4 - change the type of a component
|
||||
class ComponentFour(ComponentResource):
|
||||
def __init__(self, name, opts=ResourceOptions()):
|
||||
# Add an alias that references the old type of this resource...
|
||||
aliases = [Alias(type_="my:module:ComponentFour")]
|
||||
if opts.aliases is not None:
|
||||
for alias in opts.aliases:
|
||||
aliases.append(alias)
|
||||
|
||||
# ..and then make the super call with the new type of this resource and the added alias.
|
||||
opts_copy = copy.copy(opts)
|
||||
opts_copy.aliases = aliases
|
||||
super().__init__("my:differentmodule:ComponentFourWithADifferentTypeName", name, None, opts_copy)
|
||||
|
||||
# The child resource will also pick up an implicit alias due to the new type of the component it is parented
|
||||
# to.
|
||||
res1 = Resource1("otherchild", ResourceOptions(parent=self))
|
||||
|
||||
comp4 = ComponentFour("comp4")
|
Loading…
Reference in a new issue