12

Introducing Resource Methods for Pulumi Packages

 2 years ago
source link: https://www.pulumi.com/blog/resource-methods-for-pulumi-packages/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Introducing Resource Methods for Pulumi Packages

Posted on Monday, Oct 11, 2021

It’s now possible to provide resource methods from Pulumi Packages. Resource methods are similar to functions, but instead of being exposed as top-level functions in a module, methods are exposed as methods on a resource class. This allows for a more object-oriented approach to exposing functionality—operations performed by a resource (that potentially use the resource’s state) can now be exposed as methods on the resource. Resource methods can be implemented once, in your language of choice, and made available to users in all Pulumi languages.

When authoring component resources, it’s often useful to provide additional functionality through methods on the component. For example, the Cluster component in the eks package has a getKubeconfig method that can be used to generate a kubeconfig for authentication with the cluster that does not use the default AWS credential provider chain, but is instead scoped based on the passed-in arguments. Until now, that method has only been available from JavaScript/TypeScript (the language the Cluster component was written in). With the new support for resource methods for Pulumi Packages, we can make this method available to all the Pulumi languages, which is exactly what we’ve done in pulumi-eks v0.34.0.

const cluster = new eks.Cluster("mycluster");
const kubeconfig = cluster.getKubeconfig(...);
Copy

This post will show how you can provide resource methods from your component packages.

Authoring Methods

It’s always been possible to provide module-level functions from Pulumi Packages. For example, the aws package provides many module-level functions, such as the getAmi function, which can be used to get the ID of an existing Amazon Machine Image (AMI). Functions are declared in the package’s schema, and their functionality is implemented in the provider through the Invoke remote procedure call (RPC).

Methods are authored in a similar manner to functions. Methods are declared in the schema and implemented in the provider’s Call RPC (similar to Invoke).

Let’s walk through an example. We’ll author a Message component that accepts a message as input. The component then provides a getMessage method that accepts a recipient name and returns the message customized for the recipient. To get started authoring a component package, refer to the package documentation.

Schema

We’ll start with declaring the method and component in the Pulumi schema. First, define the function representing the method:

  "functions": {
    "example:index:Message/getMessage": {
      "inputs": {
        "properties": {
          "__self__": {
            "$ref": "#/resources/example:index:Message"
          },
          "recipient": {
            "type": "string"
          }
        },
        "required": ["__self__", "recipient"]
      },
      "outputs": {
        "properties": {
          "result": {
            "type": "string"
          }
        },
        "required": ["result"]
      }
    }
  },
Copy

Our method has two required arguments: __self__ and recipient. __self__ is a special input required by all methods that represents the component resource and is typed as such. The method has one result named result.

Next, define our Message component:

  "resources": {
    "example:index:Message": {
      "isComponent": true,
      "inputProperties": {
        "message": {
          "type": "string"
        },
      },
      "requiredInputs": ["message"],
      "properties": {
        "message": {
          "type": "string"
        },
      },
      "required": ["message"],
      "methods": {
        "getMessage": "example:index:Message/getMessage"
      }
    }
  },
Copy

Our component has a single required input/output property: message. The method is specified in the methods property, which references the method’s function definition: "getMessage": "example:index:Message/getMessage".

Here’s our schema all together:

{
  "version": "0.0.1",
  "name": "example",
  "functions": {
    "example:index:Message/getMessage": {
      "inputs": {
        "properties": {
          "__self__": {
            "$ref": "#/resources/example:index:Message"
          },
          "recipient": {
            "type": "string"
          }
        },
        "required": ["__self__", "recipient"]
      },
      "outputs": {
        "properties": {
          "result": {
            "type": "string"
          }
        },
        "required": ["result"]
      }
    }
  },
  "resources": {
    "example:index:Message": {
      "isComponent": true,
      "inputProperties": {
        "message": {
          "type": "string"
        },
      },
      "requiredInputs": ["message"],
      "properties": {
        "message": {
          "type": "string"
        },
      },
      "required": ["message"],
      "methods": {
        "getMessage": "example:index:Message/getMessage"
      }
    }
  },
  "language": {
    "csharp": {
      "packageReferences": {
        "Pulumi": "3.12"
      },
      "liftSingleValueMethodReturns": true
    },
    "go": {
      "liftSingleValueMethodReturns": true
    },
    "nodejs": {
      "devDependencies": {
        "@types/node": "latest"
      },
      "liftSingleValueMethodReturns": true
    },
    "python": {
      "liftSingleValueMethodReturns": true
    }
  }
}
Copy

To make it easier for users of our method, we also set "liftSingleValueMethodReturns": true for each language, which makes single-value methods return the single value directly, rather than wrapping the results in a Result type.

Component Implementation

We can implement the component and make it available to any Pulumi language. We’re going to show some implementation examples in TypeScript/JavaScript, Python, and Go.

Here’s the implementation of the Message component:

import * as pulumi from "@pulumi/pulumi";

interface MessageArgs {
    message: pulumi.Input<string>;
}

class Message extends pulumi.ComponentResource {
    public readonly message!: pulumi.Output<string>;

    constructor(name: string, args: MessageArgs, opts?: pulumi.ComponentResourceOptions) {
        const props = { message: args?.message }
        super("example:index:Message", name, props, opts);

        if (opts?.urn) {
            // Skip further initialization when being constructed from a resource reference.
            return;
        }

        this.registerOutputs(props);
    }

    getMessage(recipient: pulumi.Input<string>): pulumi.Output<string> {
        return pulumi.iterpolate `${recipient}, ${this.message}!`;
    }
}
Copy

Provider RPCs

Next, wire up the provider RPCs with the component implementation:

import * as pulumi from "@pulumi/pulumi";
import * as provider from "@pulumi/pulumi/provider";

class Provider implements provider.Provider {
    public readonly version = "0.0.1";

    constructor() {
        // Register any resources that can come back as resource references that need to be rehydrated.
        pulumi.runtime.registerResourceModule("example", "index", {
            version: this.version,
            construct: (name, type, urn) => {
                switch (type) {
                    case "example:index:Message":
                        return new Component(name, <any>undefined, { urn });
                    default:
                        throw new Error(`unknown resource type ${type}`);
                }
            },
        });
    }

    async construct(name: string, type: string, inputs: pulumi.Inputs,
              options: pulumi.ComponentResourceOptions): Promise<provider.ConstructResult> {
        if (type != "example:index:Message") {
            throw new Error(`unknown resource type ${type}`);
        }

        const message = new Message(name, <MessageArgs>inputs, options);
        return {
            urn: message.urn,
            state: inputs,
        };
    }

    async call(token: string, inputs: pulumi.Inputs): Promise<provider.InvokeResult> {
        switch (token) {
            case "example:index:Message/getMessage":
                const self: Component = inputs.__self__;
                return {
                    outputs: {
                        result: self.getMessage(inputs.recipient),
                    },
                };

            default:
                throw new Error(`unknown method ${token}`);
        }
    }
}

export function main(args: string[]) {
    return provider.main(new Provider(), args);
}

main(process.argv.slice(2));
Copy

The Construct RPC is called when creating an instance of the component. The Call RPC is called when a method is called.

Using the Component

Now we can build and try out using the component and its method from any Pulumi language:

const component = new example.Message("mycomponent", {
    message: "hello world",
});
export const message = component.getMessage({ recipient: "Alice" }); // Exports "Alice, hello world!"
Copy

Wrapping Up

With support for resource methods for Pulumi Packages, you can now create component resources with methods and make them available to use from any Pulumi language. We look forward to seeing the components you create!

👉 Author your first Pulumi Package


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK