Skip to content

Commit

Permalink
Cross-tests for PF's Configure
Browse files Browse the repository at this point in the history
This is a pre-requisite for #2440.
  • Loading branch information
iwahbe committed Oct 9, 2024
1 parent e36cdc9 commit e06e8c8
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 88 deletions.
4 changes: 3 additions & 1 deletion pf/tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gotest.tools v2.2.0+incompatible // indirect
gotest.tools/v3 v3.0.3 // indirect
mvdan.cc/gofumpt v0.5.0 // indirect
pgregory.net/rapid v0.6.1 // indirect
)

replace (
Expand Down Expand Up @@ -244,7 +246,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/zclconf/go-cty v1.14.2 // indirect
github.com/zclconf/go-cty v1.14.2
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
gocloud.dev v0.37.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions pf/tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2645,6 +2645,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand Down
51 changes: 51 additions & 0 deletions pf/tests/internal/cross-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Cross-tests for PF

This package provides [cross-testing](../../../../pkg/tests/cross-tests/README.md) for [Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework) based Terraform
providers, bridged into Pulumi with [pf](../../../README.md).

It *does not* contain cross-tests. It just provides a library for writing cross-tests.

An example usage looks like this:

``` go
func TestConfigure(t *testing.T) {
t.Parallel()

schema := schema.Schema{Attributes: map[string]schema.Attribute{
"k": schema.StringAttribute{Optional: true},
}}

tfInput := map[string]cty.Value{"k": cty.StringVal("foo")}

puInput := resource.PropertyMap{"k": resource.MakeSecret(resource.NewProperty("foo"))}

crosstests.Configure(schema, tfInput, puInput)
}
```

Here, the cross-test will assert that a provider who's configuration is described by
`schema` will observe the same inputs when configured in via HCL with the inputs
`tfInputs` and when bridged and configured with Pulumi and `puInputs`.

The idea is that the "Configured Provider" should not be able to tell if it was configured
via HCL or Pulumi YAML:


```
+--------------------+ +---------------------+
| Terraform Provider |--------------------->| Configure(tfInputs) |
+--------------------+ +---------------------+
| \
| \
| \
| +---------------------+
| tfbridge.ShimProvider | Configured Provider |
| +---------------------+
| /
| /
V /
+--------------------+ +---------------------+
| Pulumi Provider |--------------------->| Configure(puInputs) |
+--------------------+ +---------------------+
```

227 changes: 227 additions & 0 deletions pf/tests/internal/cross-tests/configure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package crosstests

import (
"bytes"
"context"
"os"
"path/filepath"
"testing"

"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/pulumi/providertest/providers"
"github.com/pulumi/providertest/pulumitest"
"github.com/pulumi/providertest/pulumitest/opttest"
pb "github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/providerbuilder"
"github.com/pulumi/pulumi-terraform-bridge/pf/tfbridge"
"github.com/pulumi/pulumi-terraform-bridge/pf/tfgen"
crosstests "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tests/cross-tests"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tests/tfcheck"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"
"gopkg.in/yaml.v3"
)

// MakeConfigure returns a [testing] subtest of [Configure].
//
// func TestMyProperty(t *testing.T) {
// t.Run("my-subtest", crosstests.MakeConfigure(schema, tfConfig, puConfig))
// }
//
// For details on the test itself, see [Configure].
func MakeConfigure(schema schema.Schema, tfConfig map[string]cty.Value, puConfig resource.PropertyMap) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
Configure(t, schema, tfConfig, puConfig)
}
}

