From bda8f93e14658beed7f1568655b472733bb5b46e Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Fri, 18 Oct 2024 13:11:57 +0200 Subject: [PATCH] Expose `crosstests.Create` This begins to turn cross-tests into a library with a defined interface. Existing cross-tests say that they are a prototype level API, and they feel that way. The prototyping was successful, and cross-tests have proven their worth. We should begin to move towards a more discover-able API. The first function of which is: ```go // crosstests/create.go func Create( t T, resource map[string]*schema.Schema, tfConfig cty.Value, puConfig resource.PropertyMap, options ...CreateOption, ) ``` We can continue to iterate on this API, but we should begin to formalize a boundary between the implementation of cross-tests and tests written with cross-tests. stack-info: PR: https://github.com/pulumi/pulumi-terraform-bridge/pull/2501, branch: iwahbe/stack/2 --- pkg/internal/tests/cross-tests/create.go | 119 +++++++++++++++++++++++ pkg/tests/pulcheck/pulcheck.go | 14 ++- 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 pkg/internal/tests/cross-tests/create.go diff --git a/pkg/internal/tests/cross-tests/create.go b/pkg/internal/tests/cross-tests/create.go new file mode 100644 index 000000000..445758ee0 --- /dev/null +++ b/pkg/internal/tests/cross-tests/create.go @@ -0,0 +1,119 @@ +// 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 + +package crosstests + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tests/pulcheck" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" +) + +// Create validates that a Terraform provider witnesses the same input when: +// - invoked directly with HCL on tfConfig +// - bridged and invoked via Pulumi YAML on puConfig +// +// Create only applies to resources defined with github.com/hashicorp/terraform-plugin-sdk/v2. For cross-tests +// on Plugin Framework based resources, see +// github.com/pulumi/pulumi-terraform-bridge/pkg/pf/tests/internal/cross-tests. +// +// Create *does not* verify the outputs of the resource, only that the provider witnessed the same inputs. +func Create( + t T, resource map[string]*schema.Schema, tfConfig cty.Value, puConfig resource.PropertyMap, + options ...CreateOption, +) { + var opts createOpts + for _, f := range options { + f(&opts) + } + + type result struct { + data *schema.ResourceData + meta any + wasSet bool + } + var tfResult, puResult result + + makeResource := func(writeTo *result) *schema.Resource { + return &schema.Resource{ + Schema: resource, + CreateContext: func(_ context.Context, rd *schema.ResourceData, meta any) diag.Diagnostics { + *writeTo = result{rd, meta, true} + rd.SetId("someid") // CreateContext must pick an ID + return nil + }, + } + } + + tfwd := t.TempDir() + tfd := newTFResDriver(t, tfwd, defProviderShortName, defRtype, makeResource(&tfResult)) + tfd.writePlanApply(t, resource, defRtype, "example", tfConfig, lifecycleArgs{}) + + require.True(t, tfResult.wasSet, "terraform test result was not set") + + resMap := map[string]*schema.Resource{defRtype: makeResource(&puResult)} + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider( + t, defProviderShortName, tfp, + pulcheck.WithResourceInfo(map[string]*info.Resource{defRtype: opts.resourceInfo}), + ) + pd := &pulumiDriver{ + name: defProviderShortName, + pulumiResourceToken: defRtoken, + tfResourceName: defRtype, + } + yamlProgram := pd.generateYAML(t, puConfig) + + pt := pulcheck.PulCheck(t, bridgedProvider, string(yamlProgram)) + + pt.Up(t) + + require.True(t, puResult.wasSet, "pulumi test was not set") + + // Compare the result + + assert.Equal(t, tfResult.meta, puResult.meta, + "assert that both providers were configured with the same provider metadata") + + // We are unable to assert that both providers were configured with the exact same + // data. Type information doesn't line up in the simple case. This just doesn't work: + // + // assert.Equal(t, tfResult.data, puResult.data) + // + // We make due by comparing raw data. + assertCtyValEqual(t, "RawConfig", tfResult.data.GetRawConfig(), puResult.data.GetRawConfig()) + assertCtyValEqual(t, "RawPlan", tfResult.data.GetRawPlan(), puResult.data.GetRawPlan()) + assertCtyValEqual(t, "RawState", tfResult.data.GetRawState(), puResult.data.GetRawState()) +} + +type createOpts struct { + resourceInfo *info.Resource +} + +// An option that can be used to customize [Create]. +type CreateOption func(*createOpts) + +// Specify an [info.Resource] to apply to the resource under test. +func CreateResourceInfo(info info.Resource) CreateOption { + contract.Assertf(info.Tok == "", "cannot set info.Tok, it will not be respected") + return func(o *createOpts) { o.resourceInfo = &info } +} diff --git a/pkg/tests/pulcheck/pulcheck.go b/pkg/tests/pulcheck/pulcheck.go index 4e6b32636..90f43df33 100644 --- a/pkg/tests/pulcheck/pulcheck.go +++ b/pkg/tests/pulcheck/pulcheck.go @@ -129,6 +129,7 @@ type T interface { type bridgedProviderOpts struct { DisablePlanResourceChange bool StateEdit shimv2.PlanStateEditFunc + resourceInfo map[string]*info.Resource } // BridgedProviderOpts @@ -147,11 +148,19 @@ func WithStateEdit(f shimv2.PlanStateEditFunc) BridgedProviderOpt { } } +// WithResourceInfo allows the user to set the info.Provider.Resources field within a +// [BridgedProvider]. +// +// This is an experimental API. +func WithResourceInfo(info map[string]*info.Resource) BridgedProviderOpt { + return func(o *bridgedProviderOpts) { o.resourceInfo = info } +} + // This is an experimental API. func BridgedProvider(t T, providerName string, tfp *schema.Provider, opts ...BridgedProviderOpt) info.Provider { - options := &bridgedProviderOpts{} + var options bridgedProviderOpts for _, opt := range opts { - opt(options) + opt(&options) } EnsureProviderValid(t, tfp) @@ -169,6 +178,7 @@ func BridgedProvider(t T, providerName string, tfp *schema.Provider, opts ...Bri Version: "0.0.1", MetadataInfo: &tfbridge.MetadataInfo{}, EnableZeroDefaultSchemaVersion: true, + Resources: options.resourceInfo, } makeToken := func(module, name string) (string, error) { return tokens.MakeStandard(providerName)(module, name)