Merge pull request #412 from pulumi/DynamicResources

Implement dynamic resources.
This commit is contained in:
Pat Gavlin 2017-10-16 23:20:33 -07:00 committed by GitHub
commit 5fce66ba44
18 changed files with 202 additions and 180 deletions

View file

@ -0,0 +1,85 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as pulumi from "pulumi";
import * as dynamic from "pulumi/dynamic";
class OperatorProvider implements dynamic.ResourceProvider {
private op: (l: number, r: number) => any;
constructor(op: (l: number, r: number) => any) {
this.op = op;
}
check = (inputs: any) => Promise.resolve(new dynamic.CheckResult(undefined, []));
diff = (id: pulumi.ID, olds: any, news: any) => Promise.resolve(new dynamic.DiffResult([], []));
delete = (id: pulumi.ID, props: any) => Promise.resolve();
create = (inputs: any) => Promise.resolve(new dynamic.CreateResult("0", this.op(Number(inputs.left), Number(inputs.right))));
update = (id: string, olds: any, news: any) => Promise.resolve(new dynamic.UpdateResult(this.op(Number(news.left), Number(news.right))));
}
class DivProvider extends OperatorProvider {
constructor() {
super((left: number, right: number) => <any>{ quotient: Math.floor(left / right), remainder: left % right });
}
check = (ins: any) => Promise.resolve(new dynamic.CheckResult(undefined, ins.right == 0 ? [ new dynamic.CheckFailure("right", "divisor must be non-zero") ] : []));
}
class Add extends dynamic.Resource {
public readonly sum: pulumi.Computed<number>;
private static provider = new OperatorProvider((left: number, right: number) => <any>{ sum: left + right });
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super(Add.provider, name, {left: left, right: right, sum: undefined}, undefined);
}
}
class Mul extends dynamic.Resource {
public readonly product: pulumi.Computed<number>;
private static provider = new OperatorProvider((left: number, right: number) => <any>{ product: left * right });
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super(Mul.provider, name, {left: left, right: right, product: undefined}, undefined);
}
}
class Sub extends dynamic.Resource {
public readonly difference: pulumi.Computed<number>;
private static provider = new OperatorProvider((left: number, right: number) => <any>{ difference: left - right });
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super(Sub.provider, name, {left: left, right: right, difference: undefined}, undefined);
}
}
class Div extends dynamic.Resource {
public readonly quotient: pulumi.Computed<number>;
public readonly remainder: pulumi.Computed<number>;
private static provider = new DivProvider();
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super(Div.provider, name, {left: left, right: right, quotient: undefined, remainder: undefined}, undefined);
}
}
let run = async () => {
let config = new pulumi.Config("simple:config");
let w = Number(config.require("w")), x = Number(config.require("x")), y = Number(config.require("y"));
let sum = new Add("sum", x, y);
let square = new Mul("square", sum.sum, sum.sum);
let diff = new Sub("diff", square.product, w);
let divrem = new Div("divrem", diff.difference, sum.sum);
let result = new Add("result", divrem.quotient, divrem.remainder);
console.log(`((x + y)^2 - w) / (x + y) + ((x + y)^2 - w) %% (x + y) = ${await result.sum}`);
};
run();

View file

@ -16,8 +16,7 @@
"strictNullChecks": true
},
"files": [
"index.ts",
"providers.ts"
"index.ts"
]
}

View file

