diff --git a/go.mod b/go.mod index 57575eb6..865d0191 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/tmccombs/hcl2json v0.3.3 github.com/yuin/goldmark v1.4.13 github.com/zclconf/go-cty v1.11.0 + github.com/zclconf/go-cty-yaml v1.0.3 golang.org/x/net v0.15.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index c8cf1cdf..c4735a54 100644 --- a/go.sum +++ b/go.sum @@ -362,6 +362,8 @@ github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uU github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0= github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zclconf/go-cty-yaml v1.0.3 h1:og/eOQ7lvA/WWhHGFETVWNduJM7Rjsv2RRpx1sdFMLc= +github.com/zclconf/go-cty-yaml v1.0.3/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/pkg/controller/external_nofork.go b/pkg/controller/external_nofork.go index 59903fa8..c6f9ed01 100644 --- a/pkg/controller/external_nofork.go +++ b/pkg/controller/external_nofork.go @@ -194,7 +194,21 @@ func (c *NoForkConnector) applyStateFuncToParam(sc *schema.Schema, param any) an } } } - case schema.TypeBool, schema.TypeInt, schema.TypeFloat, schema.TypeString: + case schema.TypeString: + // For String types check if it is an HCL string and process + if isHCLSnippetPattern.MatchString(param.(string)) { + hclProccessedParam, err := processHCLParam(param.(string)) + if err != nil { + c.logger.Debug("could not process param, returning original", "param", sc.GoString()) + } else { + param = hclProccessedParam + } + } + if sc.StateFunc != nil { + return sc.StateFunc(param) + } + return param + case schema.TypeBool, schema.TypeInt, schema.TypeFloat: if sc.StateFunc != nil { return sc.StateFunc(param) } diff --git a/pkg/controller/hcl.go b/pkg/controller/hcl.go new file mode 100644 index 00000000..63853ecc --- /dev/null +++ b/pkg/controller/hcl.go @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2023 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "encoding/base64" + "fmt" + "log" + "regexp" + "unicode/utf8" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclparse" + ctyyaml "github.com/zclconf/go-cty-yaml" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + ctyfuncstdlib "github.com/zclconf/go-cty/cty/function/stdlib" +) + +var Base64DecodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + AllowMarked: true, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + str, strMarks := args[0].Unmark() + s := str.AsString() + sDec, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data %s", s) + } + if !utf8.Valid(sDec) { + log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", s) + return cty.UnknownVal(cty.String), fmt.Errorf("the result of decoding the provided string is not valid UTF-8") + } + return cty.StringVal(string(sDec)).WithMarks(strMarks), nil + }, +}) + +var Base64EncodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil + }, +}) + +// evalCtx registers the known functions for HCL processing +// variable interpolation is not supported, as in our case they are irrelevant +var evalCtx = &hcl.EvalContext{ + Variables: map[string]cty.Value{}, + Functions: map[string]function.Function{ + "abs": ctyfuncstdlib.AbsoluteFunc, + "ceil": ctyfuncstdlib.CeilFunc, + "chomp": ctyfuncstdlib.ChompFunc, + "coalescelist": ctyfuncstdlib.CoalesceListFunc, + "compact": ctyfuncstdlib.CompactFunc, + "concat": ctyfuncstdlib.ConcatFunc, + "contains": ctyfuncstdlib.ContainsFunc, + "csvdecode": ctyfuncstdlib.CSVDecodeFunc, + "distinct": ctyfuncstdlib.DistinctFunc, + "element": ctyfuncstdlib.ElementFunc, + "chunklist": ctyfuncstdlib.ChunklistFunc, + "flatten": ctyfuncstdlib.FlattenFunc, + "floor": ctyfuncstdlib.FloorFunc, + "format": ctyfuncstdlib.FormatFunc, + "formatdate": ctyfuncstdlib.FormatDateFunc, + "formatlist": ctyfuncstdlib.FormatListFunc, + "indent": ctyfuncstdlib.IndentFunc, + "join": ctyfuncstdlib.JoinFunc, + "jsondecode": ctyfuncstdlib.JSONDecodeFunc, + "jsonencode": ctyfuncstdlib.JSONEncodeFunc, + "keys": ctyfuncstdlib.KeysFunc, + "log": ctyfuncstdlib.LogFunc, + "lower": ctyfuncstdlib.LowerFunc, + "max": ctyfuncstdlib.MaxFunc, + "merge": ctyfuncstdlib.MergeFunc, + "min": ctyfuncstdlib.MinFunc, + "parseint": ctyfuncstdlib.ParseIntFunc, + "pow": ctyfuncstdlib.PowFunc, + "range": ctyfuncstdlib.RangeFunc, + "regex": ctyfuncstdlib.RegexFunc, + "regexall": ctyfuncstdlib.RegexAllFunc, + "reverse": ctyfuncstdlib.ReverseListFunc, + "setintersection": ctyfuncstdlib.SetIntersectionFunc, + "setproduct": ctyfuncstdlib.SetProductFunc, + "setsubtract": ctyfuncstdlib.SetSubtractFunc, + "setunion": ctyfuncstdlib.SetUnionFunc, + "signum": ctyfuncstdlib.SignumFunc, + "slice": ctyfuncstdlib.SliceFunc, + "sort": ctyfuncstdlib.SortFunc, + "split": ctyfuncstdlib.SplitFunc, + "strrev": ctyfuncstdlib.ReverseFunc, + "substr": ctyfuncstdlib.SubstrFunc, + "timeadd": ctyfuncstdlib.TimeAddFunc, + "title": ctyfuncstdlib.TitleFunc, + "trim": ctyfuncstdlib.TrimFunc, + "trimprefix": ctyfuncstdlib.TrimPrefixFunc, + "trimspace": ctyfuncstdlib.TrimSpaceFunc, + "trimsuffix": ctyfuncstdlib.TrimSuffixFunc, + "upper": ctyfuncstdlib.UpperFunc, + "values": ctyfuncstdlib.ValuesFunc, + "zipmap": ctyfuncstdlib.ZipmapFunc, + "yamldecode": ctyyaml.YAMLDecodeFunc, + "yamlencode": ctyyaml.YAMLEncodeFunc, + "base64encode": Base64EncodeFunc, + "base64decode": Base64DecodeFunc, + }, +} + +// hclBlock is the target type for decoding the specially-crafted HCL document. +// interested in processing HCL snippets for a single parameter +type hclBlock struct { + Parameter string `hcl:"parameter"` +} + +// isHCLSnippetPattern is the regex pattern for determining whether +// the param is an HCL template +var isHCLSnippetPattern = regexp.MustCompile(`\$\{\w+\s*\([\S\s]*\}`) + +// processHCLParam processes the given string parameter +// with HCL format and including HCL functions, +// coming from the Managed Resource spec parameters. +// It prepares a tailored HCL snippet which consist of only a single attribute +// parameter = theGivenParameterValueInHCLSyntax +// It only operates on string parameters, and returns a string. +// caller should ensure that the given parameter is an HCL snippet +func processHCLParam(param string) (string, error) { + param = fmt.Sprintf("parameter = \"%s\"\n", param) + return processHCLParamBytes([]byte(param)) +} + +// processHCLParamBytes parses and decodes the HCL snippet +func processHCLParamBytes(paramValueBytes []byte) (string, error) { + hclParser := hclparse.NewParser() + // here the filename argument is not important, + // used by the hcl parser lib for tracking caching purposes + // it is just a name reference + hclFile, diag := hclParser.ParseHCL(paramValueBytes, "dummy.hcl") + if diag.HasErrors() { + return "", diag + } + + var paramWrapper hclBlock + diags := gohcl.DecodeBody(hclFile.Body, evalCtx, ¶mWrapper) + if diags.HasErrors() { + return "", diags + } + + return paramWrapper.Parameter, nil +}