Skip to content

Commit

Permalink
add support for hcl functions in string params (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
erhancagirici authored Nov 10, 2023
1 parent d9420d3 commit bd014e9
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
16 changes: 15 additions & 1 deletion pkg/controller/external_nofork.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
163 changes: 163 additions & 0 deletions pkg/controller/hcl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
//
// 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, &paramWrapper)
if diags.HasErrors() {
return "", diags
}

return paramWrapper.Parameter, nil
}

0 comments on commit bd014e9

Please sign in to comment.