infer

package
v0.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2022 License: Apache-2.0 Imports: 24 Imported by: 88

README

Infer

The infer module provides infrastructure to infer Pulumi component resources, custom resources and functions from go code.

Defining a component resource

Here we will define a component resource that aggregates two custom resources from the random provider. Our component resource will serve a username, derived from either random.RandomId or random.RandomPet. It will also serve a password, derived from random.RandomPassword. We will call the component resource Login.

To encapsulate the idea of a new component resource, we define the resource, its inputs and its outputs:

type Login struct{}
type LoginArgs struct {
  PasswordLength pulumi.IntPtrInput `pulumi:"passwordLength"`
  PetName        bool               `pulumi:"petName"`
}

type LoginState struct {
  pulumi.ResourceState

	 PasswordLength pulumi.IntPtrInput `pulumi:"passwordLength"`
	 PetName        bool               `pulumi:"petName"`
	 // Outputs
	 Username pulumi.StringOutput `pulumi:"username"`
	 Password pulumi.StringOutput `pulumi:"password"`
}

Each field is tagged with pulumi:"name". Pulumi (and the infer module) only acts on fields with this tag. Pulumi names don't need to match up with with field names, but they should be lowerCamelCase. Fields also need to be exported (capitalized) to interact with Pulumi.

Most fields on components are Inputty or Outputty types, which means they are eventual values. We will make a decision based on PetName, so it is simply a bool. This tells Pulumi that PetName needs to be a prompt value so we can make decisions based on it. Specefically, we decide if we should construct the username based on a random.RandomPet or a random.RandomId.

Now that we have defined the type of the component, we need to define how to actually construct the component resource:

func (r *Login) Construct(ctx *pulumi.Context, name, typ string, args LoginArgs, opts pulumi.ResourceOption) (
 *LoginState, error) {
	comp := &LoginState{}
	err := ctx.RegisterComponentResource(typ, name, comp, opts)
	if err != nil {
		return nil, err
	}
	if args.PetName {
		pet, err := random.NewRandomPet(ctx, name+"-pet", &random.RandomPetArgs{}, pulumi.Parent(comp))
		if err != nil {
			return nil, err
		}
		comp.Username = pet.ID().ToStringOutput()
	} else {
		id, err := random.NewRandomId(ctx, name+"-id", &random.RandomIdArgs{
			ByteLength: pulumi.Int(8),
		}, pulumi.Parent(comp))
		if err != nil {
			return nil, err
		}
		comp.Username = id.ID().ToStringOutput()
	}
	var length pulumi.IntInput = pulumi.Int(16)
	if args.PasswordLength != nil {
		length = args.PasswordLength.ToIntPtrOutput().Elem()
	}
	password, err := random.NewRandomPassword(ctx, name+"-password", &random.RandomPasswordArgs{
		Length: length,
	}, pulumi.Parent(comp))
	if err != nil {
		return nil, err
	}
	comp.Password = password.Result

	return comp, nil
}

This works almost exactly like defining a component resource in Pulumi Go normally does. It is not necessary to call ctx.RegisterComponentResourceOutputs.

The last step in defining the component is serving it. Here we define the provider, telling it that it should serve the Login component. We then run that provider in main with RunProvider.

func main() {
	err := p.RunProvider("", semver.Version{Minor: 1}, provider())
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %s", err.Error())
		os.Exit(1)
  }
}

func provider() p.Provider {
	return infer.NewProvider().
		WithComponents(infer.Component[*Login, LoginArgs, *LoginState]())
}

This is all it takes to serve a component provider.

Defining a custom resource

As our example of a custom resource, we will implement a custom resource to represent a file in the local file system. This will take us through most of the functionality that inferred custom resource support, including the full CRUD lifecycle.

Full working code for this example can be found in examples/file/main.go.

We first declare the defining struct, its arguments and its state.

type File struct{}

type FileArgs struct {
	Path    string `pulumi:"path,optional"`
	Force   bool   `pulumi:"force,optional"`
	Content string `pulumi:"content"`
}

type FileState struct {
	Path    string `pulumi:"path"`
	Force   bool   `pulumi:"force"`
	Content string `pulumi:"content"`
}

To add descriptions to the new resource, we implement the Annotated interface for File, FileArgs and FileState. This will add descriptions to the resource, its input fields and its output fields.

func (f *File) Annotate(a infer.Annotator) {
	a.Describe(&f, "A file projected into a pulumi resource")
}


