Skip to content

Commit

Permalink
Autonaming configuration in Configure and Check
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhailshilkov committed Nov 30, 2024
1 parent d7eacfb commit 7ca3905
Show file tree
Hide file tree
Showing 11 changed files with 396 additions and 8 deletions.
1 change: 1 addition & 0 deletions pkg/pf/internal/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions pkg/pf/internal/defaults/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.ModePropose,
},
},
expected: resource.PropertyMap{
"stringProp": resource.NewStringProperty("n1-777"),
},
},
{
name: "ComputeDefaults function can compute nested defaults",
fieldInfos: map[string]*tfbridge.SchemaInfo{
Expand Down
4 changes: 4 additions & 0 deletions pkg/pf/internal/plugin/provider_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,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
}

Expand Down
42 changes: 41 additions & 1 deletion pkg/tfbridge/info/autonaming.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package info
import (
"context"
"fmt"
"strings"

"github.com/golang/glog"
"github.com/pkg/errors"
Expand Down Expand Up @@ -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 ModePropose:
// 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 ModeEnforce:
// In enforce mode, we must use exactly the proposed name, ignoring all resource options
return defaultOptions.Autonaming.ProposedName, nil
case ModeDisable:
// 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 {
Expand All @@ -183,6 +222,7 @@ func ComputeAutoNameDefault(
URN: defaultOptions.URN,
Properties: defaultOptions.Properties,
Seed: defaultOptions.Seed,
Autonaming: defaultOptions.Autonaming,
}, vs)
}
return vs, nil
Expand Down
249 changes: 249 additions & 0 deletions pkg/tfbridge/info/autonaming_test.go
Original file line number Diff line number Diff line change
@@ -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: ModePropose,
},
}

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: ModePropose,
},
}

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: ModePropose,
},
}

_, 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: ModePropose,
},
}

_, 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: ModePropose,
},
}

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: ModePropose,
},
}

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: ModePropose,
},
}

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: ModePropose,
},
}

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: ModeEnforce,
},
}

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: ModeDisable,
},
}

_, err := ComputeAutoNameDefault(ctx, AutoNameOptions{}, opts)
assert.Error(t, err)
assert.Contains(t, err.Error(), "automatic naming is disabled")
})
}
Loading

0 comments on commit 7ca3905

Please sign in to comment.