diff --git a/go.mod b/go.mod index 865d0191..89b10cbf 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect @@ -104,6 +105,8 @@ require ( github.com/vmihailenco/tagparser v0.1.1 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect diff --git a/go.sum b/go.sum index c4735a54..0e81176e 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,7 @@ github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/ github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -108,6 +109,7 @@ github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -372,9 +374,15 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -599,6 +607,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/controller/external_async_nofork_test.go b/pkg/controller/external_async_nofork_test.go new file mode 100644 index 00000000..7acd0ae4 --- /dev/null +++ b/pkg/controller/external_async_nofork_test.go @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2023 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tf "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane/upjet/pkg/config" + "github.com/crossplane/upjet/pkg/resource/fake" + "github.com/crossplane/upjet/pkg/terraform" +) + +var ( + cfgAsync = &config.Resource{ + TerraformResource: &schema.Resource{ + Timeouts: &schema.ResourceTimeout{ + Create: &timeout, + Read: &timeout, + Update: &timeout, + Delete: &timeout, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "id": { + Type: schema.TypeString, + Computed: true, + Required: false, + }, + "map": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "list": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + ExternalName: config.IdentifierFromProvider, + Sensitive: config.Sensitive{AdditionalConnectionDetailsFn: func(attr map[string]any) (map[string][]byte, error) { + return nil, nil + }}, + } + objAsync = &fake.Terraformed{ + Parameterizable: fake.Parameterizable{ + Parameters: map[string]any{ + "name": "example", + "map": map[string]any{ + "key": "value", + }, + "list": []any{"elem1", "elem2"}, + }, + }, + Observable: fake.Observable{ + Observation: map[string]any{}, + }, + } +) + +func prepareNoForkAsyncExternal(r Resource, cfg *config.Resource, fns CallbackFns) *noForkAsyncExternal { + schemaBlock := cfg.TerraformResource.CoreConfigSchema() + rawConfig, err := schema.JSONMapToStateValue(map[string]any{"name": "example"}, schemaBlock) + if err != nil { + panic(err) + } + return &noForkAsyncExternal{ + noForkExternal: &noForkExternal{ + ts: terraform.Setup{}, + resourceSchema: r, + config: cfg, + params: map[string]any{ + "name": "example", + }, + rawConfig: rawConfig, + logger: logTest, + opTracker: NewAsyncTracker(), + }, + callback: fns, + } +} + +func TestAsyncNoForkConnect(t *testing.T) { + type args struct { + setupFn terraform.SetupFn + cfg *config.Resource + ots *OperationTrackerStore + obj xpresource.Managed + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Successful": { + args: args{ + setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) { + return terraform.Setup{}, nil + }, + cfg: cfgAsync, + obj: objAsync, + ots: ots, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := NewNoForkAsyncConnector(nil, tc.args.ots, tc.args.setupFn, tc.args.cfg, WithNoForkAsyncLogger(logTest)) + _, err := c.Connect(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestAsyncNoForkObserve(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + } + type want struct { + obs managed.ExternalObservation + err error + } + cases := map[string]struct { + args + want + }{ + "NotExists": { + args: args{ + r: mockResource{ + RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return nil, nil + }, + }, + cfg: cfgAsync, + obj: objAsync, + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + ResourceUpToDate: false, + ResourceLateInitialized: false, + ConnectionDetails: nil, + Diff: "", + }, + }, + }, + "UpToDate": { + args: args{ + r: mockResource{ + RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id", Attributes: map[string]string{"name": "example"}}, nil + }, + }, + cfg: cfgAsync, + obj: objAsync, + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + ResourceLateInitialized: true, + ConnectionDetails: nil, + Diff: "", + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkAsyncExternal := prepareNoForkAsyncExternal(tc.args.r, tc.args.cfg, CallbackFns{}) + observation, err := noForkAsyncExternal.Observe(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.obs, observation); diff != "" { + t.Errorf("\n%s\nObserve(...): -want observation, +got observation:\n", diff) + } + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestAsyncNoForkCreate(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + fns CallbackFns + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Successful": { + args: args{ + r: mockResource{ + ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id"}, nil + }, + }, + cfg: cfgAsync, + obj: objAsync, + fns: CallbackFns{ + CreateFn: func(s string) terraform.CallbackFn { + return func(err error, ctx context.Context) error { + return nil + } + }, + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkAsyncExternal := prepareNoForkAsyncExternal(tc.args.r, tc.args.cfg, tc.args.fns) + _, err := noForkAsyncExternal.Create(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestAsyncNoForkUpdate(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + fns CallbackFns + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Successful": { + args: args{ + r: mockResource{ + ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id"}, nil + }, + }, + cfg: cfgAsync, + obj: objAsync, + fns: CallbackFns{ + UpdateFn: func(s string) terraform.CallbackFn { + return func(err error, ctx context.Context) error { + return nil + } + }, + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkAsyncExternal := prepareNoForkAsyncExternal(tc.args.r, tc.args.cfg, tc.args.fns) + _, err := noForkAsyncExternal.Update(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestAsyncNoForkDelete(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + fns CallbackFns + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Successful": { + args: args{ + r: mockResource{ + ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id"}, nil + }, + }, + cfg: cfgAsync, + obj: objAsync, + fns: CallbackFns{ + DestroyFn: func(s string) terraform.CallbackFn { + return func(err error, ctx context.Context) error { + return nil + } + }, + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkAsyncExternal := prepareNoForkAsyncExternal(tc.args.r, tc.args.cfg, tc.args.fns) + err := noForkAsyncExternal.Delete(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} diff --git a/pkg/controller/external_nofork.go b/pkg/controller/external_nofork.go index c6f9ed01..3bcd674b 100644 --- a/pkg/controller/external_nofork.go +++ b/pkg/controller/external_nofork.go @@ -17,6 +17,7 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" tf "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/pkg/errors" @@ -102,9 +103,14 @@ func getJSONMap(mg xpresource.Managed) (map[string]any, error) { return v.(map[string]any), nil } +type Resource interface { + Apply(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) + RefreshWithoutUpgrade(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) +} + type noForkExternal struct { ts terraform.Setup - resourceSchema *schema.Resource + resourceSchema Resource config *config.Resource instanceDiff *tf.InstanceDiff params map[string]any @@ -399,7 +405,7 @@ func getTimeoutParameters(config *config.Resource) map[string]any { //nolint:goc func (n *noForkExternal) getResourceDataDiff(tr resource.Terraformed, ctx context.Context, s *tf.InstanceState, resourceExists bool) (*tf.InstanceDiff, error) { //nolint:gocyclo resourceConfig := tf.NewResourceConfigRaw(n.params) - instanceDiff, err := schema.InternalMap(n.resourceSchema.Schema).Diff(ctx, s, resourceConfig, nil, n.ts.Meta, false) + instanceDiff, err := schema.InternalMap(n.config.TerraformResource.Schema).Diff(ctx, s, resourceConfig, nil, n.ts.Meta, false) if err != nil { return nil, errors.Wrap(err, "failed to get *terraform.InstanceDiff") } @@ -417,7 +423,7 @@ func (n *noForkExternal) getResourceDataDiff(tr resource.Terraformed, ctx contex } if instanceDiff != nil { v := cty.EmptyObjectVal - v, err = instanceDiff.ApplyToValue(v, n.resourceSchema.CoreConfigSchema()) + v, err = instanceDiff.ApplyToValue(v, n.config.TerraformResource.CoreConfigSchema()) if err != nil { return nil, errors.Wrap(err, "cannot apply Terraform instance diff to an empty value") } @@ -643,7 +649,7 @@ func (n *noForkExternal) Delete(ctx context.Context, _ xpresource.Managed) error } func (n *noForkExternal) fromInstanceStateToJSONMap(newState *tf.InstanceState) (map[string]interface{}, error) { - impliedType := n.resourceSchema.CoreConfigSchema().ImpliedType() + impliedType := n.config.TerraformResource.CoreConfigSchema().ImpliedType() attrsAsCtyValue, err := newState.AttrsAsObjectValue(impliedType) if err != nil { return nil, errors.Wrap(err, "could not convert attrs to cty value") diff --git a/pkg/controller/external_nofork_test.go b/pkg/controller/external_nofork_test.go new file mode 100644 index 00000000..b7fa928b --- /dev/null +++ b/pkg/controller/external_nofork_test.go @@ -0,0 +1,382 @@ +// SPDX-FileCopyrightText: 2023 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + "testing" + "time" + + "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tf "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/crossplane/upjet/pkg/config" + "github.com/crossplane/upjet/pkg/resource/fake" + "github.com/crossplane/upjet/pkg/terraform" +) + +var ( + zl = zap.New(zap.UseDevMode(true)) + logTest = logging.NewLogrLogger(zl.WithName("provider-aws")) + ots = NewOperationStore(logTest) + timeout = time.Duration(1200000000000) + cfg = &config.Resource{ + TerraformResource: &schema.Resource{ + Timeouts: &schema.ResourceTimeout{ + Create: &timeout, + Read: &timeout, + Update: &timeout, + Delete: &timeout, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "id": { + Type: schema.TypeString, + Computed: true, + Required: false, + }, + "map": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "list": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + ExternalName: config.IdentifierFromProvider, + Sensitive: config.Sensitive{AdditionalConnectionDetailsFn: func(attr map[string]any) (map[string][]byte, error) { + return nil, nil + }}, + } + obj = &fake.Terraformed{ + Parameterizable: fake.Parameterizable{ + Parameters: map[string]any{ + "name": "example", + "map": map[string]any{ + "key": "value", + }, + "list": []any{"elem1", "elem2"}, + }, + }, + Observable: fake.Observable{ + Observation: map[string]any{}, + }, + } +) + +func prepareNoForkExternal(r Resource, cfg *config.Resource) *noForkExternal { + schemaBlock := cfg.TerraformResource.CoreConfigSchema() + rawConfig, err := schema.JSONMapToStateValue(map[string]any{"name": "example"}, schemaBlock) + if err != nil { + panic(err) + } + return &noForkExternal{ + ts: terraform.Setup{}, + resourceSchema: r, + config: cfg, + params: map[string]any{ + "name": "example", + }, + rawConfig: rawConfig, + logger: logTest, + opTracker: NewAsyncTracker(), + } +} + +type mockResource struct { + ApplyFn func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) + RefreshWithoutUpgradeFn func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) +} + +func (m mockResource) Apply(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return m.ApplyFn(ctx, s, d, meta) +} + +func (m mockResource) RefreshWithoutUpgrade(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return m.RefreshWithoutUpgradeFn(ctx, s, meta) +} + +func TestNoForkConnect(t *testing.T) { + type args struct { + setupFn terraform.SetupFn + cfg *config.Resource + ots *OperationTrackerStore + obj xpresource.Managed + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Successful": { + args: args{ + setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) { + return terraform.Setup{}, nil + }, + cfg: cfg, + obj: obj, + ots: ots, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := NewNoForkConnector(nil, tc.args.setupFn, tc.args.cfg, tc.args.ots, WithNoForkLogger(logTest)) + _, err := c.Connect(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestNoForkObserve(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + } + type want struct { + obs managed.ExternalObservation + err error + } + cases := map[string]struct { + args + want + }{ + "NotExists": { + args: args{ + r: mockResource{ + RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return nil, nil + }, + }, + cfg: cfg, + obj: obj, + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: false, + ResourceUpToDate: false, + ResourceLateInitialized: false, + ConnectionDetails: nil, + Diff: "", + }, + }, + }, + "UpToDate": { + args: args{ + r: mockResource{ + RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id", Attributes: map[string]string{"name": "example"}}, nil + }, + }, + cfg: cfg, + obj: obj, + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + ResourceLateInitialized: true, + ConnectionDetails: nil, + Diff: "", + }, + }, + }, + "InitProvider": { + args: args{ + r: mockResource{ + RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id", Attributes: map[string]string{"name": "example2"}}, nil + }, + }, + cfg: cfg, + obj: &fake.Terraformed{ + Parameterizable: fake.Parameterizable{ + Parameters: map[string]any{ + "name": "example", + "map": map[string]any{ + "key": "value", + }, + "list": []any{"elem1", "elem2"}, + }, + InitParameters: map[string]any{ + "list": []any{"elem1", "elem2", "elem3"}, + }, + }, + Observable: fake.Observable{ + Observation: map[string]any{}, + }, + }, + }, + want: want{ + obs: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: false, + ResourceLateInitialized: true, + ConnectionDetails: nil, + Diff: "", + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkExternal := prepareNoForkExternal(tc.args.r, tc.args.cfg) + observation, err := noForkExternal.Observe(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.obs, observation); diff != "" { + t.Errorf("\n%s\nObserve(...): -want observation, +got observation:\n", diff) + } + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestNoForkCreate(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Unsuccessful": { + args: args{ + r: mockResource{ + ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return nil, nil + }, + }, + cfg: cfg, + obj: obj, + }, + want: want{ + err: errors.New("failed to read the ID of the new resource"), + }, + }, + "Successful": { + args: args{ + r: mockResource{ + ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id"}, nil + }, + }, + cfg: cfg, + obj: obj, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkExternal := prepareNoForkExternal(tc.args.r, tc.args.cfg) + _, err := noForkExternal.Create(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestNoForkUpdate(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Successful": { + args: args{ + r: mockResource{ + ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id"}, nil + }, + }, + cfg: cfg, + obj: obj, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkExternal := prepareNoForkExternal(tc.args.r, tc.args.cfg) + _, err := noForkExternal.Update(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +} + +func TestNoForkDelete(t *testing.T) { + type args struct { + r Resource + cfg *config.Resource + obj xpresource.Managed + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "Successful": { + args: args{ + r: mockResource{ + ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { + return &tf.InstanceState{ID: "example-id"}, nil + }, + }, + cfg: cfg, + obj: obj, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + noForkExternal := prepareNoForkExternal(tc.args.r, tc.args.cfg) + err := noForkExternal.Delete(context.TODO(), tc.args.obj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) + } + }) + } +}