diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a363aefc4..7c71ff1ad 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,6 +8,9 @@ on: required: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Note this needs to match the shard input to the test matrix below as well as pattern in exclude. + # see jobs.test.strategy.matrix.{shard,exclude} + TOTAL_SHARDS: 15 jobs: test: @@ -21,6 +24,72 @@ jobs: go-version: [1.22.x, 1.23.x] platform: [ubuntu-latest, macos-latest, windows-latest] feature-flags: ["DEFAULT", "PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW"] + # Needs to match TOTAL_SHARDS + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + exclude: + # We do not run non-default feature flags on non-ubuntu. + - platform: windows-latest + feature-flags: "PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW" + - platform: macos-latest + feature-flags: "PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW" + # Windows and mac test runs do not need to be sharded as they are fast enough. + # In order to do that we will skip all except the 0-th shard. + - platform: windows-latest + shard: 1 + - platform: windows-latest + shard: 2 + - platform: windows-latest + shard: 3 + - platform: windows-latest + shard: 4 + - platform: windows-latest + shard: 5 + - platform: windows-latest + shard: 6 + - platform: windows-latest + shard: 7 + - platform: windows-latest + shard: 8 + - platform: windows-latest + shard: 9 + - platform: windows-latest + shard: 10 + - platform: windows-latest + shard: 11 + - platform: windows-latest + shard: 12 + - platform: windows-latest + shard: 13 + - platform: windows-latest + shard: 14 + - platform: macos-latest + shard: 1 + - platform: macos-latest + shard: 2 + - platform: macos-latest + shard: 3 + - platform: macos-latest + shard: 4 + - platform: macos-latest + shard: 5 + - platform: macos-latest + shard: 6 + - platform: macos-latest + shard: 7 + - platform: macos-latest + shard: 8 + - platform: macos-latest + shard: 9 + - platform: macos-latest + shard: 10 + - platform: macos-latest + shard: 11 + - platform: macos-latest + shard: 12 + - platform: macos-latest + shard: 13 + - platform: macos-latest + shard: 14 runs-on: ${{ matrix.platform }} steps: - name: Install pulumi @@ -39,16 +108,19 @@ jobs: go-version: ${{ matrix.go-version }} cache-dependency-path: | **/go.sum + # disable caching on windows because it's very slow + # see https://github.com/actions/setup-go/issues/495 + cache: ${{ matrix.platform != 'windows-latest' }} - name: export feature flags run: echo ${{ matrix.feature-flags }}=true >> $GITHUB_ENV if: ${{ matrix.platform != 'windows-latest' && matrix.feature-flags != 'DEFAULT' }} - - name: export feature flags - run: echo ${{ matrix.feature-flags }}=true >> $env:GITHUB_ENV - if: ${{ matrix.platform == 'windows-latest' && matrix.feature-flags != 'DEFAULT' }} - name: Build run: make build - name: Build PF run: cd pkg/pf && make build + - name: Shard tests + run: echo "RUN_TEST_CMD=$(go run github.com/pulumi/shard@5b6297aaffa0c06291fb8231968d7a9f4e6832e6 --total ${{ env.TOTAL_SHARDS }} --index ${{ matrix.shard }} --seed 314)" >> $GITHUB_ENV + if: ${{ matrix.platform == 'ubuntu-latest' }} - name: Test run: make test - name: Upload coverage reports to Codecov @@ -78,3 +150,25 @@ jobs: version: v1.62 - name: Lint run: make lint + sentinel: + name: sentinel + if: github.event_name == 'repository_dispatch' || + github.event.pull_request.head.repo.full_name == github.repository + permissions: + statuses: write + needs: + - test + - lint + runs-on: ubuntu-latest + steps: + - uses: guibranco/github-status-action-v2@0849440ec82c5fa69b2377725b9b7852a3977e76 # v1.1.13 + with: + authToken: ${{secrets.GITHUB_TOKEN}} + # Write an explicit status check called "Sentinel" which will only pass if this code really runs. + # This should always be a required check for PRs. + context: 'Sentinel' + description: 'All required checks passed' + state: 'success' + # Write to the PR commit SHA if it's available as we don't want the merge commit sha, + # otherwise use the current SHA for any other type of build. + sha: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/Makefile b/Makefile index d54e5c5ec..e475307c1 100644 --- a/Makefile +++ b/Makefile @@ -27,11 +27,12 @@ lint: lint_fix: go run scripts/build.go fix-lint +RUN_TEST_CMD ?= ./... test:: install_plugins @mkdir -p bin go build -o bin ./internal/testing/pulumi-terraform-bridge-test-provider PULUMI_TERRAFORM_BRIDGE_TEST_PROVIDER=$(shell pwd)/bin/pulumi-terraform-bridge-test-provider \ - go test -count=1 -coverprofile="coverage.txt" -coverpkg=./... -timeout 2h -parallel ${TESTPARALLELISM} ./... + go test -count=1 -coverprofile="coverage.txt" -coverpkg=./... -timeout 2h -parallel ${TESTPARALLELISM} $(value RUN_TEST_CMD) # Run tests while accepting current output as expected output "golden" # tests. In case where system behavior changes intentionally this can diff --git a/pkg/internal/tests/pulcheck/pulcheck.go b/pkg/internal/tests/pulcheck/pulcheck.go index 66dd04fb9..bf2adbb5b 100644 --- a/pkg/internal/tests/pulcheck/pulcheck.go +++ b/pkg/internal/tests/pulcheck/pulcheck.go @@ -52,8 +52,39 @@ func resourceNeedsUpdate(res *schema.Resource) bool { return false } +func copyMap[K comparable, V any](m map[K]V, cp func(V) V) map[K]V { + dst := make(map[K]V, len(m)) + for k, v := range m { + dst[k] = cp(v) + } + return dst +} + +// WithValidProvider returns a copy of tfp, with all required fields filled with default +// values. +// // This is an experimental API. -func EnsureProviderValid(t T, tfp *schema.Provider) { +func WithValidProvider(t T, tfp *schema.Provider) *schema.Provider { + if tfp == nil { + return nil + } + + // Copy tfp as deep as we will mutate. + { + dst := *tfp // memcopy + dst.ResourcesMap = copyMap(tfp.ResourcesMap, func(v *schema.Resource) *schema.Resource { + cp := *v // memcopy + cp.Schema = copyMap(cp.Schema, func(s *schema.Schema) *schema.Schema { + cp := *s + return &cp + }) + return &cp + }) + tfp = &dst + } + + // Now ensure that tfp is valid + for _, r := range tfp.ResourcesMap { if r.Schema["id"] == nil { r.Schema["id"] = &schema.Schema{ @@ -108,6 +139,8 @@ func EnsureProviderValid(t T, tfp *schema.Provider) { } } require.NoError(t, tfp.InternalValidate()) + + return tfp } func ProviderServerFromInfo( @@ -206,7 +239,7 @@ func BridgedProvider(t T, providerName string, tfp *schema.Provider, opts ...Bri opt(&options) } - EnsureProviderValid(t, tfp) + tfp = WithValidProvider(t, tfp) // If the PULUMI_ACCURATE_BRIDGE_PREVIEWS environment variable is set, use it to enable // accurate bridge previews. @@ -230,10 +263,8 @@ func BridgedProvider(t T, providerName string, tfp *schema.Provider, opts ...Bri EnableAccurateBridgePreview: accurateBridgePreviews, Config: options.configInfo, } - makeToken := func(module, name string) (string, error) { - return tokens.MakeStandard(providerName)(module, name) - } - provider.MustComputeTokens(tokens.SingleModule(providerName, "index", makeToken)) + provider.MustComputeTokens(tokens.SingleModule(providerName, + "index", tokens.MakeStandard(providerName))) return provider } diff --git a/pkg/tests/import_test.go b/pkg/tests/import_test.go index 4b356cea4..94a1fce11 100644 --- a/pkg/tests/import_test.go +++ b/pkg/tests/import_test.go @@ -16,54 +16,57 @@ import ( "gopkg.in/yaml.v3" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/pulcheck" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" ) func TestFullyComputedNestedAttribute(t *testing.T) { t.Parallel() - resMap := map[string]*schema.Resource{ - "prov_test": { - Schema: map[string]*schema.Schema{ - "attached_disks": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Optional: true, - Type: schema.TypeString, - }, - "key256": { - Computed: true, - Type: schema.TypeString, + + bridgedProvider := func(importVal any) info.Provider { + return pulcheck.BridgedProvider(t, "prov", &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "attached_disks": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Optional: true, + Type: schema.TypeString, + }, + "key256": { + Computed: true, + Type: schema.TypeString, + }, + }, }, }, + "top_level_computed": { + Type: schema.TypeString, + Computed: true, + }, }, - }, - "top_level_computed": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - } - - importer := func(val any) func(context.Context, *schema.ResourceData, interface{}) ([]*schema.ResourceData, error) { - return func(ctx context.Context, rd *schema.ResourceData, i interface{}) ([]*schema.ResourceData, error) { - elMap := map[string]any{ - "name": "disk1", - "key256": val, - } - err := rd.Set("attached_disks", []map[string]any{elMap}) - require.NoError(t, err) + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) ([]*schema.ResourceData, error) { + elMap := map[string]any{ + "name": "disk1", + "key256": importVal, + } + err := rd.Set("attached_disks", []map[string]any{elMap}) + require.NoError(t, err) - err = rd.Set("top_level_computed", "computed_val") - require.NoError(t, err) + err = rd.Set("top_level_computed", "computed_val") + require.NoError(t, err) - return []*schema.ResourceData{rd}, nil - } + return []*schema.ResourceData{rd}, nil + }, + }, + }, + }, + }) } - tfp := &schema.Provider{ResourcesMap: resMap} - bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) program := ` name: test @@ -83,9 +86,7 @@ runtime: yaml }, } { t.Run(tc.name, func(t *testing.T) { - resMap["prov_test"].Importer = &schema.ResourceImporter{ - StateContext: importer(tc.importVal), - } + bridgedProvider := bridgedProvider(tc.importVal) pt := pulcheck.PulCheck(t, bridgedProvider, program) diff --git a/pkg/tests/tfcheck/tfcheck.go b/pkg/tests/tfcheck/tfcheck.go index 2f18fb656..6efeefc09 100644 --- a/pkg/tests/tfcheck/tfcheck.go +++ b/pkg/tests/tfcheck/tfcheck.go @@ -76,7 +76,7 @@ func NewTfDriver(t pulcheck.T, dir, providerName string, prov any) *TFDriver { } func newTfDriverSDK(t pulcheck.T, dir, providerName string, prov *schema.Provider) *TFDriver { - pulcheck.EnsureProviderValid(t, prov) + prov = pulcheck.WithValidProvider(t, prov) v6server, err := tf5to6server.UpgradeServer(context.Background(), func() tfprotov5.ProviderServer { return prov.GRPCProvider() }) require.NoError(t, err) diff --git a/pkg/tfbridge/x/token_test.go b/pkg/tfbridge/x/token_test.go deleted file mode 100644 index 127367ad1..000000000 --- a/pkg/tfbridge/x/token_test.go +++ /dev/null @@ -1,944 +0,0 @@ -// Copyright 2016-2023, 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 x - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" - shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema" - "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/metadata" - md "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/metadata" -) - -func TestTokensSingleModule(t *testing.T) { - t.Parallel() - info := tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "foo_fizz_buzz": nil, - "foo_bar_hello_world": nil, - "foo_bar": nil, - }, - DataSourcesMap: schema.ResourceMap{ - "foo_source1": nil, - "foo_very_special_source": nil, - }, - }).Shim(), - } - - makeToken := func(module, name string) (string, error) { - return fmt.Sprintf("foo:%s:%s", module, name), nil - } - opts := TokensSingleModule("foo_", "index", makeToken) - err := ComputeDefaults(&info, opts) - require.NoError(t, err) - - expectedResources := map[string]*tfbridge.ResourceInfo{ - "foo_fizz_buzz": {Tok: "foo:index:FizzBuzz"}, - "foo_bar_hello_world": {Tok: "foo:index:BarHelloWorld"}, - "foo_bar": {Tok: "foo:index:Bar"}, - } - expectedDatasources := map[string]*tfbridge.DataSourceInfo{ - "foo_source1": {Tok: "foo:index:getSource1"}, - "foo_very_special_source": {Tok: "foo:index:getVerySpecialSource"}, - } - - assert.Equal(t, expectedResources, info.Resources) - assert.Equal(t, expectedDatasources, info.DataSources) - - // Now test that overrides still work - info.Resources = map[string]*tfbridge.ResourceInfo{ - "foo_bar_hello_world": {Tok: "foo:index:BarHelloPulumi"}, - } - err = ComputeDefaults(&info, DefaultStrategy{ - Resource: opts.Resource, - }) - require.NoError(t, err) - - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "foo_fizz_buzz": {Tok: "foo:index:FizzBuzz"}, - "foo_bar_hello_world": {Tok: "foo:index:BarHelloPulumi"}, - "foo_bar": {Tok: "foo:index:Bar"}, - }, info.Resources) -} - -func TestTokensKnownModules(t *testing.T) { - t.Parallel() - info := tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "cs101_fizz_buzz_one_five": nil, - "cs101_fizz_three": nil, - "cs101_fizz_three_six": nil, - "cs101_buzz_five": nil, - "cs101_buzz_ten": nil, - "cs101_game": nil, - }, - }).Shim(), - } - - err := ComputeDefaults(&info, DefaultStrategy{ - Resource: TokensKnownModules("cs101_", "index", []string{ - "fizz_", "buzz_", "fizz_buzz_", - }, func(module, name string) (string, error) { - return fmt.Sprintf("cs101:%s:%s", module, name), nil - }).Resource, - }) - require.NoError(t, err) - - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "cs101_fizz_buzz_one_five": {Tok: "cs101:fizzBuzz:OneFive"}, - "cs101_fizz_three": {Tok: "cs101:fizz:Three"}, - "cs101_fizz_three_six": {Tok: "cs101:fizz:ThreeSix"}, - "cs101_buzz_five": {Tok: "cs101:buzz:Five"}, - "cs101_buzz_ten": {Tok: "cs101:buzz:Ten"}, - "cs101_game": {Tok: "cs101:index:Game"}, - }, info.Resources) -} - -func TestTokensMappedModules(t *testing.T) { - t.Parallel() - info := tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "cs101_fizz_buzz_one_five": nil, - "cs101_fizz_three": nil, - "cs101_fizz_three_six": nil, - "cs101_buzz_five": nil, - "cs101_buzz_ten": nil, - "cs101_game": nil, - }, - }).Shim(), - } - err := ComputeDefaults(&info, DefaultStrategy{ - Resource: TokensMappedModules("cs101_", "idx", map[string]string{ - "fizz_": "fIzZ", - "buzz_": "buZZ", - "fizz_buzz_": "fizZBuzz", - }, func(module, name string) (string, error) { - return fmt.Sprintf("cs101:%s:%s", module, name), nil - }).Resource, - }) - require.NoError(t, err) - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "cs101_fizz_buzz_one_five": {Tok: "cs101:fizZBuzz:OneFive"}, - "cs101_fizz_three": {Tok: "cs101:fIzZ:Three"}, - "cs101_fizz_three_six": {Tok: "cs101:fIzZ:ThreeSix"}, - "cs101_buzz_five": {Tok: "cs101:buZZ:Five"}, - "cs101_buzz_ten": {Tok: "cs101:buZZ:Ten"}, - "cs101_game": {Tok: "cs101:idx:Game"}, - }, info.Resources) -} - -func TestUnmappable(t *testing.T) { - t.Parallel() - info := tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "cs101_fizz_buzz_one_five": nil, - "cs101_fizz_three": nil, - "cs101_fizz_three_six": nil, - "cs101_buzz_five": nil, - "cs101_buzz_ten": nil, - "cs101_game": nil, - }, - }).Shim(), - } - - strategy := TokensKnownModules("cs101_", "index", []string{ - "fizz_", "buzz_", "fizz_buzz_", - }, func(module, name string) (string, error) { - return fmt.Sprintf("cs101:%s:%s", module, name), nil - }) - strategy = strategy.Unmappable("five", "SomeGoodReason") - err := ComputeDefaults(&info, strategy) - assert.ErrorContains(t, err, "SomeGoodReason") - - // Override the unmappable resources - info.Resources = map[string]*tfbridge.ResourceInfo{ - // When "five" comes after another number, we print it as "5" - "cs101_fizz_buzz_one_five": {Tok: "cs101:fizzBuzz:One5"}, - "cs101_buzz_five": {Tok: "cs101:buzz:Five"}, - } - err = ComputeDefaults(&info, strategy) - assert.NoError(t, err) - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "cs101_fizz_buzz_one_five": {Tok: "cs101:fizzBuzz:One5"}, - "cs101_fizz_three": {Tok: "cs101:fizz:Three"}, - "cs101_fizz_three_six": {Tok: "cs101:fizz:ThreeSix"}, - "cs101_buzz_five": {Tok: "cs101:buzz:Five"}, - "cs101_buzz_ten": {Tok: "cs101:buzz:Ten"}, - "cs101_game": {Tok: "cs101:index:Game"}, - }, info.Resources) -} - -func TestIgnored(t *testing.T) { - t.Parallel() - info := tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "cs101_one_five": nil, - "cs101_three": nil, - "cs101_three_six": nil, - }, - }).Shim(), - IgnoreMappings: []string{"cs101_three"}, - } - err := ComputeDefaults(&info, TokensSingleModule("cs101_", "index_", MakeStandardToken("cs101"))) - assert.NoError(t, err) - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "cs101_one_five": {Tok: "cs101:index/oneFive:OneFive"}, - "cs101_three_six": {Tok: "cs101:index/threeSix:ThreeSix"}, - }, info.Resources) -} - -func TestTokensInferredModules(t *testing.T) { - t.Parallel() - tests := []struct { - name string - resourceMapping map[string]string - opts *InferredModulesOpts - }{ - { - name: "oci-example", - // Motivating example and explanation: - // - // The algorithm only has the list of token names to work off - // of. It doesn't know what modules should exist, so it needs to - // figure out. - // - // Tokens can be cleanly divided into segments at '_' - // boundaries. However, its unclear how many segments make up the - // module, and how many segments make up the name. - // - // Giving a concrete example, the algorithm needs to figure out - // what Pulumi token to give the Terraform token - // oci_apm_apm_domain: - // - // Dividing into segments, the algorithm has module [apm apm - // domain] and name [], written [apm apm domain]:[]. - // - // It starts by considering all token segments as part of the - // module name. Examining the module [apm apm domain], the - // algorithm notices that there are not enough objects in the - // [apm apm domain] module to satisfy MinimumModuleSize. It then - // downshifts the perspective token to [apm apm]:[domain]. - // - // The algorithm will process all tokens with modules that start - // with [apm apm $NEXT] for all $NEXT before it reconsiders the - // [apm apm] module. - // - // Next iteration, the algorithm considers [apm - // apm]:[domain]. Because the [apm apm] module has only 1 - // member, the algorithm downshifs [apm apm]:[domain] to - // [apm]:[apm domain]. - // - // Next iteration, the algorithm sees 2 different tokens within - // the [apm] module: [apm]:[apm domain] and [apm]:[sub - // domain]. Since 2 >= MinimumModuleSize, the algorithm - // finalizes both tokens into apm:ApmDomain and apm:SubDomain - // respectively. - // - // - // The process is unstable for insertion: if the user added - // "oci_apm_apm_thingy" resource, then there'd be two entries and - // it might decide oci_apm_apm is now a module. - resourceMapping: map[string]string{ - "oci_adm_knowledge_base": "index:AdmKnowledgeBase", - "oci_apm_apm_domain": "apm:ApmDomain", - "oci_apm_sub_domain": "apm:SubDomain", - - "oci_apm_config_config": "apmConfig:Config", - "oci_apm_config_user": "apmConfig:User", - - "oci_apm_synthetics_monitor": "apmSynthetics:Monitor", - "oci_apm_synthetics_script": "apmSynthetics:Script", - "oci_apm_synthetics_dedicated_vantage_point": "apmSynthetics:DedicatedVantagePoint", - }, - opts: &InferredModulesOpts{ - TfPkgPrefix: "oci_", - MinimumModuleSize: 2, - MimimumSubmoduleSize: 2, - }, - }, - { - name: "non-overlapping mapping", - resourceMapping: map[string]string{ - "pkg_foo_bar": "index:FooBar", - "pkg_fizz_buzz": "index:FizzBuzz", - "pkg_resource": "index:Resource", - "pkg_very_long_name": "index:VeryLongName", - "pkg_very_very_long_name": "index:VeryVeryLongName", - }, - }, - { - name: "detect a simple module", - resourceMapping: map[string]string{ - "pkg_hello_world": "hello:World", - "pkg_hello_pulumi": "hello:Pulumi", - "pkg_hello": "hello:Hello", - "pkg_goodbye_folks": "index:GoodbyeFolks", - "pkg_hi": "index:Hi", - }, - opts: &InferredModulesOpts{ - // We set MinimumModuleSize down to 3 to so we only need - // tree entries prefixed with `pkg_hello` to have a hello - // module created. - MinimumModuleSize: 3, - }, - }, - { - name: "nested modules", - resourceMapping: map[string]string{ - "pkg_mod_r1": "mod:R1", - "pkg_mod_r2": "mod:R2", - "pkg_mod_r3": "mod:R3", - "pkg_mod_r4": "mod:R4", - "pkg_mod_sub_r1": "modSub:R1", - "pkg_mod_sub_r2": "modSub:R2", - "pkg_mod_sub_r3": "modSub:R3", - "pkg_mod_sub_r4": "modSub:R4", - "pkg_mod_not_r1": "mod:NotR1", - "pkg_mod_not_r2": "mod:NotR2", - }, - opts: &InferredModulesOpts{ - TfPkgPrefix: "pkg_", - // We set the minimum module size to 4. This ensures that - // `pkg_mod` is picked up as a module. - MinimumModuleSize: 4, - // We set the MimimumSubmoduleSize to 3, ensuring that - // `pkg_mod_sub_*` is is given its own `modSub` module (4 - // elements), while `pkg_mod_not_*` is put in the `mod` - // module, since `pkg_mod_not` only has 2 elements. - MimimumSubmoduleSize: 3, - }, - }, - { - name: "nested-collapse", - resourceMapping: map[string]string{ - "pkg_mod_r1": "mod:R1", - "pkg_mod_r2": "mod:R2", - "pkg_mod_sub_r1": "mod:SubR1", - "pkg_mod_sub_r2": "mod:SubR2", - }, - opts: &InferredModulesOpts{ - TfPkgPrefix: "pkg_", - MinimumModuleSize: 4, - MimimumSubmoduleSize: 3, - }, - }, - { - name: "module and item", - resourceMapping: map[string]string{ - "pkg_mod": "mod:Mod", - "pkg_mod_r1": "mod:R1", - "pkg_mod_r2": "mod:R2", - "pkg_r1": "index:R1", - }, - opts: &InferredModulesOpts{ - MinimumModuleSize: 3, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - resources := schema.ResourceMap{} - for k := range tt.resourceMapping { - resources[k] = nil - } - info := &tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: resources, - }).Shim(), - } - - strategy, err := TokensInferredModules(info, - func(module, name string) (string, error) { return module + ":" + name, nil }, - tt.opts) - require.NoError(t, err) - err = ComputeDefaults(info, strategy) - require.NoError(t, err) - - mapping := map[string]string{} - for k, v := range info.Resources { - mapping[k] = v.Tok.String() - } - assert.Equal(t, tt.resourceMapping, mapping) - }) - } -} - -func TestTokenAliasing(t *testing.T) { - t.Parallel() - provider := func() *tfbridge.ProviderInfo { - return &tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "pkg_mod1_r1": nil, - "pkg_mod1_r2": nil, - "pkg_mod2_r1": nil, - }, - }).Shim(), - } - } - simple := provider() - - metadata, err := metadata.New(nil) - require.NoError(t, err) - - err = ComputeDefaults(simple, TokensSingleModule("pkg_", "index", MakeStandardToken("pkg"))) - require.NoError(t, err) - - err = AutoAliasing(simple, metadata) - require.NoError(t, err) - - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "pkg_mod1_r1": {Tok: "pkg:index/mod1R1:Mod1R1"}, - "pkg_mod1_r2": {Tok: "pkg:index/mod1R2:Mod1R2"}, - "pkg_mod2_r1": {Tok: "pkg:index/mod2R1:Mod2R1"}, - }, simple.Resources) - - modules := provider() - modules.Version = "1.0.0" - - knownModules := TokensKnownModules("pkg_", "", - []string{"mod1", "mod2"}, MakeStandardToken("pkg")) - - err = ComputeDefaults(modules, knownModules) - require.NoError(t, err) - - err = AutoAliasing(modules, metadata) - require.NoError(t, err) - - hist2 := md.Clone(metadata) - ref := func(s string) *string { return &s } - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "pkg_mod1_r1": { - Tok: "pkg:mod1/r1:R1", - Aliases: []tfbridge.AliasInfo{{Type: ref("pkg:index/mod1R1:Mod1R1")}}, - }, - "pkg_mod1_r1_legacy": { - Tok: "pkg:index/mod1R1:Mod1R1", - DeprecationMessage: "pkg.index/mod1r1.Mod1R1 has been deprecated in favor of pkg.mod1/r1.R1", - }, - "pkg_mod1_r2": { - Tok: "pkg:mod1/r2:R2", - Aliases: []tfbridge.AliasInfo{{Type: ref("pkg:index/mod1R2:Mod1R2")}}, - }, - "pkg_mod1_r2_legacy": { - Tok: "pkg:index/mod1R2:Mod1R2", - DeprecationMessage: "pkg.index/mod1r2.Mod1R2 has been deprecated in favor of pkg.mod1/r2.R2", - }, - "pkg_mod2_r1": { - Tok: "pkg:mod2/r1:R1", - Aliases: []tfbridge.AliasInfo{{Type: ref("pkg:index/mod2R1:Mod2R1")}}, - }, - "pkg_mod2_r1_legacy": { - Tok: "pkg:index/mod2R1:Mod2R1", - DeprecationMessage: "pkg.index/mod2r1.Mod2R1 has been deprecated in favor of pkg.mod2/r1.R1", - }, - }, modules.Resources) - - modules2 := provider() - modules2.Version = "1.0.0" - - err = ComputeDefaults(modules2, knownModules) - require.NoError(t, err) - - err = AutoAliasing(modules2, metadata) - require.NoError(t, err) - - hist3 := md.Clone(metadata) - assert.Equal(t, string(hist2.Marshal()), string(hist3.Marshal()), - "No changes should imply no change in history") - assert.Equal(t, modules, modules2) - - modules3 := provider() - modules3.Version = "100.0.0" - - err = ComputeDefaults(modules3, knownModules) - require.NoError(t, err) - - err = AutoAliasing(modules3, metadata) - require.NoError(t, err) - - // All hard aliases should be removed on a major version upgrade - assert.Equal(t, map[string]*tfbridge.ResourceInfo{ - "pkg_mod1_r1": { - Tok: "pkg:mod1/r1:R1", - Aliases: []tfbridge.AliasInfo{{Type: ref("pkg:index/mod1R1:Mod1R1")}}, - }, - "pkg_mod1_r2": { - Tok: "pkg:mod1/r2:R2", - Aliases: []tfbridge.AliasInfo{{Type: ref("pkg:index/mod1R2:Mod1R2")}}, - }, - "pkg_mod2_r1": { - Tok: "pkg:mod2/r1:R1", - Aliases: []tfbridge.AliasInfo{{Type: ref("pkg:index/mod2R1:Mod2R1")}}, - }, - }, modules3.Resources) - - // A provider with no version should assume the most recent major - // version in history – in this case, all aliases should be kept - modules4 := provider() - - err = ComputeDefaults(modules4, knownModules) - require.NoError(t, err) - - err = AutoAliasing(modules4, metadata) - require.NoError(t, err) - assert.Equal(t, modules.Resources, modules4.Resources) -} - -func TestMaxItemsOneAliasing(t *testing.T) { - t.Parallel() - provider := func(f1, f2 bool) *tfbridge.ProviderInfo { - prov := &tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "pkg_r1": (&schema.Resource{Schema: schema.SchemaMap{ - "f1": Schema{MaxItemsOne: f1}, - "f2": Schema{MaxItemsOne: f2}, - "f3": Schema{typ: shim.TypeString}, - }}).Shim(), - }, - }).Shim(), - } - err := ComputeDefaults(prov, TokensSingleModule("pkg_", "index", MakeStandardToken("pkg"))) - require.NoError(t, err) - return prov - } - info := provider(true, false) - metadata, err := metadata.New(nil) - require.NoError(t, err) - - // Save current state into metadata - err = AutoAliasing(info, metadata) - require.NoError(t, err) - - v := string(metadata.MarshalIndent()) - expected := `{ - "auto-aliasing": { - "resources": { - "pkg_r1": { - "current": "pkg:index/r1:R1", - "fields": { - "f1": { - "maxItemsOne": true - }, - "f2": { - "maxItemsOne": false - } - } - } - } - } -}` - assert.Equal(t, expected, v) - - info = provider(false, true) - - // Apply metadata back into the provider - err = AutoAliasing(info, metadata) - require.NoError(t, err) - - assert.True(t, *info.Resources["pkg_r1"].Fields["f1"].MaxItemsOne) - assert.False(t, *info.Resources["pkg_r1"].Fields["f2"].MaxItemsOne) - assert.Equal(t, expected, string(metadata.MarshalIndent())) - - // Apply metadata back into the provider again, making sure there isn't a diff - err = AutoAliasing(info, metadata) - require.NoError(t, err) - - assert.True(t, *info.Resources["pkg_r1"].Fields["f1"].MaxItemsOne) - assert.False(t, *info.Resources["pkg_r1"].Fields["f2"].MaxItemsOne) - assert.Equal(t, expected, string(metadata.MarshalIndent())) - - // Validate that overrides work - - info = provider(true, false) - info.Resources["pkg_r1"].Fields = map[string]*tfbridge.SchemaInfo{ - "f1": {MaxItemsOne: tfbridge.False()}, - } - - err = AutoAliasing(info, metadata) - require.NoError(t, err) - assert.False(t, *info.Resources["pkg_r1"].Fields["f1"].MaxItemsOne) - assert.False(t, *info.Resources["pkg_r1"].Fields["f2"].MaxItemsOne) - assert.Equal(t, `{ - "auto-aliasing": { - "resources": { - "pkg_r1": { - "current": "pkg:index/r1:R1", - "fields": { - "f1": { - "maxItemsOne": false - }, - "f2": { - "maxItemsOne": false - } - } - } - } - } -}`, string(metadata.MarshalIndent())) -} - -func TestMaxItemsOneAliasingExpiring(t *testing.T) { - t.Parallel() - provider := func(f1, f2 bool) *tfbridge.ProviderInfo { - prov := &tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "pkg_r1": (&schema.Resource{Schema: schema.SchemaMap{ - "f1": Schema{MaxItemsOne: f1}, - "f2": Schema{MaxItemsOne: f2}, - }}).Shim(), - }, - }).Shim(), - } - err := ComputeDefaults(prov, TokensSingleModule("pkg_", "index", MakeStandardToken("pkg"))) - require.NoError(t, err) - return prov - } - info := provider(true, false) - metadata, err := metadata.New(nil) - require.NoError(t, err) - - // Save current state into metadata - err = AutoAliasing(info, metadata) - require.NoError(t, err) - - v := string(metadata.MarshalIndent()) - expected := `{ - "auto-aliasing": { - "resources": { - "pkg_r1": { - "current": "pkg:index/r1:R1", - "fields": { - "f1": { - "maxItemsOne": true - }, - "f2": { - "maxItemsOne": false - } - } - } - } - } -}` - assert.Equal(t, expected, v) - - info = provider(false, true) - - // Apply metadata back into the provider - info.Version = "1.0.0" // New major version - err = AutoAliasing(info, metadata) - require.NoError(t, err) - - assert.Nil(t, info.Resources["pkg_r1"].Fields["f1"]) - assert.Nil(t, info.Resources["pkg_r1"].Fields["f2"]) - assert.Equal(t, `{ - "auto-aliasing": { - "resources": { - "pkg_r1": { - "current": "pkg:index/r1:R1", - "majorVersion": 1, - "fields": { - "f1": { - "maxItemsOne": false - }, - "f2": { - "maxItemsOne": true - } - } - } - } - } -}`, string(metadata.MarshalIndent())) -} - -func TestMaxItemsOneAliasingNested(t *testing.T) { - t.Parallel() - provider := func(f1, f2 bool) *tfbridge.ProviderInfo { - prov := &tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "pkg_r1": (&schema.Resource{Schema: schema.SchemaMap{ - "f1": Schema{}, - "f2": Schema{elem: (&schema.Resource{ - Schema: schema.SchemaMap{ - "n1": Schema{MaxItemsOne: f1}, - "n2": Schema{MaxItemsOne: f2}, - }, - }).Shim()}, - }}).Shim(), - }, - }).Shim(), - } - err := ComputeDefaults(prov, TokensSingleModule("pkg_", "index", MakeStandardToken("pkg"))) - require.NoError(t, err) - return prov - } - info := provider(true, false) - metadata, err := metadata.New(nil) - require.NoError(t, err) - - // Save current state into metadata - err = AutoAliasing(info, metadata) - require.NoError(t, err) - - v := string(metadata.MarshalIndent()) - expected := `{ - "auto-aliasing": { - "resources": { - "pkg_r1": { - "current": "pkg:index/r1:R1", - "fields": { - "f1": { - "maxItemsOne": false - }, - "f2": { - "maxItemsOne": false, - "elem": { - "fields": { - "n1": { - "maxItemsOne": true - }, - "n2": { - "maxItemsOne": false - } - } - } - } - } - } - } - } -}` - assert.Equal(t, expected, v) - - // Apply the saved metadata to a new provider - info = provider(false, true) - err = AutoAliasing(info, metadata) - require.NoError(t, err) - - assert.Equal(t, expected, string(metadata.MarshalIndent())) - assert.True(t, *info.Resources["pkg_r1"].Fields["f2"].Elem.Fields["n1"].MaxItemsOne) - assert.False(t, *info.Resources["pkg_r1"].Fields["f2"].Elem.Fields["n2"].MaxItemsOne) -} - -// (*ProviderInfo).SetAutonaming skips fields that have a SchemaInfo already defined in -// their resource's ResourceInfo.Fields. We need to make sure that unless we mark a field -// as `MaxItemsOne: nonNil` for some non-nil value, we don't leave that field entry behind -// since that will disable SetAutonaming. -func TestMaxItemsOneAliasingWithAutoNaming(t *testing.T) { - t.Parallel() - provider := func() *tfbridge.ProviderInfo { - prov := &tfbridge.ProviderInfo{ - P: (&schema.Provider{ - ResourcesMap: schema.ResourceMap{ - "pkg_r1": (&schema.Resource{Schema: schema.SchemaMap{ - "name": Schema{typ: shim.TypeString}, - "nest_list": Schema{elem: Schema{typ: shim.TypeBool}}, - "nest_flat": Schema{ - elem: Schema{typ: shim.TypeBool}, - MaxItemsOne: true, - }, - "override_list": Schema{elem: Schema{typ: shim.TypeBool}}, - "override_flat": Schema{ - elem: Schema{typ: shim.TypeInt}, - MaxItemsOne: true, - }, - }}).Shim(), - }, - }).Shim(), - } - err := ComputeDefaults(prov, TokensSingleModule("pkg_", "index", MakeStandardToken("pkg"))) - require.NoError(t, err) - return prov - } - - assertExpected := func(t *testing.T, p *tfbridge.ProviderInfo, hist *metadata.Data) { - r := p.Resources["pkg_r1"] - assert.True(t, r.Fields["name"].Default.AutoNamed) - - assert.Nil(t, r.Fields["nest_list"]) - assert.Nil(t, r.Fields["override_list"]) - - assert.JSONEq(t, `{ - "auto-aliasing": { - "resources": { - "pkg_r1": { - "current": "pkg:index/r1:R1", - "fields": { - "nest_flat": { - "maxItemsOne": true - }, - "nest_list": { - "maxItemsOne": false - }, - "override_flat": { - "maxItemsOne": true - }, - "override_list": { - "maxItemsOne": false - } - } - } - } - } - }`, string(hist.Marshal())) - } - - t.Run("auto-named-then-aliased", func(t *testing.T) { - p := provider() - - info, err := metadata.New(nil) - require.NoError(t, err) - p.SetAutonaming(24, "-") - err = AutoAliasing(p, info) - require.NoError(t, err) - - assertExpected(t, p, info) - }) - - t.Run("auto-aliased-then-named", func(t *testing.T) { - p := provider() - info, err := metadata.New(nil) - require.NoError(t, err) - err = AutoAliasing(p, info) - require.NoError(t, err) - p.SetAutonaming(24, "-") - - assertExpected(t, p, info) - }) -} - -func TestMaxItemsOneDataSourceAliasing(t *testing.T) { - t.Parallel() - provider := func() *tfbridge.ProviderInfo { - prov := &tfbridge.ProviderInfo{ - P: (&schema.Provider{ - DataSourcesMap: schema.ResourceMap{ - "pkg_r1": (&schema.Resource{Schema: schema.SchemaMap{ - "name": Schema{typ: shim.TypeString}, - "nest_list": Schema{elem: Schema{typ: shim.TypeBool}}, - "nest_flat": Schema{ - elem: Schema{typ: shim.TypeBool}, - MaxItemsOne: true, - }, - "override_list": Schema{elem: Schema{typ: shim.TypeBool}}, - "override_flat": Schema{ - elem: Schema{typ: shim.TypeInt}, - MaxItemsOne: true, - }, - }}).Shim(), - }, - }).Shim(), - } - err := ComputeDefaults(prov, TokensSingleModule("pkg_", "index", MakeStandardToken("pkg"))) - require.NoError(t, err) - return prov - } - - assertExpected := func(t *testing.T, p *tfbridge.ProviderInfo, hist *metadata.Data) { - r := p.DataSources["pkg_r1"] - - assert.Nil(t, r.Fields["nest_list"]) - assert.Nil(t, r.Fields["override_list"]) - - assert.JSONEq(t, `{ - "auto-aliasing": { - "datasources": { - "pkg_r1": { - "current": "pkg:index/getR1:getR1", - "fields": { - "nest_flat": { - "maxItemsOne": true - }, - "nest_list": { - "maxItemsOne": false - }, - "override_flat": { - "maxItemsOne": true - }, - "override_list": { - "maxItemsOne": false - } - } - } - } - } - }`, string(hist.Marshal())) - } - - t.Run("auto-named-then-aliased", func(t *testing.T) { - p := provider() - - info, err := metadata.New(nil) - require.NoError(t, err) - p.SetAutonaming(24, "-") - err = AutoAliasing(p, info) - require.NoError(t, err) - - assertExpected(t, p, info) - }) - - t.Run("auto-aliased-then-named", func(t *testing.T) { - p := provider() - info, err := metadata.New(nil) - require.NoError(t, err) - err = AutoAliasing(p, info) - require.NoError(t, err) - p.SetAutonaming(24, "-") - - assertExpected(t, p, info) - }) -} - -type Schema struct { - shim.Schema - MaxItemsOne bool - typ shim.ValueType - elem any -} - -func (s Schema) MaxItems() int { - if s.MaxItemsOne { - return 1 - } - return 0 -} - -func (s Schema) Type() shim.ValueType { - if s.typ == shim.TypeInvalid { - return shim.TypeList - } - return s.typ -} - -func (s Schema) Optional() bool { return true } -func (s Schema) Required() bool { return false } - -func (s Schema) Elem() any { return s.elem }