Add python aliases support. (#2974)

This commit is contained in:
CyrusNajmabadi 2019-07-25 11:21:06 -07:00 committed by GitHub
parent c6916051f0
commit 237f8d2222
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 615 additions and 55 deletions

View file

@ -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)

View file

@ -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 (

View file

@ -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])

View file

@ -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)

View file

@ -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,
},
},
})
})
}
}

View file

@ -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" }],
});

View file

@ -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);

View file

@ -0,0 +1,3 @@
name: aliases_adopt_into_component
description: A program that replaces a resource with a new name and alias.
runtime: python

View file

@ -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))

View file

@ -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))

View file

@ -0,0 +1,3 @@
name: aliases_rename
description: A program that replaces a resource with a new name and alias.
runtime: python

View 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")

View 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")]))

View file

@ -0,0 +1,3 @@
name: aliases_rename_component
description: A program that replaces a resource with a new name and alias.
runtime: python

View file

@ -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")

View file

@ -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")]))

View file

@ -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

View file

@ -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")

View file

@ -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")]))

View file

@ -0,0 +1,3 @@
name: aliases_retype_component
description: A program that replaces a resource with a new name and alias.
runtime: python

View file

@ -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")

View file

@ -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")