@ -26,10 +26,9 @@ func TestExamples(t *testing.T) {
},
},
{
Dir: path.Join(cwd, "test-provider/simple"),
Dir: path.Join(cwd, "dynamic-provider/simple"),
Dependencies: []string{"pulumi"},
Config: map[string]string{
"testing:providers:module": "./bin/providers.js",
"simple:config:w": "1",
"simple:config:x": "1",
"simple:config:y": "1",

View file

@ -1,52 +0,0 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as pulumi from "pulumi";
class Add extends pulumi.CustomResource {
public readonly sum: pulumi.Computed<number>;
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super("testing:simple:add", name, {left: left, right: right, sum: undefined}, undefined);
}
}
class Mul extends pulumi.CustomResource {
public readonly product: pulumi.Computed<number>;
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super("testing:simple:mul", name, {left: left, right: right, product: undefined}, undefined);
}
}
class Sub extends pulumi.CustomResource {
public readonly difference: pulumi.Computed<number>;
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super("testing:simple:sub", name, {left: left, right: right, difference: undefined}, undefined);
}
}
class Div extends pulumi.CustomResource {
public readonly quotient: pulumi.Computed<number>;
public readonly remainder: pulumi.Computed<number>;
constructor(name: string, left: pulumi.ComputedValue<number>, right: pulumi.ComputedValue<number>) {
super("testing:simple:div", name, {left: left, right: right, quotient: undefined, remainder: undefined}, undefined);
}
}
let config = new pulumi.Config("simple:config");
let w = Number(config.require("w")), x = Number(config.require("x")), y = Number(config.require("y"));
let sum = new Add("sum", x, y);
let square = new Mul("square", sum.sum, sum.sum);
let diff = new Sub("diff", square.product, w);
let divrem = new Div("divrem", diff.difference, sum.sum);
let result = new Add("result", divrem.quotient, divrem.remainder);
let output = async function(): Promise<void> {
console.log(`((x + y)^2 - w) / (x + y) + ((x + y)^2 - w) %% (x + y) = ${await result.sum}`);
};
output();

View file

@ -1,38 +0,0 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as pulumi from "pulumi";
export class Operator implements pulumi.testing.ResourceProvider {
private op: (l: number, r: number) => any;
constructor(op: (l: number, r: number) => any) {
this.op = op;
}
check(inputs: any): Promise<pulumi.testing.CheckResult> { return Promise.resolve(new pulumi.testing.CheckResult(undefined, [])); }
diff(id: pulumi.ID, olds: any, news: any): Promise<pulumi.testing.DiffResult> { return Promise.resolve(new pulumi.testing.DiffResult([], [])); }
delete(id: pulumi.ID, props: any): Promise<void> { return Promise.resolve(); }
create(inputs: any): Promise<pulumi.testing.CreateResult> {
return Promise.resolve(new pulumi.testing.CreateResult("0", this.op(Number(inputs.left), Number(inputs.right))));
}
update(id: string, olds: any, news: any): Promise<pulumi.testing.UpdateResult> {
return Promise.resolve(new pulumi.testing.UpdateResult(this.op(Number(news.left), Number(news.right))));
}
}
export class Div extends Operator {
constructor() {
super((left: number, right: number) => <any>{ quotient: Math.floor(left / right), remainder: left % right });
}
check(ins: any): Promise<pulumi.testing.CheckResult> {
return Promise.resolve(new pulumi.testing.CheckResult(undefined, ins.right == 0 ? [ new pulumi.testing.CheckFailure("right", "divisor must be non-zero") ] : []));
}
}
export var add = new Operator((left: number, right: number) => <any>{ sum: left + right });
export var mul = new Operator((left: number, right: number) => <any>{ product: left * right });
export var sub = new Operator((left: number, right: number) => <any>{ difference: left - right });
export var div = new Div();

View file

@ -1,7 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
typescript@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d"

View file