func (f *FileArgs) Annotate(a infer.Annotator) {
	a.Describe(&f.Content, "The content of the file.")
	a.Describe(&f.Force, "If an already existing file should be deleted if it exists.")
	a.Describe(&f.Path, "The path of the file. This defaults to the name of the pulumi resource.")
}

func (f *FileState) Annotate(a infer.Annotator) {
	a.Describe(&f.Content, "The content of the file.")
	a.Describe(&f.Force, "If an already existing file should be deleted if it exists.")
	a.Describe(&f.Path, "The path of the file.")
}

The only mandatory method for a CustomResource is Create.

func (*File) Create(ctx p.Context, name string, input FileArgs, preview bool) (
 id string, output FileState, err error) {
	if !input.Force {
		_, err := os.Stat(input.Path)
		if !os.IsNotExist(err) {
			return "", FileState{}, fmt.Errorf("file already exists; pass force=true to override")
		}
	}

	if preview { // Don't do the actual creating if in preview
		return input.Path, FileState{}, nil
	}

	f, err := os.Create(input.Path)
	if err != nil {
		return "", FileState{}, err
	}
	defer f.Close()
	n, err := f.WriteString(input.Content)
	if err != nil {
		return "", FileState{}, err
	}
	if n != len(input.Content) {
		return "", FileState{}, fmt.Errorf("only wrote %d/%d bytes", n, len(input.Content))
	}
	return input.Path, FileState{
		Path:    input.Path,
		Force:   input.Force,
		Content: input.Content,
	}, nil
}

We would like the file to be deleted when the custom resource is deleted. We can do that by implementing the Delete method:

func (*File) Delete(ctx p.Context, id string, props FileState) error {
	err := os.Remove(props.Path)
	if os.IsNotExist(err) {
		ctx.Logf(diag.Warning, "file %q already deleted", props.Path)
		err = nil
	}
	return err
}

Note that we can issue diagnostics to the user via the passed on Context. The diagnostic messages are tied to the resource that the method is called on, and pulumi will group them nicely:

Diagnostics:
  fs:index:File (managedFile):
    warning: file "managedFile" already deleted

The next method to implement is Check. We say in the description of FileArgs.Path that it defaults to the name of the resource, but that isn't implement in Create. Instead, we automatically fill the FileArgs.Path field from name if it isn't present in our check implementation.

func (*File) Check(ctx p.Context, name string, oldInputs, newInputs resource.PropertyMap) (
 FileArgs, []p.CheckFailure, error) {
	if _, ok := newInputs["path"]; !ok {
		newInputs["path"] = resource.NewStringProperty(name)
	}
	return infer.DefaultCheck[FileArgs](newInputs)
}

We still want to make sure our inputs are valid, so we make the adjustment for giving "path" a default value and the call into DefaultCheck, which ensures that all fields are valid given the constraints of their types.

We want to allow our users to change the content of the file they are managing. To allow updates, we need to implement the Update method:

func (*File) Update(ctx p.Context, id string, olds FileState, news FileArgs, preview bool) (FileState, error) {
	if !preview && olds.Content != news.Content {
		f, err := os.Create(olds.Path)
		if err != nil {
			return FileState{}, err
		}
		defer f.Close()
		n, err := f.WriteString(news.Content)
		if err != nil {
			return FileState{}, err
		}
		if n != len(news.Content) {
			return FileState{}, fmt.Errorf("only wrote %d/%d bytes", n, len(news.Content))
		}
	}

	return FileState{
		Path:    news.Path,
		Force:   news.Force,
		Content: news.Content,
	}, nil

}

The above code is pretty strait forward. Note that we don't handle when FileArgs.Path changes, since thats not really an update to an existing file. Its more of a replace operation. To tell pulumi that changes in FileArgs.Content and FileArgs.Force can be handled by updates, but that changes to FileArgs.Path require a replace, we need to override how diff works:

func (*File) Diff(ctx p.Context, id string, olds FileState, news FileArgs) (p.DiffResponse, error) {
	diff := map[string]p.PropertyDiff{}
	if news.Content != olds.Content {
		diff["content"] = p.PropertyDiff{Kind: p.Update}
	}
	if news.Force != olds.Force {
		diff["force"] = p.PropertyDiff{Kind: p.Update}
	}
	if news.Path != olds.Path {
		diff["path"] = p.PropertyDiff{Kind: p.UpdateReplace}
	}
	return p.DiffResponse{
		DeleteBeforeReplace: true,
		HasChanges:          len(diff) > 0,
		DetailedDiff:        diff,
	}, nil
}

