Skip to content

Commit

Permalink
Represent TerraformBackend.Config with cty.Value
Browse files Browse the repository at this point in the history
* Add `Dict` struct to map string ket to `cty.Value` and handle YAML marshal/unmarshal;
* Use `Dict` instead of `map[string]interface{}` for `TerraformBackend.Config`.
  • Loading branch information
mr0re1 committed Apr 9, 2023
1 parent 8364463 commit ffebeb0
Show file tree
Hide file tree
Showing 10 changed files with 386 additions and 77 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ require (
require (
cloud.google.com/go/serviceusage v1.6.0
github.com/go-git/go-billy/v5 v5.4.1
github.com/google/go-cmp v0.5.9
github.com/googleapis/gax-go/v2 v2.8.0
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b
google.golang.org/api v0.114.0
)

Expand All @@ -45,7 +47,6 @@ require (
github.com/go-git/gcfg v1.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
Expand Down Expand Up @@ -283,6 +284,7 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -408,6 +410,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
Expand Down Expand Up @@ -466,6 +469,7 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand All @@ -474,8 +478,11 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.13.1 h1:0a6bRwuiSHtAmqCqNOE+c2oHgepv0ctoxU4FUe43kwc=
github.com/zclconf/go-cty v1.13.1/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
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 Expand Up @@ -541,6 +548,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
17 changes: 8 additions & 9 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (dc DeploymentConfig) getGroupByID(groupID string) (DeploymentGroup, error)
// TerraformBackend defines the configuration for the terraform state backend
type TerraformBackend struct {
Type string
Configuration map[string]interface{}
Configuration Dict
}

type validatorName int64
Expand Down Expand Up @@ -596,9 +596,9 @@ func checkBackend(b TerraformBackend) error {
if hasVariable(b.Type) {
return fmt.Errorf(errMsg, "type", b.Type)
}
for k, v := range b.Configuration {
if s, ok := v.(string); ok && hasVariable(s) {
return fmt.Errorf(errMsg, k, s)
for k, v := range b.Configuration.Items() {
if v.Type() == cty.String && hasVariable(v.AsString()) {
return fmt.Errorf(errMsg, k, v.AsString())
}
}
return nil
Expand Down Expand Up @@ -664,10 +664,9 @@ func (dc *DeploymentConfig) SetCLIVariables(cliVariables []string) error {
func (dc *DeploymentConfig) SetBackendConfig(cliBEConfigVars []string) error {
// Set "gcs" as default value when --backend-config is specified at CLI
if len(cliBEConfigVars) > 0 {
dc.Config.TerraformBackendDefaults.Type = "gcs"
dc.Config.TerraformBackendDefaults.Configuration = make(map[string]interface{})
dc.Config.TerraformBackendDefaults = TerraformBackend{Type: "gcs"}
}

be := &dc.Config.TerraformBackendDefaults
for _, config := range cliBEConfigVars {
arr := strings.SplitN(config, "=", 2)

Expand All @@ -678,9 +677,9 @@ func (dc *DeploymentConfig) SetBackendConfig(cliBEConfigVars []string) error {
key, value := arr[0], arr[1]
switch key {
case "type":
dc.Config.TerraformBackendDefaults.Type = value
be.Type = value
default:
dc.Config.TerraformBackendDefaults.Configuration[key] = value
be.Configuration.Set(key, cty.StringVal(value))
}

}
Expand Down
64 changes: 24 additions & 40 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,7 @@ deployment_groups:
Vars: map[string]interface{}{
"project_id": "test-project",
"labels": defaultLabels},
DeploymentGroups: []DeploymentGroup{{Name: "DeploymentGroup1", TerraformBackend: TerraformBackend{}, Modules: testModules}},
TerraformBackendDefaults: TerraformBackend{
Type: "",
Configuration: map[string]interface{}{},
},
DeploymentGroups: []DeploymentGroup{{Name: "DeploymentGroup1", Modules: testModules}},
}
// For expand.go
requiredVar = modulereader.VarInfo{
Expand Down Expand Up @@ -196,17 +192,9 @@ func getDeploymentConfigForTest() DeploymentConfig {
"deployment_name": "deployment_name",
"project_id": "test-project",
},
TerraformBackendDefaults: TerraformBackend{
Type: "",
Configuration: map[string]interface{}{},
},
DeploymentGroups: []DeploymentGroup{
{
Name: "group1",
TerraformBackend: TerraformBackend{
Type: "",
Configuration: map[string]interface{}{},
},
Name: "group1",
Modules: []Module{testModule, testModuleWithLabels},
},
},
Expand Down Expand Up @@ -961,10 +949,8 @@ func (s *MySuite) TestSetCLIVariables(c *C) {
func (s *MySuite) TestSetBackendConfig(c *C) {
// Success
dc := getDeploymentConfigForTest()
c.Assert(dc.Config.TerraformBackendDefaults.Type, Equals, "")
c.Assert(dc.Config.TerraformBackendDefaults.Configuration["bucket"], IsNil)
c.Assert(dc.Config.TerraformBackendDefaults.Configuration["impersonate_service_account"], IsNil)
c.Assert(dc.Config.TerraformBackendDefaults.Configuration["prefix"], IsNil)
be := dc.Config.TerraformBackendDefaults
c.Check(be, DeepEquals, TerraformBackend{})

cliBEType := "gcs"
cliBEBucket := "a_bucket"
Expand All @@ -979,10 +965,13 @@ func (s *MySuite) TestSetBackendConfig(c *C) {
err := dc.SetBackendConfig(cliBEConfigVars)

c.Assert(err, IsNil)
c.Assert(dc.Config.TerraformBackendDefaults.Type, Equals, cliBEType)
c.Assert(dc.Config.TerraformBackendDefaults.Configuration["bucket"], Equals, cliBEBucket)
c.Assert(dc.Config.TerraformBackendDefaults.Configuration["impersonate_service_account"], Equals, cliBESA)
c.Assert(dc.Config.TerraformBackendDefaults.Configuration["prefix"], Equals, cliBEPrefix)
be = dc.Config.TerraformBackendDefaults
c.Check(be.Type, Equals, cliBEType)
c.Check(be.Configuration.Items(), DeepEquals, map[string]cty.Value{
"bucket": cty.StringVal(cliBEBucket),
"impersonate_service_account": cty.StringVal(cliBESA),
"prefix": cty.StringVal(cliBEPrefix),
})

// Failure: Variable without '='
dc = getDeploymentConfigForTest()
Expand Down Expand Up @@ -1207,13 +1196,10 @@ func (s *MySuite) TestCheckBackends(c *C) {
}

{ // OK. No variables used
b := TerraformBackend{
Type: "gcs",
Configuration: map[string]interface{}{
"bucket": "trenta",
"impersonate_service_account": "who",
},
}
b := TerraformBackend{Type: "gcs"}
b.Configuration.
Set("bucket", cty.StringVal("trenta")).
Set("impersonate_service_account", cty.StringVal("who"))
c.Check(check(b), IsNil)
}

Expand Down Expand Up @@ -1243,21 +1229,19 @@ func (s *MySuite) TestCheckBackends(c *C) {
}

{ // FAIL. Variable in defaults configuration
b := TerraformBackend{
Type: "gcs",
Configuration: map[string]interface{}{"bucket": "$(trenta)"},
}
b := TerraformBackend{Type: "gcs"}
b.Configuration.Set("bucket", cty.StringVal("$(trenta)"))
c.Check(check(b), ErrorMatches, ".*bucket.*trenta.*")
}

{ // OK. handles nested configuration
b := TerraformBackend{
Type: "gcs",
Configuration: map[string]interface{}{
"bucket": "trenta",
"complex": map[string]string{"alpha": "a", "beta": "b"},
},
}
b := TerraformBackend{Type: "gcs"}
b.Configuration.
Set("bucket", cty.StringVal("trenta")).
Set("complex", cty.MapVal(map[string]cty.Value{
"alpha": cty.StringVal("a"),
"beta": cty.StringVal("b"),
}))
c.Check(check(b), IsNil)
}
}
Expand Down
163 changes: 163 additions & 0 deletions pkg/config/dict.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
"encoding/json"
"fmt"

"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
ctyJson "github.com/zclconf/go-cty/cty/json"
"gopkg.in/yaml.v3"
)

// Dict maps string key to cty.Value.
// Zero Dict value is initialized (as oposed to nil map).
type Dict struct {
m map[string]cty.Value
}

// Get returns stored value or cty.NilVal.
func (d *Dict) Get(k string) cty.Value {
if d.m == nil {
return cty.NilVal
}
return d.m[k]
}

// Has tests if key is present in map.
func (d *Dict) Has(k string) bool {
if d.m == nil {
return false
}
_, ok := d.m[k]
return ok
}

// Set adds/overrides value by key.
// Returns refernce to Dict-self.
func (d *Dict) Set(k string, v cty.Value) *Dict {
if d.m == nil {
d.m = map[string]cty.Value{k: v}
} else {
d.m[k] = v
}
return d
}

// Items returns instance of map[sring]cty.Value
// will same set of key-value pairs as stored in Dict.
// This map is a copy, changes to returned map have no effect on the Dict.
func (d *Dict) Items() map[string]cty.Value {
m := map[string]cty.Value{}
if d.m != nil {
for k, v := range d.m {
m[k] = v
}
}
return m
}

// yamlValue is wrapper around cty.Value to handle YAML unmarshal.
type yamlValue struct {
v cty.Value
}

// UnmarshalYAML implements custom YAML unmarshaling.
func (y *yamlValue) UnmarshalYAML(n *yaml.Node) error {
var err error
switch n.Kind {
case yaml.ScalarNode:
err = y.unmarshalScalar(n)
case yaml.MappingNode:
err = y.unmarshalObject(n)
case yaml.SequenceNode:
err = y.unmarshalTuple(n)
default:
err = fmt.Errorf("line %d: cannot decode node with unknown kind %d", n.Line, n.Kind)
}
return err
}

func (y *yamlValue) unmarshalScalar(n *yaml.Node) error {
var s interface{}
if err := n.Decode(&s); err != nil {
return err
}
ty, err := gocty.ImpliedType(s)
if err != nil {
return err
}
y.v, err = gocty.ToCtyValue(s, ty)
return err
}

func (y *yamlValue) unmarshalObject(n *yaml.Node) error {
var my map[string]yamlValue
if err := n.Decode(&my); err != nil {
return err
}
mv := map[string]cty.Value{}
for k, y := range my {
mv[k] = y.v
}
y.v = cty.ObjectVal(mv)
return nil
}

func (y *yamlValue) unmarshalTuple(n *yaml.Node) error {
var ly []yamlValue
if err := n.Decode(&ly); err != nil {
return err
}
lv := []cty.Value{}
for _, y := range ly {
lv = append(lv, y.v)
}
y.v = cty.TupleVal(lv)
return nil
}

// UnmarshalYAML implements custom YAML unmarshaling.
func (d *Dict) UnmarshalYAML(n *yaml.Node) error {
var m map[string]yamlValue
if err := n.Decode(&m); err != nil {
return err
}
for k, y := range m {
d.Set(k, y.v)
}
return nil
}

// MarshalYAML implements custom YAML marshaling.
func (d Dict) MarshalYAML() (interface{}, error) {
mi := map[string]interface{}{}
for k, v := range d.Items() {
j := ctyJson.SimpleJSONValue{Value: v}
b, err := j.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to marshal JSON: %v", err)
}
var g interface{}
err = json.Unmarshal(b, &g)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
}
mi[k] = g
}
return mi, nil
}
Loading

0 comments on commit ffebeb0

Please sign in to comment.