// Configure will assert that a provider who's configuration is described by
// schema will observe the same inputs when configured in via HCL with the inputs
// tfInputs and when bridged and configured with Pulumi and puInputs.
//
// The idea is that the "Configured Provider" should not be able to tell if it was configured
// via HCL or Pulumi YAML:
//
// +--------------------+ +---------------------+
// | Terraform Provider |--------------------->| Configure(tfInputs) |
// +--------------------+ +---------------------+
// | \
// | \
// | \
// | +---------------------+
// | tfbridge.ShimProvider | Configured Provider |
// | +---------------------+
// | /
// | /
// V /
// +--------------------+ +---------------------+
// | Pulumi Provider |--------------------->| Configure(puInputs) |
// +--------------------+ +---------------------+
//
// Configure should be safe to run in parallel.
func Configure(t *testing.T, schema schema.Schema, tfConfig map[string]cty.Value, puConfig resource.PropertyMap) {
// By default, logs only show when they are on a failed test. By logging to
// topLevelT, we can log items to be shown if downstream tests fail.
topLevelT := t
const providerName = "test"

prov := func(config *tfsdk.Config) *pb.Provider {
return pb.NewProvider(pb.NewProviderArgs{
TypeName: providerName,
ProviderSchema: schema,
OnConfigure: func(c tfsdk.Config) { *config = c },
AllResources: []pb.Resource{{
Name: "res",
}},
})
}

var tfOutput, puOutput tfsdk.Config
t.Run("tf", func(t *testing.T) {

var hcl bytes.Buffer
err := crosstests.WritePF(&hcl).Provider(schema, providerName, tfConfig)
require.NoError(t, err)
// TF does not configure providers unless they are involved with creating
// a resource or datasource, so we create "res" to give the TF provider a
// reason to be configured.
hcl.WriteString(`
resource "` + providerName + `_res" "res" {}
`)

prov := prov(&tfOutput)
driver := tfcheck.NewTfDriver(t, t.TempDir(), prov.TypeName, prov)

driver.Write(t, hcl.String())
plan, err := driver.Plan(t)
require.NoError(t, err)
err = driver.Apply(t, plan)
require.NoError(t, err)
})

t.Run("bridged", func(t *testing.T) {
dir := t.TempDir()

pulumiYaml := map[string]any{
"name": "project",
"runtime": "yaml",
"backend": map[string]any{
"url": "file://./data",
},
"resources": map[string]any{
"p": map[string]any{
"type": "pulumi:providers:" + providerName,
"properties": convertResourceValue(t, puConfig),
},
},
}

bytes, err := yaml.Marshal(pulumiYaml)
require.NoError(t, err)
topLevelT.Logf("Pulumi.yaml:\n%s", string(bytes))
err = os.WriteFile(filepath.Join(dir, "Pulumi.yaml"), bytes, 0600)
require.NoError(t, err)

makeProvider := func(providers.PulumiTest) (pulumirpc.ResourceProviderServer, error) {
ctx, sink := context.Background(), testLogSink{t}
p := info.Provider{
Name: providerName,
P: tfbridge.ShimProvider(prov(&puOutput)),
Version: "0.1.0-dev",
UpstreamRepoPath: ".",
}
p.MustComputeTokens(tokens.SingleModule(providerName, "index", tokens.MakeStandard(providerName)))

for _, v := range p.DataSources {
v.Docs = &info.Doc{Markdown: []byte{' '} /* don't warn the user that docs cannot be found */}
}
for _, v := range p.Resources {
v.Docs = &info.Doc{Markdown: []byte{' '} /* don't warn the user that docs cannot be found */}
}
schema, err := tfgen.GenerateSchema(ctx, tfgen.GenerateSchemaOptions{
ProviderInfo: p,
})
if err != nil {
return nil, err
}

p.MetadataInfo = &info.Metadata{Path: "non-empty"}
return tfbridge.NewProviderServer(ctx, sink, p, tfbridge.ProviderMetadata{
PackageSchema: schema.ProviderMetadata.PackageSchema,
})
}

test := pulumitest.NewPulumiTest(t, dir,
opttest.AttachProviderServer(providerName, makeProvider),
opttest.SkipInstall(),
opttest.Env("PULUMI_DISABLE_AUTOMATIC_PLUGIN_ACQUISITION", "true"),
)
contract.Ignore(test.Preview()) // Assert that the preview succeeded, but not the result.
contract.Ignore(test.Up()) // Assert that the update succeeded, but not the result.
})

skipCompare := t.Failed()
t.Run("compare", func(t *testing.T) {
if skipCompare {
t.Skipf("skipping comparison due to earlier test failure")
}
assert.Equal(t, tfOutput, puOutput)
})
}

type testLogSink struct{ t *testing.T }

func (s testLogSink) Log(_ context.Context, sev diag.Severity, urn resource.URN, msg string) error {
return s.log("LOG", sev, urn, msg)
}
func (s testLogSink) LogStatus(_ context.Context, sev diag.Severity, urn resource.URN, msg string) error {
return s.log("STATUS", sev, urn, msg)
}

func (s testLogSink) log(kind string, sev diag.Severity, urn resource.URN, msg string) error {
var urnMsg string
if urn != "" {
urnMsg = " (" + string(urn) + ")"
}
s.t.Logf("Provider[%s]: %s%s: %s", kind, sev, urnMsg, msg)
return nil
}

func convertResourceValue(t *testing.T, properties resource.PropertyMap) map[string]any {
var convertValue func(resource.PropertyValue) (any, bool)
convertValue = func(v resource.PropertyValue) (any, bool) {
if v.IsComputed() {
require.Fail(t, "cannot convert computed value to YAML")
}
var isSecret bool
if v.IsOutput() {
o := v.OutputValue()
if !o.Known {
require.Fail(t, "cannot convert unknown output value to YAML")
}
v = o.Element
isSecret = o.Secret
}
if v.IsSecret() {
isSecret = true
v = v.SecretValue().Element
}

if isSecret {
return map[string]any{
"fn::secret": v.MapRepl(nil, convertValue),
}, true
}
return nil, false

}
return properties.MapRepl(nil, convertValue)
}
14 changes: 12 additions & 2 deletions pf/tests/internal/providerbuilder/build_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type Provider struct {
Version string
ProviderSchema schema.Schema
AllResources []Resource

onConfigure func(tfsdk.Config)
}

var _ provider.Provider = (*Provider)(nil)
Expand All @@ -49,10 +51,13 @@ func (impl *Provider) Schema(ctx context.Context, req provider.SchemaRequest, re
resp.Schema = impl.ProviderSchema
}

func (*Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
func (impl *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
if impl.onConfigure != nil {
impl.onConfigure(req.Config)
}
}

func (*Provider) DataSources(ctx context.Context) []func() datasource.DataSource {
func (impl *Provider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{}
}

Expand All @@ -76,6 +81,9 @@ type NewProviderArgs struct {
Version string
ProviderSchema schema.Schema
AllResources []Resource

// OnConfigure is called when a provider is configured.
OnConfigure func(tfsdk.Config)
}

// NewProvider creates a new provider with the given resources, filling reasonable defaults.
Expand All @@ -85,6 +93,8 @@ func NewProvider(params NewProviderArgs) *Provider {
Version: params.Version,
ProviderSchema: params.ProviderSchema,
AllResources: params.AllResources,

onConfigure: params.OnConfigure,
}

if prov.TypeName == "" {
Expand Down
Loading

0 comments on commit e06e8c8

Please sign in to comment.