We check for each field, and if there is a change, we record it. Changes in news.Content and news.Force result in an Update, but changes in news.Path result in an UpdateReplace. Since the `id (file path) is globally unique, we also tell Pulumi that it needs to perform deletes before the associated create.

Last but not least, we want to be able to read state from the file system as is. Unsurprisingly, we do this by implementing yet another method:

func (*File) Read(ctx p.Context, id string, inputs FileArgs, state FileState) (
 string, FileArgs, FileState, err error) {
	path := id
	byteContent, err := ioutil.ReadFile(path)
	if err != nil {
		return "", FileArgs{}, FileState{}, err
	}
	content := string(byteContent)
	return path, FileArgs{
			Path:    path,
			Force:   inputs.Force && state.Force,
			Content: content,
		}, FileState{
			Path:    path,
			Force:   inputs.Force && state.Force,
			Content: content,
		}, nil
}

Here we get a partial view of the id, inputs and state and need to figure out the rest. We return the correct id, args and state.

This is an example of a fully functional custom resource, with full participation in the Pulumi lifecycle.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultCheck

func DefaultCheck[I any](inputs resource.PropertyMap) (I, []p.CheckFailure, error)

Ensure that `inputs` can deserialize cleanly into `I`.

func GetConfig

func GetConfig[T any](ctx p.Context) T

Retrieve the configuration of this provider.

Note: Config will panic if the type of T does not match the type of the config or if the provider has not supplied a config.

Types

type Annotated

type Annotated interface {
	Annotate(Annotator)
}

Annotated is used to describe the fields of an object or a resource. Annotated can be implemented by `CustomResource`s, the input and output types for all resources and invokes, as well as other structs used the above.

type Annotator

type Annotator interface {
	// Annotate a struct field with a text description.
	Describe(i any, description string)

	// Annotate a struct field with a default value. The default value must be a primitive
	// type in the pulumi type system.
	SetDefault(i any, defaultValue any, env ...string)
}

The methods of Annotator must be called on pointers to fields of their receivers, or on their receiver itself.

func (*s Struct) Annotated(a Annotator) {
 a.Describe(&s, "A struct")            // Legal
	a.Describe(&s.field1, "A field")      // Legal
	a.Describe(s.field2, "A field")       // Not legal, since the pointer is missing.
	otherS := &Struct{}
	a.Describe(&otherS.field1, "A field") // Not legal, since describe is not called on its receiver.
}

type ComponentResource

type ComponentResource[I any, O pulumi.ComponentResource] interface {
	// Construct a component resource
	//
	// ctx.RegisterResource needs to be called, but ctx.RegisterOutputs does not need to
	// be called.
	Construct(ctx *pulumi.Context, name, typ string, inputs I, opts pulumi.ResourceOption) (O, error)
}

A component resource.

type Crawler

type Crawler func(t reflect.Type) (drill bool, err error)

type CustomCheck

type CustomCheck[I any] interface {
	// Maybe oldInputs can be of type I
	Check(ctx p.Context, name string, oldInputs resource.PropertyMap, newInputs resource.PropertyMap) (
		I, []p.CheckFailure, error)
}

A resource that understands how to check its inputs.

By default, infer handles checks by ensuring that a inputs de-serialize correctly. This is where you can extend that behavior. The returned input is given to subsequent calls to `Create` and `Update`.

Example: TODO - Maybe a resource that has a regex. We could reject invalid regex before the up actually happens.

type CustomDelete

type CustomDelete[O any] interface {
	// Delete is called before a resource is removed from pulumi state.
	Delete(ctx p.Context, id string, props O) error
}

A resource that knows how to delete itself.

If a resource does not implement Delete, no code will be run on resource deletion.

type CustomDiff

type CustomDiff[I, O any] interface {
	// Maybe oldInputs can be of type I
	Diff(ctx p.Context, id string, olds O, news I) (p.DiffResponse, error)
}

A resource that understands how to diff itself given a new set of inputs.

By default, infer handles diffs by structural equality among inputs. If CustomUpdate is implemented, changes will result in updates. Otherwise changes will result in replaces.

Example: TODO - Indicate replacements for certain changes but not others.

type CustomRead

type CustomRead[I, O any] interface {
	// Read accepts a resource id, and a best guess of the input and output state. It returns
	// a normalized version of each, assuming it can be recovered.
	Read(ctx p.Context, id string, inputs I, state O) (
		canonicalID string, normalizedInputs I, normalizedState O, err error)
}

A resource that can recover its state from the provider.

If CustomRead is not implemented, it will default to checking that the inputs and state fit into I and O respectively. If they do, then the values will be returned as is. Otherwise an error will be returned.

Example: TODO - Probably something to do with the file system.

type CustomResource

type CustomResource[I any, O any] interface {
	Create(ctx p.Context, name string, input I, preview bool) (id string, output O, err error)
}

A resource that understands how to create itself. This is the minimum requirement for defining a new custom resource.

This interface should be implemented by the resource controller, with `I` the resource inputs and `O` the full set of resource fields. It is recommended that `O` is a superset of `I`, but it is not strictly required. The fields of `I` and `O` should consist of non-pulumi types i.e. `string` and `int` instead of `pulumi.StringInput` and `pulumi.IntOutput`.

The behavior of a CustomResource resource can be extended by implementing any of the following traits: - CustomCheck - CustomDiff - CustomUpdate - CustomRead - CustomDelete

Example: TODO

type CustomUpdate

type CustomUpdate[I, O any] interface {
	Update(ctx p.Context, id string, olds O, news I, preview bool) (O, error)
}

A resource that can adapt to new inputs with a delete and replace.

There is no default behavior for CustomUpdate.

Here the old state (as returned by Create or Update) as well as the new inputs are passed. Update should return the new state of the resource. If preview is true, then the update is part of `pulumi preview` and no changes should be made.

Example: TODO

type Enum

type Enum[T EnumKind] interface {
	// A list of all allowed values for the enum.
	Values() []EnumValue[T]
}

An Enum in the Pulumi type system.

type EnumKind

type EnumKind interface {
	~string | ~float64 | ~bool | ~int
}

The set of allowed enum underlying values.

type EnumValue

type EnumValue[T any] struct {
	Name        string
	Value       T
	Description string
}

An EnumValue represents an allowed value for an Enum.

type ExplicitDependencies

type ExplicitDependencies[I, O any] interface {
	// WireDependencies specifies the dependencies between inputs and outputs.
	WireDependencies(f FieldSelector, args *I, state *O)
}

A custom resource with the dataflow between its arguments (`I`) and outputs (`O`) specified. If a CustomResource implements ExplicitDependencies then WireDependencies will be called for each Create and Update call with `args` and `state` holding the values they will have for that call.

type FieldSelector

type FieldSelector interface {
	// Create an input field. The argument to InputField must be a pointer to a field of
	// the associated input type I.
	//
	// For example:
	// “`go
	// func (r *MyResource) WireDependencies(f infer.FieldSelector, args *MyArgs, state *MyState) {
	//   f.InputField(&args.Field)
	// }
	// “`
	InputField(any) InputField
	// Create an output field. The argument to OutputField must be a pointer to a field of
	// the associated output type O.
	//
	// For example:
	// “`go
	// func (r *MyResource) WireDependencies(f infer.FieldSelector, args *MyArgs, state *MyState) {
	//   f.OutputField(&state.Field)
	// }
	// “`
	OutputField(any) OutputField
	// contains filtered or unexported methods
}

