-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
autoname
implementation as an example (#255)
Add an example of how autonaming can be implemented with the pulumi-go-provider.
- Loading branch information
Showing
3 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |