Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sdk/nodejs,python] Add support for explicit providers for packaged components #13282

Merged
merged 6 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changes:
- type: feat
scope: sdk/nodejs,python
description: Support explicit providers for packaged components
17 changes: 6 additions & 11 deletions sdk/nodejs/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { ResourceError } from "./errors";
import * as log from "./log";
import { Input, Inputs, interpolate, Output, output } from "./output";
import { getResource, readResource, registerResource, registerResourceOutputs } from "./runtime/resource";
import { getResource, pkgFromType, readResource, registerResource, registerResourceOutputs } from "./runtime/resource";
import { unknownValue } from "./runtime/rpc";
import { getProject, getStack } from "./runtime/settings";
import { getStackResource } from "./runtime/state";
Expand Down Expand Up @@ -279,11 +279,10 @@ export abstract class Resource {

// getProvider fetches the provider for the given module member, if any.
public getProvider(moduleMember: string): ProviderResource | undefined {
const memComponents = moduleMember.split(":");
if (memComponents.length !== 3) {
const pkg = pkgFromType(moduleMember);
if (pkg === undefined) {
return undefined;
}
const pkg = memComponents[0];

return this.__providers[pkg];
}
Expand Down Expand Up @@ -384,12 +383,8 @@ export abstract class Resource {
// 1. opts.provider
// 2. a matching provider in opts.providers
// 3. a matching provider inherited from opts.parent
if (custom && opts.provider === undefined) {
let pkg = undefined;
const memComponents = t.split(":");
if (memComponents.length === 3) {
pkg = memComponents[0];
}
if ((custom || remote) && opts.provider === undefined) {
const pkg = pkgFromType(t);
const parentProvider = parent?.getProvider(t);

if (pkg && pkg in this.__providers) {
Expand All @@ -400,7 +395,7 @@ export abstract class Resource {
}

this.__protect = !!opts.protect;
this.__prov = custom ? opts.provider : undefined;
this.__prov = custom || remote ? opts.provider : undefined;
this.__version = opts.version;
this.__pluginDownloadURL = opts.pluginDownloadURL;

Expand Down
29 changes: 28 additions & 1 deletion sdk/nodejs/runtime/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,11 +688,26 @@ export async function prepareResource(
// If no parent was provided, parent to the root resource.
const parentURN = parent ? await parent.urn.promise() : undefined;

let providerRef: string | undefined;
let importID: ID | undefined;
if (custom) {
const customOpts = <CustomResourceOptions>opts;
importID = customOpts.import;
}

let providerRef: string | undefined;
let sendProvider = custom;
if (remote && opts.provider) {
// If it's a remote component and a provider was specified, only
// send the provider in the request if the provider's package is
// the same as the component's package. Otherwise, don't send it
// because the user specified `provider: someProvider` as shorthand
// for `providers: [someProvider]`.
const pkg = pkgFromType(type!);
if (pkg && pkg === opts.provider.getPackage()) {
sendProvider = true;
}
}
if (sendProvider) {
providerRef = await ProviderResource.register(opts.provider);
}

Expand Down Expand Up @@ -1114,3 +1129,15 @@ function runAsyncResourceOp(label: string, callback: () => Promise<void>, serial
}
}
}

/**
* Extract the pkg from the type token of the form "pkg:module:member".
* @internal
*/
export function pkgFromType(type: string): string | undefined {
const parts = type.split(":");
if (parts.length === 3) {
return parts[0];
}
return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ class Provider extends pulumi.ProviderResource {
}
}

class FooProvider extends pulumi.ProviderResource {
constructor(name, opts) {
super("foo", name, {}, opts);
}
}

class RemoteComponent extends pulumi.ComponentResource {
constructor(name, opts) {
super("test:index:Component", name, {}, opts, true /*remote*/);
Expand All @@ -20,3 +26,9 @@ const myprovider = new Provider("myprovider");
new RemoteComponent("singular", { provider: myprovider });
new RemoteComponent("map", { providers: { test: myprovider } });
new RemoteComponent("array", { providers: [myprovider] });

const fooprovider = new FooProvider("fooprovider");

new RemoteComponent("foo-singular", { provider: fooprovider });
new RemoteComponent("foo-map", { providers: { foo: fooprovider } });
new RemoteComponent("foo-array", { providers: [fooprovider] });
10 changes: 7 additions & 3 deletions sdk/nodejs/tests/runtime/langhost/run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1469,7 +1469,7 @@ describe("rpc", () => {
},
remote_component_providers: {
program: path.join(base, "068.remote_component_providers"),
expectResourceCount: 4,
expectResourceCount: 8,
registerResource: (
ctx: any,
dryrun: boolean,
Expand All @@ -1489,10 +1489,14 @@ describe("rpc", () => {
providers?: any,
) => {
if (name === "singular" || name === "map" || name === "array") {
assert.strictEqual(provider, "");
assert.strictEqual(provider, "pulumi:providers:test::myprovider::1");
assert.deepStrictEqual(Object.keys(providers), ["test"]);
}
return { urn: makeUrn(t, name), id: undefined, props: undefined };
if (name === "foo-singular" || name === "foo-map" || name === "foo-array") {
assert.strictEqual(provider, "");
assert.deepStrictEqual(Object.keys(providers), ["foo"]);
}
return { urn: makeUrn(t, name), id: name === "myprovider" ? "1" : undefined, props: undefined };
},
},
ambiguous_entrypoints: {
Expand Down
14 changes: 5 additions & 9 deletions sdk/python/lib/pulumi/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from .metadata import get_project, get_stack
from .runtime import known_types
from .runtime.resource import (
_pkg_from_type,
get_resource,
register_resource,
register_resource_outputs,
Expand Down Expand Up @@ -843,15 +844,11 @@ def __init__(
# Infer providers and provider maps from parent, if one was provided.
self._providers = opts.parent._providers

type_components = t.split(":")
pkg = None
if len(type_components) == 3:
[pkg, _, _] = type_components

pkg = _pkg_from_type(t)
opts.provider, opts.providers = self._get_providers(t, pkg, opts)

self._protect = bool(opts.protect)
self._provider = opts.provider if custom else None
self._provider = opts.provider if (custom or remote) else None
if self._provider and self._provider.package != pkg:
action = (
"get"
Expand Down Expand Up @@ -994,11 +991,10 @@ def get_provider(self, module_member: str) -> Optional["ProviderResource"]:
:return: The :class:`ProviderResource` associated with the given module member, or None if one does not exist.
:rtype: Optional[ProviderResource]
"""
components = module_member.split(":")
if len(components) != 3:
pkg = _pkg_from_type(module_member)
if pkg is None:
return None

[pkg, _, _] = components
return self._providers.get(pkg)


Expand Down
20 changes: 19 additions & 1 deletion sdk/python/lib/pulumi/runtime/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,15 @@ async def prepare_resource(

# Construct the provider reference, if we were given a provider to use.
provider_ref = None
if custom and opts is not None and opts.provider is not None:
send_provider = custom
if remote and opts is not None and opts.provider is not None:
# If it's a remote component and a provider was specified, only
# send the provider in the request if the provider's package is
# the same as the component's package.
pkg = _pkg_from_type(ty)
if pkg is not None and pkg == opts.provider.package:
send_provider = True
if send_provider and opts is not None and opts.provider is not None:
provider = opts.provider

# If we were given a provider, wait for it to resolve and construct a provider reference from it.
Expand Down Expand Up @@ -1106,3 +1114,13 @@ async def _resolve_depends_on_urns(
all_deps.add(direct_dep)

return await rpc._expand_dependencies(all_deps, from_resource)


def _pkg_from_type(ty: str) -> Optional[str]:
"""
Extract the pkg from the type token of the form "pkg:module:member".
"""
parts = ty.split(":")
if len(parts) != 3:
return None
return parts[0]
2 changes: 1 addition & 1 deletion sdk/python/lib/test/langhost/aliases/test_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_component_dependencies(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, _protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
return {
"urn": f"urn:pulumi:stack::project::{ty}::{name}",
"id": "myID",
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/lib/test/langhost/asset/test_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_asset(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual(ty, "test:index:MyResource")
if name == "file":
self.assertIsInstance(_resource["asset"], FileAsset)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_chained_failure(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if ty == "test:index:ResourceA":
self.assertEqual(name, "resourceA")
self.assertDictEqual(_resource, {"inprop": 777})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_component_dependencies(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):

if name == "resD":
self.assertListEqual(_dependencies, ["resA"], msg=f"{name}._dependencies")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_component_provider_resolution(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if name == "combined-mine":
self.assertTrue(protect)
self.assertEqual(_provider, "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_component_resource_list_of_providers(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if _custom and not ty.startswith("pulumi:providers:"):
expect_protect = False
expect_provider_name = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_component_resource_single_provider(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if _custom and not ty.startswith("pulumi:providers:"):
expect_protect = False
expect_provider_name = ""
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/lib/test/langhost/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_config(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual("test:index:MyResource", ty)
self.assertEqual("myname", name)
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_delete_before_replace(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual("foo", name)
self.assertTrue(_delete_before_replace)
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_first_class_provider(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if name == "testprov":
# Provider resource.
self.assertEqual("pulumi:providers:test", ty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def invoke(self, _ctx, token, args, provider, _version):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if name == "testprov":
self.assertEqual("pulumi:providers:test", ty)
self.prov_urn = self.make_urn(ty, name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_first_class_provider_unknown(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if name == "testprov":
self.assertEqual("pulumi:providers:test", ty)
# Only provide an ID when doing an update. When doing a preview the ID will be unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def invoke(self, _ctx, token, args, provider, _version):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual("test:index:MyResource", ty)
return {
"urn": self.make_urn(ty, name),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_future_input(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual(ty, "test:index:FileResource")
self.assertEqual(name, "file")
self.assertDictEqual(_resource, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_ignore_changes(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):

# Note that here we expect to receive `ignoredProperty`, even though the user provided `ignored_property`.
self.assertListEqual(_ignore_changes, ["ignoredProperty", "ignored_property_other"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_inherit_defaults(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
if _custom and not ty.startswith("pulumi:providers:"):
expect_protect = False
expect_provider_name = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_inheritance_translation(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual("test:index:MyResource", ty)
return {
"urn": self.make_urn(ty, name),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_inheritance_types(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual("test:index:MyResource", ty)
return {
"urn": self.make_urn(ty, name),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_input_type_mismatch(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
self.assertEqual("test:index:MyResource", ty)

policy = _resource["policy"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_input_values_for_outputs(self):

def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect,
_provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import,
_replace_on_changes):
_replace_on_changes, _providers):
return {
"urn": self.make_urn(ty, name),
"id": name,
Expand Down