An interface to help wire fields together.

type Fn

type Fn[I any, O any] interface {
	// A function is a mapping from `I` to `O`.
	Call(ctx p.Context, input I) (output O, err error)
}

A Function (also called Invoke) inferred from code. `I` is the function input, and `O` is the function output. Both must be structs.

type InferredComponent

type InferredComponent interface {
	t.ComponentResource
	schema.Resource
	// contains filtered or unexported methods
}

A component resource inferred from code. To get an instance of an InferredComponent, call the function Component.

func Component

Define a component resource from go code. Here `R` is the component resource anchor, `I` describes its inputs and `O` its outputs. To add descriptions to `R`, `I` and `O`, see the `Annotated` trait defined in this module.

type InferredConfig

type InferredConfig interface {
	schema.Resource
	// contains filtered or unexported methods
}

func Config

func Config[T any]() InferredConfig

Turn an object into a description for the provider configuration.

`T` has the same properties as an input or output type for a custom resource, and is responsive to the same interfaces.

`T` can implement CustomDiff and CustomCheck.

type InferredFunction

type InferredFunction interface {
	t.Invoke
	schema.Function
	// contains filtered or unexported methods
}

A function inferred from code. See Function for creating a InferredFunction.

func Function

func Function[F Fn[I, O], I, O any]() InferredFunction

