diff --git a/pkg/config/canonical.go b/pkg/config/canonical.go new file mode 100644 index 00000000..e63fab00 --- /dev/null +++ b/pkg/config/canonical.go @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "github.com/pkg/errors" + + "github.com/crossplane/upjet/pkg/resource/json" +) + +const ( + errFmtNotJSONString = "parameter at path %q with value %v is not a (JSON) string" + errFmtCanonicalize = "failed to canonicalize the parameter at path %q" +) + +// CanonicalizeJSONParameters returns a ConfigurationInjector that computes +// and stores the canonical forms of the JSON documents for the specified list +// of top-level Terraform configuration arguments. Please note that currently +// only top-level configuration arguments are supported by this function. +func CanonicalizeJSONParameters(tfPath ...string) ConfigurationInjector { + return func(jsonMap map[string]any, tfMap map[string]any) error { + for _, param := range tfPath { + p, ok := tfMap[param] + if !ok { + continue + } + s, ok := p.(string) + if !ok { + return errors.Errorf(errFmtNotJSONString, param, p) + } + if s == "" { + continue + } + cJSON, err := json.Canonicalize(s) + if err != nil { + return errors.Wrapf(err, errFmtCanonicalize, param) + } + tfMap[param] = cJSON + } + return nil + } +} diff --git a/pkg/config/resource.go b/pkg/config/resource.go index f45a2f96..a9e16344 100644 --- a/pkg/config/resource.go +++ b/pkg/config/resource.go @@ -512,7 +512,7 @@ type CustomDiff func(diff *terraform.InstanceDiff, state *terraform.InstanceStat // values from the specified managed resource into the specified configuration // map. jsonMap is the map obtained by converting the `spec.forProvider` using // the JSON tags and tfMap is obtained by using the TF tags. -type ConfigurationInjector func(jsonMap map[string]any, tfMap map[string]any) +type ConfigurationInjector func(jsonMap map[string]any, tfMap map[string]any) error // SchemaElementOptions represents schema element options for the // schema elements of a Resource. diff --git a/pkg/controller/external_tfpluginsdk.go b/pkg/controller/external_tfpluginsdk.go index ae49b59c..a98543a6 100644 --- a/pkg/controller/external_tfpluginsdk.go +++ b/pkg/controller/external_tfpluginsdk.go @@ -136,7 +136,9 @@ func getExtendedParameters(ctx context.Context, tr resource.Terraformed, externa if err != nil { return nil, errors.Wrap(err, "cannot get JSON map for the managed resource's spec.forProvider value") } - config.TerraformConfigurationInjector(m, params) + if err := config.TerraformConfigurationInjector(m, params); err != nil { + return nil, errors.Wrap(err, "cannot invoke the configured TerraformConfigurationInjector") + } } tfID, err := config.ExternalName.GetIDFn(ctx, externalName, params, ts.Map()) diff --git a/pkg/resource/json/canonical.go b/pkg/resource/json/canonical.go new file mode 100644 index 00000000..691b2ee8 --- /dev/null +++ b/pkg/resource/json/canonical.go @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package json + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +const ( + errFmtJSONUnmarshal = "failed to unmarshal the JSON document: %s" + errFmtJSONMarshal = "failed to marshal the dictionary: %v" +) + +var ( + cJSON = jsoniter.Config{ + SortMapKeys: true, + }.Froze() +) + +// Canonicalize minifies and orders the keys of the specified JSON document +// to return a canonical form of it, along with any errors encountered during +// the process. +func Canonicalize(json string) (string, error) { + var m map[string]any + if err := cJSON.Unmarshal([]byte(json), &m); err != nil { + return "", errors.Wrapf(err, errFmtJSONUnmarshal, json) + } + buff, err := cJSON.Marshal(m) + return string(buff), errors.Wrapf(err, errFmtJSONMarshal, m) +}