Skip to content

Commit

Permalink
Add autoname implementation as an example (#255)
Browse files Browse the repository at this point in the history
Add an example of how autonaming can be implemented with the
pulumi-go-provider.
  • Loading branch information
iwahbe authored Jul 25, 2024
1 parent 61cac4c commit b1fe34e
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 0 deletions.
18 changes: 18 additions & 0 deletions examples/auto-naming/consumer/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: consume-autoname
runtime: yaml

plugins:
providers:
- name: auto-naming
path: ..

resources:
auto-named:
type: auto-naming:User
manually-named:
type: auto-naming:User
properties:
name: ${auto-named.name}

outputs:
user: ${auto-named.name}
98 changes: 98 additions & 0 deletions examples/auto-naming/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// package main shows how a [infer] based provider can implement auto-naming.
package main

import (
"context"
"fmt"
"os"

p "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/infer"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)

func main() {
err := p.RunProvider("auto-naming", "0.1.0", provider())
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s", err.Error())
os.Exit(1)
}
}

func provider() p.Provider {
return infer.Provider(infer.Options{
Resources: []infer.InferredResource{infer.Resource[*User, UserArgs, UserState]()},
ModuleMap: map[tokens.ModuleName]tokens.ModuleName{
"auto-naming": "index",
},
})
}

type (
User struct{}
UserArgs struct {
Name *string `pulumi:"name,optional"`
}
UserState struct{ UserArgs }
)

func (*User) Create(ctx context.Context, name string, input UserArgs, preview bool) (string, UserState, error) {
return name, UserState{input}, nil
}

var _ infer.CustomCheck[UserArgs] = ((*User)(nil))

func (*User) Check(
ctx context.Context, name string, oldInputs, newInputs resource.PropertyMap,
) (UserArgs, []p.CheckFailure, error) {
// Apply default arguments
args, failures, err := infer.DefaultCheck[UserArgs](newInputs)
if err != nil {
return args, failures, err
}

// Apply autonaming
//
// If args.Name is unset, we set it to a value based off of the resource name.
args.Name, err = autoname(args.Name, name, "name", oldInputs)
return args, failures, err
}

// autoname makes the field it is called on auto-named.
//
// It should be called in [infer.CustomCheck.Check].
//
// field is a reference to a *string typed field.
//
// name is the name of the resource as passed in via check.
//
// fieldName is the name of the place referenced by field. This is what was written in the
// `pulumi:"<fieldName>"` tag.
//
// oldInputs are the old inputs as passed in via [infer.CustomCheck.Check].
func autoname(
field *string, name string, fieldName resource.PropertyKey,
oldInputs resource.PropertyMap,
) (*string, error) {
if field != nil {
return field, nil
}

prev := oldInputs[fieldName]
if prev.IsSecret() {
prev = prev.SecretValue().Element
}

if prev.IsString() && prev.StringValue() != "" {
n := prev.StringValue()
field = &n
} else {
n, err := resource.NewUniqueHex(name+"-", 6, 20)
if err != nil {
return nil, err
}
field = &n
}
return field, nil
}
135 changes: 135 additions & 0 deletions examples/auto-naming/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package main

import (
"strings"
"testing"

"github.com/blang/semver"
p "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/integration"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAutoName(t *testing.T) {
t.Parallel()

s := integration.NewServer("autoname", semver.MustParse("0.1.0"), provider())

tests := []struct {
name string
olds resource.PropertyMap
news resource.PropertyMap
validate func(*testing.T, resource.PropertyMap)
}{
{
name: "create new auto-named resource",
news: resource.PropertyMap{},
validate: func(t *testing.T, result resource.PropertyMap) {
name, ok := result["name"]
require.True(t, ok, "could not find name in %q", result)
assert.True(t, strings.HasPrefix(name.StringValue(), "name-"))
assert.Len(t, name.StringValue(), 11)
assert.Len(t, result, 1)
},
},
{
name: "create new manually named resource",
news: resource.PropertyMap{
"name": resource.NewProperty("custom-name"),
},
validate: func(t *testing.T, result resource.PropertyMap) {
name, ok := result["name"]
require.True(t, ok, "could not find name in %q", result)
assert.Equal(t, "custom-name", name.StringValue())
},
},
{
name: "update an auto-named resource (same name)",
news: resource.PropertyMap{},
olds: resource.PropertyMap{
"name": resource.NewProperty("name-123456"),
},
validate: func(t *testing.T, result resource.PropertyMap) {
name, ok := result["name"]
require.True(t, ok, "could not find name in %q", result)
assert.Equal(t, "name-123456", name.StringValue())
},
},
{
name: "update an auto-named resource (new name)",
news: resource.PropertyMap{
"name": resource.NewProperty("custom-name"),
},
olds: resource.PropertyMap{
"name": resource.NewProperty("name-123456"),
},
validate: func(t *testing.T, result resource.PropertyMap) {
name, ok := result["name"]
require.True(t, ok, "could not find name in %q", result)
assert.Equal(t, "custom-name", name.StringValue())
},
},
{
name: "update a manually named resource (same name)",
news: resource.PropertyMap{
"name": resource.NewProperty("custom-name"),
},
olds: resource.PropertyMap{
"name": resource.NewProperty("custom-name"),
},
validate: func(t *testing.T, result resource.PropertyMap) {
name, ok := result["name"]
require.True(t, ok, "could not find name in %q", result)
assert.Equal(t, "custom-name", name.StringValue())
},
},
{
name: "update a manually named resource (different name)",
news: resource.PropertyMap{
"name": resource.NewProperty("custom-name1"),
},
olds: resource.PropertyMap{
"name": resource.NewProperty("custom-name2"),
},
validate: func(t *testing.T, result resource.PropertyMap) {
name, ok := result["name"]
require.True(t, ok, "could not find name in %q", result)
assert.Equal(t, "custom-name1", name.StringValue())
},
},
{
name: "convert from a named to an auto-named resource",
news: resource.PropertyMap{},
olds: resource.PropertyMap{
"name": resource.NewProperty("custom-name"),
},
validate: func(t *testing.T, result resource.PropertyMap) {
t.Skipf("It's not possible to drop custom-named resources without a side channel")
name, ok := result["name"]
require.True(t, ok, "could not find name in %q", result)

assert.True(t, strings.HasPrefix(name.StringValue(), "name-"))
assert.Len(t, name.StringValue(), 11)
assert.Len(t, result, 1)
},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
t.Helper()
resp, err := s.Check(p.CheckRequest{
Urn: resource.NewURN("dev", "test", "", "autoname:index:User", "name"),
News: tt.news,
Olds: tt.olds,
})
require.NoError(t, err)
require.Empty(t, resp.Failures)
tt.validate(t, resp.Inputs)
})
}
}

0 comments on commit b1fe34e

Please sign in to comment.