Infer a function from `F`, which maps `I` to `O`.

type InferredResource

type InferredResource interface {
	t.CustomResource
	schema.Resource
	// contains filtered or unexported methods
}

A resource inferred by the Resource function.

This interface cannot be implemented directly. Instead consult the Resource function.

func Resource

func Resource[R CustomResource[I, O], I, O any]() InferredResource

Create a new InferredResource, where `R` is the resource controller, `I` is the resources inputs and `O` is the resources outputs.

type InputField

type InputField interface {
	// contains filtered or unexported methods
}

A field of the input (args).

type OutputField

type OutputField interface {
	// Specify that a state (output) field is always secret, regardless of its dependencies.
	AlwaysSecret()
	// Specify that a state (output) field is never secret, regardless of its dependencies.
	NeverSecret()
	// Specify that a state (output) Field uses data from some args (input) Fields.
	DependsOn(dependencies ...InputField)
	// contains filtered or unexported methods
}

A field of the output (state).

type Provider

type Provider struct {
	p.Provider
	// contains filtered or unexported fields
}

A provider that serves resources inferred from go code.

func NewProvider

func NewProvider() *Provider

Create a new base provider to serve resources inferred from go code.

func (*Provider) CheckConfig

func (prov *Provider) CheckConfig(ctx p.Context, req p.CheckRequest) (p.CheckResponse, error)

func (*Provider) Configure

func (prov *Provider) Configure(ctx p.Context, req p.ConfigureRequest) error

func (*Provider) DiffConfig

func (prov *Provider) DiffConfig(ctx p.Context, req p.DiffRequest) (p.DiffResponse, error)

func (*Provider) WithComponents

func (prov *Provider) WithComponents(components ...InferredComponent) *Provider

Add inferred component resources to the provider.

To allow method chaining, WithComponents mutates the instance it was called on and then returns it.

func (*Provider) WithConfig

func (prov *Provider) WithConfig(config InferredConfig) *Provider

Give the provider global state. This will define a provider resource.

func (*Provider) WithDescription added in v0.6.0

func (prov *Provider) WithDescription(description string) *Provider

func (*Provider) WithDisplayName added in v0.6.0

func (prov *Provider) WithDisplayName(name string) *Provider

func (*Provider) WithFunctions

func (prov *Provider) WithFunctions(fns ...InferredFunction) *Provider

Add inferred functions (also mentioned as invokes) to the provider.

To allow method chaining, WithFunctions mutates the instance it was called on and then returns it.

func (*Provider) WithHomepage added in v0.6.0

func (prov *Provider) WithHomepage(homepage string) *Provider

func (*Provider) WithKeywords added in v0.6.0

func (prov *Provider) WithKeywords(keywords []string) *Provider

func (*Provider) WithLanguageMap added in v0.6.0

func (prov *Provider) WithLanguageMap(languages map[string]any) *Provider

func (*Provider) WithLicense added in v0.6.0

func (prov *Provider) WithLicense(license string) *Provider

func (*Provider) WithLogoURL added in v0.6.0

func (prov *Provider) WithLogoURL(logoURL string) *Provider

func (*Provider) WithModuleMap

func (prov *Provider) WithModuleMap(m map[tokens.ModuleName]tokens.ModuleName) *Provider

WithModuleMap provides a mapping between go modules and pulumi modules.

For example, given a provider `pkg` with defines resources `foo.Foo`, `foo.Bar`, and `fizz.Buzz` the provider will expose resources at `pkg:foo:Foo`, `pkg:foo:Bar` and `pkg:fizz:Buzz`. Adding

`WithModuleMap(map[tokens.ModuleName]tokens.ModuleName{"foo": "bar"})`

will instead result in exposing the same resources at `pkg:bar:Foo`, `pkg:bar:Bar` and `pkg:fizz:Buzz`.

func (*Provider) WithPublisher added in v0.6.0

func (prov *Provider) WithPublisher(publisher string) *Provider

func (*Provider) WithRepository added in v0.6.0

func (prov *Provider) WithRepository(repoURL string) *Provider

func (*Provider) WithResources

func (prov *Provider) WithResources(resources ...InferredResource) *Provider

Add inferred resources to the provider.

To allow method chaining, WithResources mutates the instance it was called on and then returns it.

Directories

Path Synopsis
tests module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL