diff --git a/dynamic/provider_test.go b/dynamic/provider_test.go index 41753566c..c921fba03 100644 --- a/dynamic/provider_test.go +++ b/dynamic/provider_test.go @@ -234,6 +234,7 @@ func TestConfigure(t *testing.T) { }), }, noParallel, expect(autogold.Expect(`{ "acceptResources": true, + "supportsAutonamingConfiguration": true, "supportsPreview": true }`)))(t) diff --git a/pkg/pf/internal/defaults/defaults.go b/pkg/pf/internal/defaults/defaults.go index 1dde80e5d..628e928ed 100644 --- a/pkg/pf/internal/defaults/defaults.go +++ b/pkg/pf/internal/defaults/defaults.go @@ -153,6 +153,7 @@ func getDefaultValue( URN: cdOptions.URN, Properties: cdOptions.Properties, Seed: cdOptions.Seed, + Autonaming: cdOptions.Autonaming, }) if err != nil { msg := fmt.Errorf("Failed computing a default value for property '%s': %w", diff --git a/pkg/pf/internal/defaults/defaults_test.go b/pkg/pf/internal/defaults/defaults_test.go index db8e06c84..b95ce46eb 100644 --- a/pkg/pf/internal/defaults/defaults_test.go +++ b/pkg/pf/internal/defaults/defaults_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema" ) @@ -66,6 +67,10 @@ func TestApplyDefaultInfoValues(t *testing.T) { return resource.NewStringProperty(unique), err } + testFromAutoname := func(res *tfbridge.PulumiResource) (interface{}, error) { + return resource.NewStringProperty(res.Autonaming.ProposedName), nil + } + testComputeDefaults := func( t *testing.T, expectPriorValue resource.PropertyValue, @@ -238,6 +243,28 @@ func TestApplyDefaultInfoValues(t *testing.T) { "stringProp": resource.NewStringProperty("n1-453"), }, }, + { + name: "From function can compute defaults with autoname", + fieldInfos: map[string]*tfbridge.SchemaInfo{ + "string_prop": { + Default: &tfbridge.DefaultInfo{ + From: testFromAutoname, + }, + }, + }, + computeDefaultOptions: tfbridge.ComputeDefaultOptions{ + URN: "urn:pulumi:test::test::pkgA:index:t1::n1", + Properties: resource.PropertyMap{}, + Seed: []byte(`123`), + Autonaming: &info.ComputeDefaultAutonamingOptions{ + ProposedName: "n1-777", + Mode: info.ComputeDefaultAutonamingModePropose, + }, + }, + expected: resource.PropertyMap{ + "stringProp": resource.NewStringProperty("n1-777"), + }, + }, { name: "ComputeDefaults function can compute nested defaults", fieldInfos: map[string]*tfbridge.SchemaInfo{ diff --git a/pkg/pf/internal/plugin/provider_context.go b/pkg/pf/internal/plugin/provider_context.go index b38c539ae..bdb5db555 100644 --- a/pkg/pf/internal/plugin/provider_context.go +++ b/pkg/pf/internal/plugin/provider_context.go @@ -25,6 +25,8 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" ) // A version of Provider interface that is enhanced by giving access to the request Context. @@ -44,7 +46,8 @@ type ProviderWithContext interface { ConfigureWithContext(ctx context.Context, inputs resource.PropertyMap) error CheckWithContext(ctx context.Context, urn resource.URN, olds, news resource.PropertyMap, - allowUnknowns bool, randomSeed []byte) (resource.PropertyMap, []p.CheckFailure, error) + allowUnknowns bool, randomSeed []byte, autonaming *info.ComputeDefaultAutonamingOptions, + ) (resource.PropertyMap, []p.CheckFailure, error) DiffWithContext(ctx context.Context, urn resource.URN, id resource.ID, olds resource.PropertyMap, news resource.PropertyMap, allowUnknowns bool, ignoreChanges []string) (p.DiffResult, error) @@ -148,8 +151,15 @@ func (prov *provider) Configure( func (prov *provider) Check( ctx context.Context, req plugin.CheckRequest, ) (plugin.CheckResponse, error) { + var autonaming *info.ComputeDefaultAutonamingOptions + if req.Autonaming != nil { + autonaming = &info.ComputeDefaultAutonamingOptions{ + ProposedName: req.Autonaming.ProposedName, + Mode: info.ComputeDefaultAutonamingOptionsMode(req.Autonaming.Mode), + } + } c, f, err := prov.ProviderWithContext.CheckWithContext( - ctx, req.URN, req.Olds, req.News, req.AllowUnknowns, req.RandomSeed) + ctx, req.URN, req.Olds, req.News, req.AllowUnknowns, req.RandomSeed, autonaming) return plugin.CheckResponse{Properties: c, Failures: f}, err } diff --git a/pkg/pf/internal/plugin/provider_server.go b/pkg/pf/internal/plugin/provider_server.go index 3b2de812c..4580c5354 100644 --- a/pkg/pf/internal/plugin/provider_server.go +++ b/pkg/pf/internal/plugin/provider_server.go @@ -29,6 +29,8 @@ import ( pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" ) type providerServer struct { @@ -333,6 +335,10 @@ func (p *providerServer) Configure(ctx context.Context, // reason about data flow within the underlying provider (TF), we allow // the engine to apply its own heuristics. AcceptSecrets: false, + + // Check will accept a configuration property for engine to propose auto-naming format and mode + // when user opts in to control it. + SupportsAutonamingConfiguration: true, }, nil } @@ -349,7 +355,15 @@ func (p *providerServer) Check(ctx context.Context, req *pulumirpc.CheckRequest) return nil, err } - newInputs, failures, err := p.provider.CheckWithContext(ctx, urn, state, inputs, true, req.RandomSeed) + var autonaming *info.ComputeDefaultAutonamingOptions + if req.Autonaming != nil { + autonaming = &info.ComputeDefaultAutonamingOptions{ + ProposedName: req.Autonaming.ProposedName, + Mode: info.ComputeDefaultAutonamingOptionsMode(req.Autonaming.Mode), + } + } + + newInputs, failures, err := p.provider.CheckWithContext(ctx, urn, state, inputs, true, req.RandomSeed, autonaming) if err != nil { return nil, err } diff --git a/pkg/pf/tests/autonaming_test.go b/pkg/pf/tests/autonaming_test.go new file mode 100644 index 000000000..963192a32 --- /dev/null +++ b/pkg/pf/tests/autonaming_test.go @@ -0,0 +1,57 @@ +package tfbridgetests + +import ( + "testing" + + rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/pulumi/providertest/pulumitest/opttest" + "github.com/stretchr/testify/require" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tests/internal/providerbuilder" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tests/pulcheck" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" +) + +func TestAutonaming(t *testing.T) { + t.Parallel() + provBuilder := providerbuilder.NewProvider( + providerbuilder.NewProviderArgs{ + AllResources: []providerbuilder.Resource{ + providerbuilder.NewResource(providerbuilder.NewResourceArgs{ + ResourceSchema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "name": rschema.StringAttribute{Optional: true}, + }, + }, + }), + }, + }) + + prov := bridgedProvider(provBuilder) + prov.Resources["testprovider_test"] = &tfbridge.ResourceInfo{ + Tok: "testprovider:index:Test", + Fields: map[string]*tfbridge.SchemaInfo{ + "name": tfbridge.AutoName("name", 50, "-"), + }, + } + program := ` +name: test +runtime: yaml +config: + pulumi:autonaming: + value: + pattern: ${project}-${name} +resources: + hello: + type: testprovider:index:Test +outputs: + testOut: ${hello.name} +` + opts := []opttest.Option{ + opttest.Env("PULUMI_EXPERIMENTAL", "true"), + } + pt, err := pulcheck.PulCheck(t, prov, program, opts...) + require.NoError(t, err) + res := pt.Up(t) + require.Equal(t, "test-hello", res.Outputs["testOut"].Value) +} diff --git a/pkg/pf/tests/provider_check_test.go b/pkg/pf/tests/provider_check_test.go index 30fc24d8d..87e12764c 100644 --- a/pkg/pf/tests/provider_check_test.go +++ b/pkg/pf/tests/provider_check_test.go @@ -190,7 +190,8 @@ func TestCheck(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true } }, { @@ -238,7 +239,8 @@ func TestCheck(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true } }, { diff --git a/pkg/pf/tests/provider_configure_test.go b/pkg/pf/tests/provider_configure_test.go index c294f2e66..a1602b2df 100644 --- a/pkg/pf/tests/provider_configure_test.go +++ b/pkg/pf/tests/provider_configure_test.go @@ -491,7 +491,8 @@ func TestConfigureToCreate(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true } }, { @@ -527,7 +528,8 @@ func TestConfigureBooleans(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true } }`) } @@ -610,7 +612,8 @@ func TestJSONNestedConfigure(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true } }`) } @@ -635,7 +638,8 @@ func TestJSONNestedConfigureWithSecrets(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true } }, { @@ -687,7 +691,8 @@ func TestConfigureWithSecrets(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true } }, { diff --git a/pkg/pf/tests/provider_read_test.go b/pkg/pf/tests/provider_read_test.go index 6012f9b91..0688fd5fa 100644 --- a/pkg/pf/tests/provider_read_test.go +++ b/pkg/pf/tests/provider_read_test.go @@ -57,7 +57,8 @@ func TestReadFromRefresh(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -433,7 +434,8 @@ func TestRefreshSupportsCustomID(t *testing.T) { }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-delete-preview.json b/pkg/pf/tests/testdata/genrandom/random-delete-preview.json index 6a7264150..78dac0ff5 100644 --- a/pkg/pf/tests/testdata/genrandom/random-delete-preview.json +++ b/pkg/pf/tests/testdata/genrandom/random-delete-preview.json @@ -328,7 +328,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-delete-update.json b/pkg/pf/tests/testdata/genrandom/random-delete-update.json index 5681bea1f..6bc378219 100644 --- a/pkg/pf/tests/testdata/genrandom/random-delete-update.json +++ b/pkg/pf/tests/testdata/genrandom/random-delete-update.json @@ -327,7 +327,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-empty-preview.json b/pkg/pf/tests/testdata/genrandom/random-empty-preview.json index 219e35808..c40350f99 100644 --- a/pkg/pf/tests/testdata/genrandom/random-empty-preview.json +++ b/pkg/pf/tests/testdata/genrandom/random-empty-preview.json @@ -373,7 +373,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-empty-update.json b/pkg/pf/tests/testdata/genrandom/random-empty-update.json index d78547677..65c4715f6 100644 --- a/pkg/pf/tests/testdata/genrandom/random-empty-update.json +++ b/pkg/pf/tests/testdata/genrandom/random-empty-update.json @@ -373,7 +373,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-initial-preview.json b/pkg/pf/tests/testdata/genrandom/random-initial-preview.json index 3cc612a36..51ff10f5e 100644 --- a/pkg/pf/tests/testdata/genrandom/random-initial-preview.json +++ b/pkg/pf/tests/testdata/genrandom/random-initial-preview.json @@ -348,7 +348,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-initial-update.json b/pkg/pf/tests/testdata/genrandom/random-initial-update.json index e0b41e54f..4065ec8ff 100644 --- a/pkg/pf/tests/testdata/genrandom/random-initial-update.json +++ b/pkg/pf/tests/testdata/genrandom/random-initial-update.json @@ -348,7 +348,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-replace-preview.json b/pkg/pf/tests/testdata/genrandom/random-replace-preview.json index d3f47b733..f8396d4ff 100644 --- a/pkg/pf/tests/testdata/genrandom/random-replace-preview.json +++ b/pkg/pf/tests/testdata/genrandom/random-replace-preview.json @@ -373,7 +373,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/genrandom/random-replace-update.json b/pkg/pf/tests/testdata/genrandom/random-replace-update.json index 705f91859..a1086188a 100644 --- a/pkg/pf/tests/testdata/genrandom/random-replace-update.json +++ b/pkg/pf/tests/testdata/genrandom/random-replace-update.json @@ -373,7 +373,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tests/testdata/updateprogram.json b/pkg/pf/tests/testdata/updateprogram.json index 580db34cb..84436a6ad 100644 --- a/pkg/pf/tests/testdata/updateprogram.json +++ b/pkg/pf/tests/testdata/updateprogram.json @@ -124,7 +124,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -376,7 +377,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -558,7 +560,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -844,7 +847,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -1099,7 +1103,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -1200,7 +1205,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -1536,7 +1542,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", @@ -1839,7 +1846,8 @@ }, "response": { "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }, "metadata": { "kind": "resource", diff --git a/pkg/pf/tfbridge/provider_check.go b/pkg/pf/tfbridge/provider_check.go index 77cae635a..d6334a9bb 100644 --- a/pkg/pf/tfbridge/provider_check.go +++ b/pkg/pf/tfbridge/provider_check.go @@ -26,6 +26,7 @@ import ( "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/defaults" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" ) // Check validates the given resource inputs from the user program and computes checked inputs that fill out default @@ -37,6 +38,7 @@ func (p *provider) CheckWithContext( inputs resource.PropertyMap, allowUnknowns bool, randomSeed []byte, + autonaming *info.ComputeDefaultAutonamingOptions, ) (resource.PropertyMap, []plugin.CheckFailure, error) { ctx = p.initLogging(ctx, p.logSink, urn) @@ -71,6 +73,7 @@ func (p *provider) CheckWithContext( Properties: checkedInputs, Seed: randomSeed, PriorState: priorState, + Autonaming: autonaming, }, PropertyMap: checkedInputs, ProviderConfig: p.lastKnownProviderConfig, diff --git a/pkg/tests/autonaming_test.go b/pkg/tests/autonaming_test.go new file mode 100644 index 000000000..7e8d8f38a --- /dev/null +++ b/pkg/tests/autonaming_test.go @@ -0,0 +1,53 @@ +package tests + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/providertest/pulumitest/opttest" + "github.com/stretchr/testify/require" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/pulcheck" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" +) + +func TestAutonaming(t *testing.T) { + t.Parallel() + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + bridgedProvider.Resources["prov_test"] = &tfbridge.ResourceInfo{ + Tok: "prov:index:Test", + Fields: map[string]*tfbridge.SchemaInfo{ + "name": tfbridge.AutoName("name", 50, "-"), + }, + } + program := ` +name: test +runtime: yaml +config: + pulumi:autonaming: + value: + pattern: ${name}-world +resources: + hello: + type: prov:index:Test +outputs: + testOut: ${hello.name} +` + opts := []opttest.Option{ + opttest.Env("PULUMI_EXPERIMENTAL", "true"), + } + pt := pulcheck.PulCheck(t, bridgedProvider, program, opts...) + res := pt.Up(t) + require.Equal(t, "hello-world", res.Outputs["testOut"].Value) +} diff --git a/pkg/tfbridge/info/autonaming.go b/pkg/tfbridge/info/autonaming.go index 5aa2ab395..370f7bfc8 100644 --- a/pkg/tfbridge/info/autonaming.go +++ b/pkg/tfbridge/info/autonaming.go @@ -17,6 +17,7 @@ package info import ( "context" "fmt" + "strings" "github.com/golang/glog" "github.com/pkg/errors" @@ -170,7 +171,45 @@ func ComputeAutoNameDefault( if options.Transform != nil { vs = options.Transform(vs) } - if options.Randlen > 0 { + if defaultOptions.Autonaming != nil { + switch defaultOptions.Autonaming.Mode { + case ComputeDefaultAutonamingModePropose: + // In propose mode, we can use the proposed name as a suggestion + vs = defaultOptions.Autonaming.ProposedName + if options.Transform != nil { + vs = options.Transform(vs) + } + // Apply maxlen constraint if specified + if options.Maxlen > 0 && len(vs) > options.Maxlen { + return nil, fmt.Errorf("calculated name '%s' exceeds maximum length of %d", vs, options.Maxlen) + } + // Apply charset constraint if specified + if len(options.Charset) > 0 { + charsetStr := string(options.Charset) + + // Replace separators that aren't in the valid charset + if !strings.ContainsRune(charsetStr, '-') { + vs = strings.ReplaceAll(vs, "-", options.Separator) + } + if !strings.ContainsRune(charsetStr, '_') { + vs = strings.ReplaceAll(vs, "_", options.Separator) + } + + for _, c := range vs { + if !strings.ContainsRune(charsetStr, c) { + return nil, fmt.Errorf("calculated name '%s' contains invalid character '%c' not in charset '%s'", + vs, c, charsetStr) + } + } + } + case ComputeDefaultAutonamingModeEnforce: + // In enforce mode, we must use exactly the proposed name, ignoring all resource options + return defaultOptions.Autonaming.ProposedName, nil + case ComputeDefaultAutonamingModeDisable: + // In disable mode, we should return an error if no explicit name was provided + return nil, fmt.Errorf("automatic naming is disabled but no explicit name was provided") + } + } else if options.Randlen > 0 { uniqueHex, err := resource.NewUniqueName( defaultOptions.Seed, vs+options.Separator, options.Randlen, options.Maxlen, options.Charset) if err != nil { @@ -183,6 +222,7 @@ func ComputeAutoNameDefault( URN: defaultOptions.URN, Properties: defaultOptions.Properties, Seed: defaultOptions.Seed, + Autonaming: defaultOptions.Autonaming, }, vs) } return vs, nil diff --git a/pkg/tfbridge/info/autonaming_test.go b/pkg/tfbridge/info/autonaming_test.go new file mode 100644 index 000000000..b4d70c13b --- /dev/null +++ b/pkg/tfbridge/info/autonaming_test.go @@ -0,0 +1,249 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package info + +import ( + "context" + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/stretchr/testify/assert" +) + +func TestComputeAutoNameDefault(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("basic", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{}, opts) + assert.NoError(t, err) + assert.Equal(t, "name", result) + }) + + t.Run("with separator and random suffix", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Separator: "-", + Randlen: 4, + }, opts) + assert.NoError(t, err) + assert.Regexp(t, "^name-[0-9a-f]{4}$", result) + }) + + t.Run("respects prior state", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + PriorState: resource.PropertyMap{ + "name": resource.NewStringProperty("existing-name"), + }, + PriorValue: resource.NewStringProperty("existing-name"), + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{}, opts) + assert.NoError(t, err) + assert.Equal(t, "existing-name", result) + }) + + t.Run("propose mode", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "proposed-name", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{}, opts) + assert.NoError(t, err) + assert.Equal(t, "proposed-name", result) + }) + + t.Run("propose mode with transform", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "proposed-name", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Transform: func(s string) string { + return s + "-transformed" + }, + }, opts) + assert.NoError(t, err) + assert.Equal(t, "proposed-name-transformed", result) + }) + + t.Run("propose mode with maxlen", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "this-is-a-very-long-proposed-name", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + _, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Maxlen: 10, + }, opts) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum length") + }) + + t.Run("propose mode with charset", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "name-123", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + _, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Charset: []rune("abcdefghijklmnopqrstuvwxyz-"), + }, opts) + assert.Error(t, err) + assert.Contains(t, err.Error(), "contains invalid character") + }) + + t.Run("propose mode ignores separator if no charset specified", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "name-with-dashes", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Separator: "_", + }, opts) + assert.NoError(t, err) + assert.Equal(t, "name-with-dashes", result) + }) + + t.Run("propose mode with separator replacement and charset", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "name-with_mixed-separators", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Separator: ".", + Charset: []rune("abcdefghijklmnopqrstuvwxyz."), + }, opts) + assert.NoError(t, err) + assert.Equal(t, "name.with.mixed.separators", result) + }) + + t.Run("propose mode with separator in charset", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "name-with-dashes", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Separator: "-", + Charset: []rune("abcdefghijklmnopqrstuvwxyz-"), + }, opts) + assert.NoError(t, err) + // Should preserve dashes since they're in the charset + assert.Equal(t, "name-with-dashes", result) + }) + + t.Run("propose mode with mixed separators and partial charset", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "name-with_mixed-separators", + Mode: ComputeDefaultAutonamingModePropose, + }, + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + Separator: "+", + // Only include - in charset, _ should still be replaced + Charset: []rune("abcdefghijklmnopqrstuvwxyz+-"), + }, opts) + assert.NoError(t, err) + // Should preserve - but replace _ with + + assert.Equal(t, "name-with+mixed-separators", result) + }) + + t.Run("enforce mode", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "proposed-name", + Mode: ComputeDefaultAutonamingModeEnforce, + }, + } + + result, err := ComputeAutoNameDefault(ctx, AutoNameOptions{ + // All of these options are ignored by design when mode is enforce. + Transform: func(s string) string { + return s + "-transformed" + }, + PostTransform: func(res *PulumiResource, s string) (string, error) { + return s + "-posttransformed", nil + }, + Maxlen: 5, + Charset: []rune("abc"), + Separator: "_", + }, opts) + assert.NoError(t, err) + // In enforce mode, the transform should be ignored and proposed name used exactly + assert.Equal(t, "proposed-name", result) + }) + + t.Run("disable mode", func(t *testing.T) { + opts := ComputeDefaultOptions{ + URN: resource.URN("urn:pulumi:stack::project::type::name"), + Seed: []byte("test-seed"), + Autonaming: &ComputeDefaultAutonamingOptions{ + ProposedName: "proposed-name", + Mode: ComputeDefaultAutonamingModeDisable, + }, + } + + _, err := ComputeAutoNameDefault(ctx, AutoNameOptions{}, opts) + assert.Error(t, err) + assert.Contains(t, err.Error(), "automatic naming is disabled") + }) +} diff --git a/pkg/tfbridge/info/info.go b/pkg/tfbridge/info/info.go index 26037c941..5c3ebdaa1 100644 --- a/pkg/tfbridge/info/info.go +++ b/pkg/tfbridge/info/info.go @@ -666,6 +666,29 @@ type Default struct { EnvVars []string } +// ComputeDefaultAutonamingOptionsMode is the mode that controls how the provider handles the proposed name. If not +// specified, defaults to `Propose`. +type ComputeDefaultAutonamingOptionsMode int32 + +const ( + // ComputeDefaultAutonamingModePropose means the provider may use the proposed name as a suggestion but is free + // to modify it. + ComputeDefaultAutonamingModePropose ComputeDefaultAutonamingOptionsMode = iota + // ComputeDefaultAutonamingModeEnforce means the provider must use exactly the proposed name (if present) + // or return an error if the proposed name is invalid. + ComputeDefaultAutonamingModeEnforce ComputeDefaultAutonamingOptionsMode = 1 + // ComputeDefaultAutonamingModeDisable means the provider should disable automatic naming and return an error + // if no explicit name is provided by user's program. + ComputeDefaultAutonamingModeDisable ComputeDefaultAutonamingOptionsMode = 2 +) + +// ComputeDefaultAutonamingOptions controls how auto-naming behaves when the engine provides explicit naming +// preferences. This is used by the engine to pass user preference for naming patterns. +type ComputeDefaultAutonamingOptions struct { + ProposedName string + Mode ComputeDefaultAutonamingOptionsMode +} + // Configures [Default.ComputeDefault]. type ComputeDefaultOptions struct { // URN identifying the Resource. Set when computing default properties for a Resource, and unset for functions. @@ -685,6 +708,9 @@ type ComputeDefaultOptions struct { // example, that random values generated across "pulumi preview" and "pulumi up" in the same deployment are // consistent. This currently is only available for resource changes. Seed []byte + + // The engine can provide auto-naming options if the user configured an explicit preference for it. + Autonaming *ComputeDefaultAutonamingOptions } // PulumiResource is just a little bundle that carries URN, seed and properties around. @@ -692,6 +718,7 @@ type PulumiResource struct { URN resource.URN Properties resource.PropertyMap Seed []byte + Autonaming *ComputeDefaultAutonamingOptions } // Overlay contains optional overlay information. Each info has a 1:1 correspondence with a module and diff --git a/pkg/tfbridge/names.go b/pkg/tfbridge/names.go index 66f4996df..7ebc4b155 100644 --- a/pkg/tfbridge/names.go +++ b/pkg/tfbridge/names.go @@ -309,6 +309,7 @@ func FromName(options AutoNameOptions) func(res *PulumiResource) (interface{}, e URN: res.URN, Properties: res.Properties, Seed: res.Seed, + Autonaming: res.Autonaming, }) } } diff --git a/pkg/tfbridge/names_test.go b/pkg/tfbridge/names_test.go index a38e4c542..37c65ae34 100644 --- a/pkg/tfbridge/names_test.go +++ b/pkg/tfbridge/names_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" shimv1 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v1" shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" ) @@ -154,6 +155,32 @@ func TestFromName(t *testing.T) { assert.True(t, strings.HasSuffix(out1.(string), ".fifo")) } +func TestFromNameSeedAndAutonaming(t *testing.T) { + t.Parallel() + res := &PulumiResource{ + URN: "urn:pulumi:test::test::pkgA:index:t1::n1", + Properties: resource.PropertyMap{}, + Seed: []byte("test-seed"), + Autonaming: &info.ComputeDefaultAutonamingOptions{ + ProposedName: "proposed-name", + Mode: info.ComputeDefaultAutonamingModePropose, + }, + } + + f := FromName(AutoNameOptions{ + Separator: "-", + Maxlen: 80, + Randlen: 7, + }) + + out, err := f(res) + assert.NoError(t, err) + + // Verify the output is a string and has expected format + outStr := out.(string) + assert.Equal(t, "proposed-name", outStr) +} + func TestBijectiveNameConversion(t *testing.T) { t.Parallel() diff --git a/pkg/tfbridge/provider.go b/pkg/tfbridge/provider.go index 50593a885..42e8b33ac 100644 --- a/pkg/tfbridge/provider.go +++ b/pkg/tfbridge/provider.go @@ -926,7 +926,8 @@ func (p *Provider) Configure(ctx context.Context, } return &pulumirpc.ConfigureResponse{ - SupportsPreview: true, + SupportsPreview: true, + SupportsAutonamingConfiguration: true, }, nil } @@ -1015,9 +1016,17 @@ func (p *Provider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (*pul } } + var autonaming *info.ComputeDefaultAutonamingOptions + if req.Autonaming != nil { + autonaming = &info.ComputeDefaultAutonamingOptions{ + ProposedName: req.Autonaming.ProposedName, + Mode: info.ComputeDefaultAutonamingOptionsMode(req.Autonaming.Mode), + } + } + tfname := res.TFName inputs, _, err := makeTerraformInputsWithOptions(ctx, - &PulumiResource{URN: urn, Properties: news, Seed: req.RandomSeed}, + &PulumiResource{URN: urn, Properties: news, Seed: req.RandomSeed, Autonaming: autonaming}, p.configValues, olds, news, schemaMap, res.Schema.Fields, makeTerraformInputsOptions{DisableTFDefaults: true, UnknownCollectionsSupported: p.tf.SupportsUnknownCollections()}) if err != nil { @@ -1036,7 +1045,7 @@ func (p *Provider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (*pul // Now re-generate the inputs WITH the TF defaults inputs, assets, err := makeTerraformInputsWithOptions(ctx, - &PulumiResource{URN: urn, Properties: news, Seed: req.RandomSeed}, + &PulumiResource{URN: urn, Properties: news, Seed: req.RandomSeed, Autonaming: autonaming}, p.configValues, olds, news, schemaMap, res.Schema.Fields, makeTerraformInputsOptions{UnknownCollectionsSupported: p.tf.SupportsUnknownCollections()}) if err != nil { diff --git a/pkg/tfbridge/provider_test.go b/pkg/tfbridge/provider_test.go index d52ca61dc..38b903b87 100644 --- a/pkg/tfbridge/provider_test.go +++ b/pkg/tfbridge/provider_test.go @@ -799,6 +799,54 @@ func TestProviderPreviewV2(t *testing.T) { }).DeepEquals(outs["nestedResources"])) } +func TestProviderCheckWithAutonaming(t *testing.T) { + t.Parallel() + provider := &Provider{ + tf: shimv2.NewProvider(testTFProviderV2), + config: shimv2.NewSchemaMap(testTFProviderV2.Schema), + } + provider.resources = map[tokens.Type]Resource{ + "ExampleResource": { + TF: shimv1.NewResource(testTFProvider.ResourcesMap["example_resource"]), + TFName: "example_resource", + Schema: &ResourceInfo{ + Tok: "ExampleResource", + Fields: map[string]*SchemaInfo{ + "string_property_value": AutoNameWithCustomOptions("string_property_value", AutoNameOptions{ + Separator: "-", + Maxlen: 50, + Randlen: 8, + }), + }, + }, + }, + } + urn := resource.NewURN("stack", "project", "", "ExampleResource", "name") + + pulumiIns, err := plugin.MarshalProperties(resource.PropertyMap{ + "arrayPropertyValues": resource.NewArrayProperty([]resource.PropertyValue{resource.NewStringProperty("foo")}), + }, plugin.MarshalOptions{KeepUnknowns: true}) + assert.NoError(t, err) + checkResp, err := provider.Check(context.Background(), &pulumirpc.CheckRequest{ + Urn: string(urn), + News: pulumiIns, + Autonaming: &pulumirpc.CheckRequest_AutonamingOptions{ + ProposedName: "this-name-please", + Mode: pulumirpc.CheckRequest_AutonamingOptions_ENFORCE, + }, + }) + + require.NoError(t, err) + require.NotNil(t, checkResp) + require.Empty(t, checkResp.Failures) + ins, err := plugin.UnmarshalProperties(checkResp.GetInputs(), plugin.MarshalOptions{}) + require.NoError(t, err) + name := ins["string_property_value"] + require.True(t, name.IsString()) + require.Equal(t, "this-name-please", name.StringValue()) + _ = name +} + func testCheckFailures(t *testing.T, provider *Provider, typeName tokens.Type) []*pulumirpc.CheckFailure { urn := resource.NewURN("stack", "project", "", typeName, "name") unknown := resource.MakeComputed(resource.NewStringProperty("")) @@ -896,7 +944,8 @@ func TestCheckCallback(t *testing.T) { } }, "response": { - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true } }, { @@ -1932,7 +1981,8 @@ func TestConfigure(t *testing.T) { "acceptResources": true }, "response": { - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true } }`) }) @@ -3668,7 +3718,8 @@ func TestMaxItemsOneConflictsWith(t *testing.T) { "variables": {} }, "response": { - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true } }, { @@ -3701,7 +3752,8 @@ func TestMaxItemsOneConflictsWith(t *testing.T) { "variables": {} }, "response": { - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true } }, { @@ -3767,7 +3819,8 @@ func TestMinMaxItemsOneOptional(t *testing.T) { "variables": {} }, "response": { - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true } }, { @@ -3798,7 +3851,8 @@ func TestMinMaxItemsOneOptional(t *testing.T) { "variables": {} }, "response": { - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true } }, { @@ -3872,7 +3926,8 @@ func TestComputedMaxItemsOneNotSpecified(t *testing.T) { "variables": {} }, "response": { - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true } }, { diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go index 2ce0ce7ae..ec5cb38c0 100644 --- a/pkg/tfbridge/schema.go +++ b/pkg/tfbridge/schema.go @@ -313,6 +313,7 @@ func makeTerraformInputsWithOptions( PriorState: olds, Properties: instance.Properties, Seed: instance.Seed, + Autonaming: instance.Autonaming, URN: instance.URN, } } @@ -849,6 +850,7 @@ func (ctx *conversionContext) applyDefaults( URN: ctx.ComputeDefaultOptions.URN, Properties: ctx.ComputeDefaultOptions.Properties, Seed: ctx.ComputeDefaultOptions.Seed, + Autonaming: ctx.ComputeDefaultOptions.Autonaming, }) if err != nil { return err diff --git a/pkg/x/muxer/muxer.go b/pkg/x/muxer/muxer.go index bdf760952..d8db6efbe 100644 --- a/pkg/x/muxer/muxer.go +++ b/pkg/x/muxer/muxer.go @@ -300,10 +300,11 @@ func (m *muxer) Configure(ctx context.Context, req *pulumirpc.ConfigureRequest) } } response := &pulumirpc.ConfigureResponse{ - AcceptSecrets: true, - SupportsPreview: true, - AcceptResources: true, - AcceptOutputs: true, + AcceptSecrets: true, + SupportsPreview: true, + AcceptResources: true, + AcceptOutputs: true, + SupportsAutonamingConfiguration: true, } errs := new(multierror.Error) for _, r := range asyncJoin(subs) { @@ -315,6 +316,8 @@ func (m *muxer) Configure(ctx context.Context, req *pulumirpc.ConfigureRequest) response.AcceptResources = response.AcceptResources && r.A.GetAcceptResources() response.AcceptSecrets = response.AcceptSecrets && r.A.GetAcceptSecrets() response.SupportsPreview = response.SupportsPreview && r.A.GetSupportsPreview() + response.SupportsAutonamingConfiguration = response.SupportsAutonamingConfiguration && + r.A.GetSupportsAutonamingConfiguration() } return response, m.muxedErrors(errs) } diff --git a/pkg/x/muxer/tests/muxer_test.go b/pkg/x/muxer/tests/muxer_test.go index ba2edce7e..67de0eecf 100644 --- a/pkg/x/muxer/tests/muxer_test.go +++ b/pkg/x/muxer/tests/muxer_test.go @@ -136,7 +136,8 @@ func TestConfigure(t *testing.T) { "c": "3" } }`, `{ - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true }`, nil, part(0, `{ "args": { @@ -146,7 +147,8 @@ func TestConfigure(t *testing.T) { } }`, `{ "acceptSecrets": true, - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true }`, nil), part(1, `{ "args": { @@ -156,7 +158,8 @@ func TestConfigure(t *testing.T) { } }`, `{ "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }`, nil), )) } diff --git a/x/muxer/tests/muxer_test.go b/x/muxer/tests/muxer_test.go index 64f6de92c..47545e1c9 100644 --- a/x/muxer/tests/muxer_test.go +++ b/x/muxer/tests/muxer_test.go @@ -35,7 +35,7 @@ import ( ) func TestSimpleDispatch(t *testing.T) { - t.Parallel() + t.Parallel() var m muxer.DispatchTable m.Resources = map[string]int{ "test:mod:A": 0, @@ -70,7 +70,7 @@ func TestSimpleDispatch(t *testing.T) { } func TestCheckConfigErrorNotDuplicated(t *testing.T) { - t.Parallel() + t.Parallel() var m muxer.DispatchTable m.Resources = map[string]int{ "test:mod:A": 0, @@ -85,7 +85,7 @@ func TestCheckConfigErrorNotDuplicated(t *testing.T) { } func TestCheckConfigDifferentErrorsNotDropped(t *testing.T) { - t.Parallel() + t.Parallel() var m muxer.DispatchTable m.Resources = map[string]int{ "test:mod:A": 0, @@ -106,7 +106,7 @@ func TestCheckConfigDifferentErrorsNotDropped(t *testing.T) { } func TestCheckConfigOneErrorReturned(t *testing.T) { - t.Parallel() + t.Parallel() var m muxer.DispatchTable m.Resources = map[string]int{ "test:mod:A": 0, @@ -121,7 +121,7 @@ func TestCheckConfigOneErrorReturned(t *testing.T) { } func TestConfigure(t *testing.T) { - t.Parallel() + t.Parallel() var m muxer.DispatchTable m.Resources = map[string]int{ "test:mod:A": 0, @@ -136,7 +136,8 @@ func TestConfigure(t *testing.T) { "c": "3" } }`, `{ - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true }`, nil, part(0, `{ "args": { @@ -146,7 +147,8 @@ func TestConfigure(t *testing.T) { } }`, `{ "acceptSecrets": true, - "supportsPreview": true + "supportsPreview": true, + "supportsAutonamingConfiguration": true }`, nil), part(1, `{ "args": { @@ -156,13 +158,14 @@ func TestConfigure(t *testing.T) { } }`, `{ "supportsPreview": true, - "acceptResources": true + "acceptResources": true, + "supportsAutonamingConfiguration": true }`, nil), )) } func TestDivergentCheckConfig(t *testing.T) { - t.Parallel() + t.Parallel() // Early versions of muxer failed hard on divergent responses from CheckConfig. This test ensures that it can // tolerate such responses (with logging or warning). The practical case is divergent handling of secret markers // where pf and v3 based providers respond with the same value but do not agree on the secret markers. @@ -203,7 +206,7 @@ func TestDivergentCheckConfig(t *testing.T) { } func TestGetMapping(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("single-responding-server", func(t *testing.T) { var m muxer.DispatchTable m.Resources = map[string]int{