@ -226,7 +226,9 @@ func (rm *resmon) Invoke(ctx context.Context, req *lumirpc.InvokeRequest) (*lumi
tok := tokens.ModuleMember(req.GetTok())
prov, err := rm.src.plugctx.Host.Provider(tok.Package())
if err != nil {
return nil, errors.Wrapf(err, "failed to load resource provider for %v", tok)
return nil, err
} else if prov == nil {
return nil, errors.Errorf("could not load resource provider for package '%v' from $PATH", tok.Package())
}
// Now unpack all of the arguments and prepare to perform the invocation.
@ -237,7 +239,7 @@ func (rm *resmon) Invoke(ctx context.Context, req *lumirpc.InvokeRequest) (*lumi
}
// Do the invoke and then return the arguments.
glog.V(5).Info("ResourceMonitor.Invoke received: tok=%v #args=%v", tok, len(args))
glog.V(5).Infof("ResourceMonitor.Invoke received: tok=%v #args=%v", tok, len(args))
ret, failures, err := prov.Invoke(tok, args)
if err != nil {
return nil, errors.Wrapf(err, "invocation of %v returned an error", tok)

View file

@ -0,0 +1,3 @@
#!/usr/bin/env node
require("./cmd/dynamic-provider");

View file

@ -1,3 +0,0 @@
#!/usr/bin/env node
require("./cmd/test-provider");

View file

@ -1,64 +1,45 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
// This is a mock resource provider that can be used to implement custom CRUD operations in JavaScript.
// It is configured using a single variable, `pulumi:testing:providers`, that provides the path to the JS
// module that implements CRUD operations for various types. When an operation is requested by the engine
// for a resource of a particular type, that type's `testing.Provider` is loaded from the input module
// using the unqualified type name.
import * as minimist from "minimist";
import * as path from "path";
import * as resource from "../../resource";
import * as testing from "../../testing";
import * as dynamic from "../../dynamic";
import * as resource from "../../resource";
const requireFromString = require("require-from-string");
const grpc = require("grpc");
const emptyproto = require("google-protobuf/google/protobuf/empty_pb.js");
const structproto = require("google-protobuf/google/protobuf/struct_pb.js");
const provproto = require("../../proto/provider_pb.js");
const provrpc = require("../../proto/provider_grpc_pb.js");
class Providers {
private registry: any;
const providerKey: string = "__provider";
constructor(registry: any) {
this.registry = registry;
}
get(urn: string): testing.ResourceProvider {
const urnNameDelimiter: string = "::";
const nsDelimiter: string = ":";
const type = urn.split(urnNameDelimiter)[2].split(nsDelimiter)[2];
return this.registry[type];
}
function getProvider(props: any): dynamic.ResourceProvider {
// TODO[pulumi/pulumi#414]: investigate replacing requireFromString with eval
return requireFromString(props[providerKey]).handler();
}
let providers: Providers;
// Each of the *RPC functions below implements a single method of the resource provider gRPC interface. The CRUD
// functions--checkRPC, diffRPC, createRPC, updateRPC, and deleteRPC--all operate in a similar fashion:
// 1. Deserialize the dyanmic provider for the resource on which the function is operating
// 2. Call the dynamic provider's corresponding {check,diff,create,update,delete} method
// 3. Convert and return the results
// In all cases, the dynamic provider is available in its serialized form as a property of the resource;
// getProvider` is responsible for handling its deserialization. In the case of diffRPC, if the provider itself
// has changed, `diff` reports that the resource requires replacement and does not delegate to the dynamic provider.
// This allows the creation of the replacement resource to use the new provider while the deletion of the old
// resource uses the provider with which it was created.
function configureRPC(call: any, callback: any): void {
try {
const req = call.request;
const variables = req.getVariablesMap();
let providersJS = variables.get(testing.ProvidersConfigKey);
if (providersJS.startsWith("./") || providersJS.startsWith("../")) {
providersJS = path.normalize(path.join(process.cwd(), providersJS));
}
providers = new Providers(require(providersJS));
callback(undefined, new emptyproto.Empty());
} catch (e) {
console.error(new Error().stack);
callback(e, undefined);
}
callback(undefined, new emptyproto.Empty());
}
async function invokeRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
const resp = new provproto.InvokeResponse();
// TODO[pulumi/pulumi#406]: implement this.
callback(undefined, resp);
callback(new Error(`unknown function ${req.getTok()}`), undefined);
}
async function checkRPC(call: any, callback: any): Promise<void> {
@ -66,7 +47,10 @@ async function checkRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
const resp = new provproto.CheckResponse();
const result = await providers.get(req.getUrn()).check(req.getProperties().toJavaScript());
const props = req.getProperties().toJavaScript();
const provider = getProvider(props);
const result = await provider.check(props);
if (result.defaults) {
resp.setDefaults(structproto.Struct.fromJavaScript(result.defaults));
}
@ -84,7 +68,7 @@ async function checkRPC(call: any, callback: any): Promise<void> {
callback(undefined, resp);
} catch (e) {
console.error(new Error().stack);
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
@ -94,15 +78,25 @@ async function diffRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
const resp = new provproto.DiffResponse();
const result: any = await providers.get(req.getUrn())
.diff(req.getId(), req.getOlds().toJavaScript(), req.getNews().toJavaScript());
if (result.replaces.length !== 0) {
resp.setReplaces(result.replaces);
// If the provider itself has changed, do not delegate to the dynamic provider. Instead, simply report that the
// resource requires replacement. This allows the new resource to be created using the new provider and the old
// resource to be deleted using the old provider.
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
if (olds[providerKey] !== news[providerKey]) {
resp.setReplacesList([ providerKey ]);
} else {
const provider = getProvider(olds);
const result: any = await provider.diff(req.getId(), olds, news);
if (result.replaces.length !== 0) {
resp.setReplacesList(result.replaces);
}
}
callback(undefined, resp);
} catch (e) {
console.error(new Error().stack);
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
@ -112,7 +106,10 @@ async function createRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
const resp = new provproto.CreateResponse();
const result = await providers.get(req.getUrn()).create(req.getProperties().toJavaScript());
const props = req.getProperties().toJavaScript();
const provider = getProvider(props);
const result = await provider.create(props);
resp.setId(result.id);
if (result.outs) {
resp.setProperties(structproto.Struct.fromJavaScript(result.outs));
@ -120,7 +117,7 @@ async function createRPC(call: any, callback: any): Promise<void> {
callback(undefined, resp);
} catch (e) {
console.error(new Error().stack);
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
@ -130,15 +127,21 @@ async function updateRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
const resp = new provproto.UpdateResponse();
const result: any = await providers.get(req.getUrn())
.update(req.getId(), req.getOlds().toJavaScript(), req.getNews().toJavaScript());
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
if (olds[providerKey] !== news[providerKey]) {
throw new Error("changes to provider should require replacement");
}
const provider = getProvider(olds);
const result: any = await provider.update(req.getId(), olds, news);
if (result.outs) {
resp.setProperties(structproto.Struct.fromJavaScript(result.outs));
}
callback(undefined, resp);
} catch (e) {
console.error(new Error().stack);
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
@ -146,10 +149,11 @@ async function updateRPC(call: any, callback: any): Promise<void> {
async function deleteRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
await providers.get(req.getUrn()).delete(req.getId(), req.getProperties());
const props: any = req.getProperties().toJavaScript();
await getProvider(props).delete(req.getId(), props);
callback(undefined, new emptyproto.Empty());
} catch (e) {
console.error(new Error().stack);
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}

View file

@ -1,13 +1,7 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as resource from "../resource";
/**
* ProvidersConfigKey is the configuration key used to provide the testing provider with the path to a JavaScript module
* that exports a map from (unqualified) type names to `ResourceProvider`s. This map is then used to decide which
* `ResourceProvider` should be used to implement the CRUD operations for a particular resource type.
*/
export const ProvidersConfigKey = "testing:providers:module"
import * as resource from "./resource";
import * as runtime from "./runtime";
/**
* CheckResult represents the results of a call to `ResourceProvider.check`.
@ -123,7 +117,7 @@ export class UpdateResult {
public readonly outs: any | undefined;
/**
* Constructs a new udpate result.
* Constructs a new update result.
*
* @param outs Any properties that were computed during updating.
*/
@ -133,7 +127,7 @@ export class UpdateResult {
}
/**
* ResourceProvider represents an object that provides CRUD operations for a particular
* ResourceProvider represents an object that provides CRUD operations for a particular type of resource.
*/
export interface ResourceProvider {
/**
@ -141,7 +135,7 @@ export interface ResourceProvider {
*
* @param inputs The full properties to use for validation.
*/
check(inputs: any): Promise<CheckResult>;
check: (inputs: any) => Promise<CheckResult>;
/**
* Diff checks what impacts a hypothetical update will have on the resource's properties.
@ -150,7 +144,7 @@ export interface ResourceProvider {
* @param olds The old values of properties to diff.
* @param news The new values of properties to diff.
*/
diff(id: resource.ID, olds: any, news: any): Promise<DiffResult>;
diff: (id: resource.ID, olds: any, news: any) => Promise<DiffResult>;
/**
* Create allocates a new instance of the provided resource and returns its unique ID afterwards.
@ -158,7 +152,7 @@ export interface ResourceProvider {
*
* @param inputs The properties to set during creation.
*/
create(inputs: any): Promise<CreateResult>;
create: (inputs: any) => Promise<CreateResult>;
/**
* Update updates an existing resource with new values.
@ -167,7 +161,7 @@ export interface ResourceProvider {
* @param olds The old values of properties to update.
* @param news The new values of properties to update.
*/
update(id: resource.ID, olds: any, news: any): Promise<UpdateResult>;
update: (id: resource.ID, olds: any, news: any) => Promise<UpdateResult>;
/**
* Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
@ -175,5 +169,37 @@ export interface ResourceProvider {
* @param id The ID of the resource to delete.
* @param props The current properties on the resource.
*/
delete(id: resource.ID, props: any): Promise<void>;
delete: (id: resource.ID, props: any) => Promise<void>;
}
async function serializeProvider(provider: ResourceProvider): Promise<string> {
return runtime.serializeJavaScriptText(await runtime.serializeClosure(() => provider));
}
/**
* Resource represents a Pulumi Resource that incorporates an inline implementation of the Resource's CRUD operations.
*/
export abstract class Resource extends resource.CustomResource {
/**
* Creates a new dynamic resource.
*
* @param provider The implementation of the resource's CRUD operations.
* @param name The name of the resource.
* @param props The arguments to use to populate the new resource. Must not define the reserved
* property "__provider".
* @param dependsOn Optional additional explicit dependencies on other resources.
*/
public constructor(provider: ResourceProvider,
name: string,
props: resource.ComputedValues,
dependsOn?: resource.Resource[]) {
const providerKey: string = "__provider";
if (props[providerKey]) {
throw new Error("A dynamic resource must not define the __provider key");
}
props[providerKey] = serializeProvider(provider);
super("pulumi-nodejs:dynamic:Resource", name, props, dependsOn);
}
}

View file

@ -9,8 +9,8 @@ export * from "./resource";
// Export submodules individually.
import * as asset from "./asset";
import * as dynamic from "./dynamic";
import * as log from "./log";
import * as runtime from "./runtime";
import * as testing from "./testing";
export { asset, log, runtime, testing };
export { asset, dynamic, log, runtime };

View file

@ -19,6 +19,7 @@
"google-protobuf": "^3.4.0",
"grpc": "^1.6.0",
"minimist": "^1.2.0",
"require-from-string": "^2.0.1",
"source-map-support": "^0.4.16"
}
}

View file

@ -18,6 +18,7 @@
"files": [
"index.ts",
"config.ts",
"dynamic.ts",
"errors.ts",
"resource.ts",
@ -37,11 +38,9 @@
"runtime/rpc.ts",
"runtime/settings.ts",
"cmd/dynamic-provider/index.ts",
"cmd/langhost/index.ts",
"cmd/run/index.ts",
"cmd/test-provider/index.ts",
"testing/index.ts",
"tests/config.spec.ts",
"tests/init.spec.ts",

View file

@ -1010,6 +1010,10 @@ request@2, request@^2.81.0:
tunnel-agent "^0.6.0"
uuid "^3.0.0"
require-from-string@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff"
resolve@1.1.x:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"