diff --git a/controllers/tc000061_vars_hcl_input_test.go b/controllers/tc000061_vars_hcl_input_test.go index de0fd578..38659e3a 100644 --- a/controllers/tc000061_vars_hcl_input_test.go +++ b/controllers/tc000061_vars_hcl_input_test.go @@ -137,7 +137,7 @@ func Test_000061_vars_hcl_input_test(t *testing.T) { return -1, err } return len(outputSecret.Data), nil - }, timeout, interval).Should(Equal(4)) + }, timeout, interval).Should(Equal(7)) By("checking that the TF output secrets contains the correct output provisioned by the TF hello world") // Value is a JSON representation of TF's OutputMeta @@ -145,20 +145,26 @@ func Test_000061_vars_hcl_input_test(t *testing.T) { "Name": "tf-output-" + terraformName, "Namespace": "flux-system", "Values": map[string]string{ - "cluster_id": "eu-test-1:stg:winter-squirrel", - "active": "true", - "node_count": "10", - "azs": `["eu-test-1a","eu-test-1b","eu-test-1c"]`, + "cluster_id": "eu-test-1:stg:winter-squirrel", + "active": "true", + "active.type": `"bool"`, + "node_count": "10", + "node_count.type": `"number"`, + "azs": "[\n \"eu-test-1a\",\n \"eu-test-1b\",\n \"eu-test-1c\"\n ]", + "azs.type": "[\n \"list\",\n \"string\"\n ]", }, "OwnerRef[0]": string(createdHelloWorldTF.UID), } g.Eventually(func() (map[string]interface{}, error) { err := k8sClient.Get(ctx, outputKey, &outputSecret) values := map[string]string{ - "cluster_id": string(outputSecret.Data["cluster_id"]), - "active": string(outputSecret.Data["active"]), - "node_count": string(outputSecret.Data["node_count"]), - "azs": string(outputSecret.Data["azs"]), + "cluster_id": string(outputSecret.Data["cluster_id"]), + "active": string(outputSecret.Data["active"]), + "active.type": string(outputSecret.Data["active.type"]), + "node_count": string(outputSecret.Data["node_count"]), + "node_count.type": string(outputSecret.Data["node_count.type"]), + "azs": string(outputSecret.Data["azs"]), + "azs.type": string(outputSecret.Data["azs.type"]), } return map[string]interface{}{ "Name": outputSecret.Name, diff --git a/controllers/tf_controller_outputs.go b/controllers/tf_controller_outputs.go index 84068397..c3033dd0 100644 --- a/controllers/tf_controller_outputs.go +++ b/controllers/tf_controller_outputs.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "encoding/json" "fmt" "sort" "strings" @@ -75,6 +74,9 @@ func (r *TerraformReconciler) processOutputs(ctx context.Context, runnerClient r log := ctrl.LoggerFrom(ctx) objectKey := types.NamespacedName{Namespace: terraform.Namespace, Name: terraform.Name} + // OutputMeta has + // 1. type + // 2. value outputs := map[string]tfexec.OutputMeta{} var err error terraform, err = r.obtainOutputs(ctx, terraform, tfInstance, runnerClient, revision, &outputs) @@ -133,81 +135,37 @@ func (r *TerraformReconciler) writeOutput(ctx context.Context, terraform infrav1 wots := terraform.Spec.WriteOutputsToSecret data := map[string][]byte{} - // if not specified .spec.writeOutputsToSecret.outputs, - // then it means export all outputs + var filteredOutputs map[string]tfexec.OutputMeta if len(wots.Outputs) == 0 { - for output, v := range outputs { - ct, err := ctyjson.UnmarshalType(v.Type) - if err != nil { - return terraform, err - } - // if it's a string, we can embed it directly into Secret's data - switch ct { - case cty.String: - cv, err := ctyjson.Unmarshal(v.Value, ct) - if err != nil { - return terraform, err - } - data[output] = []byte(cv.AsString()) - // there's no need to unmarshal and convert to []byte - // we'll just pass the []byte directly from OutputMeta Value - case cty.Number, cty.Bool: - data[output] = v.Value - default: - outputBytes, err := json.Marshal(v.Value) - if err != nil { - return terraform, err - } - data[output] = outputBytes - } - } + filteredOutputs = outputs } else { - // filter only defined output - // output maybe contain mapping output:mapped_name - for _, outputMapping := range wots.Outputs { - parts := strings.SplitN(outputMapping, ":", 2) - var output string - var mappedTo string - if len(parts) == 1 { - output = parts[0] - mappedTo = parts[0] - // no mapping - } else if len(parts) == 2 { - output = parts[0] - mappedTo = parts[1] - } else { - log.Error(fmt.Errorf("invalid mapping format"), outputMapping) - continue - } + if result, err := filterOutputs(outputs, wots.Outputs); err != nil { + return infrav1.TerraformNotReady( + terraform, + revision, + infrav1.OutputsWritingFailedReason, + err.Error(), + ), err + } else { + filteredOutputs = result + } + } - v, exist := outputs[output] - if !exist { - log.Error(fmt.Errorf("output not found"), output) - continue - } + for outputOrAlias, outputMeta := range filteredOutputs { + ct, err := ctyjson.UnmarshalType(outputMeta.Type) + if err != nil { + return terraform, err + } - ct, err := ctyjson.UnmarshalType(v.Type) + if ct == cty.String { + cv, err := ctyjson.Unmarshal(outputMeta.Value, ct) if err != nil { return terraform, err } - switch ct { - case cty.String: - cv, err := ctyjson.Unmarshal(v.Value, ct) - if err != nil { - return terraform, err - } - data[mappedTo] = []byte(cv.AsString()) - // there's no need to unmarshal and convert to []byte - // we'll just pass the []byte directly from OutputMeta Value - case cty.Number, cty.Bool: - data[mappedTo] = v.Value - default: - outputBytes, err := json.Marshal(v.Value) - if err != nil { - return terraform, err - } - data[mappedTo] = outputBytes - } + data[outputOrAlias] = []byte(cv.AsString()) + } else { + data[outputOrAlias] = outputMeta.Value + data[outputOrAlias+".type"] = outputMeta.Type } } @@ -243,3 +201,41 @@ func (r *TerraformReconciler) writeOutput(ctx context.Context, terraform infrav1 return infrav1.TerraformOutputsWritten(terraform, revision, "Outputs written"), nil } + +func filterOutputs(outputs map[string]tfexec.OutputMeta, outputsToWrite []string) (map[string]tfexec.OutputMeta, error) { + if outputs == nil || outputsToWrite == nil { + return nil, fmt.Errorf("input maps or outputsToWrite slice cannot be nil") + } + + filteredOutputs := make(map[string]tfexec.OutputMeta) + for _, outputMapping := range outputsToWrite { + if len(outputMapping) == 0 { + return nil, fmt.Errorf("output mapping cannot be empty") + } + + // parse output mapping (output[:alias]) + parts := strings.SplitN(outputMapping, ":", 2) + var ( + output string + alias string + ) + if len(parts) == 1 { + output = parts[0] + alias = parts[0] + } else if len(parts) == 2 { + output = parts[0] + alias = parts[1] + } else { + return nil, fmt.Errorf("invalid output mapping format: %s", outputMapping) + } + + outputMeta, exist := outputs[output] + if !exist { + return nil, fmt.Errorf("output not found: %s", output) + } + + filteredOutputs[alias] = outputMeta + } + + return filteredOutputs, nil +} diff --git a/docs/References/terraform.md b/docs/References/terraform.md index ccb6e950..1e7ef157 100644 --- a/docs/References/terraform.md +++ b/docs/References/terraform.md @@ -1485,7 +1485,7 @@ int32 (Optional) -

Parallelism limits the number of concurrent operations of Terraform apply step.

+

Parallelism limits the number of concurrent operations of Terraform apply step. Zero (0) means using the default value.

@@ -1969,7 +1969,7 @@ int32 (Optional) -

Parallelism limits the number of concurrent operations of Terraform apply step.

+

Parallelism limits the number of concurrent operations of Terraform apply step. Zero (0) means using the default value.

diff --git a/go.mod b/go.mod index 155c8de1..5f848d90 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/google/uuid v1.3.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-retryablehttp v0.7.1 + github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 github.com/hashicorp/terraform-exec v0.16.1 github.com/hashicorp/terraform-json v0.13.0 github.com/onsi/gomega v1.24.0 @@ -60,6 +61,9 @@ require ( github.com/Microsoft/go-winio v0.5.2 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg v1.0.0 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect @@ -116,7 +120,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/magiconair/properties v1.8.6 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect @@ -149,6 +153,7 @@ require ( github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/testify v1.8.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/theckman/yacspin v0.13.12 // indirect github.com/xlab/treeprint v1.1.0 // indirect diff --git a/go.sum b/go.sum index 361da1e7..689e84be 100644 --- a/go.sum +++ b/go.sum @@ -95,12 +95,19 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= +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/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -157,6 +164,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -287,6 +295,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -309,6 +319,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= @@ -399,6 +410,7 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsD github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -408,6 +420,7 @@ github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= @@ -426,6 +439,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/hc-install v0.4.0 h1:cZkRFr1WVa0Ty6x5fTvL1TuO1flul231rWkGH92oYYk= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 h1:PFfGModn55JA0oBsvFghhj0v93me+Ctr3uHC/UmFAls= +github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= @@ -478,6 +493,8 @@ 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 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= @@ -487,8 +504,9 @@ github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= @@ -514,6 +532,7 @@ github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HK github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= @@ -554,11 +573,13 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= @@ -620,6 +641,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -648,6 +670,7 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -658,8 +681,9 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -667,8 +691,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= @@ -676,6 +701,7 @@ github.com/tf-controller/terraform-exec v0.15.1-0.20220809152546-4850a69faedb h1 github.com/tf-controller/terraform-exec v0.15.1-0.20220809152546-4850a69faedb/go.mod h1:aj0lVshy8l+MHhFNoijNHtqTJQI3Xlowv5EOsEaGO7M= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= @@ -689,6 +715,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/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.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= @@ -728,6 +755,7 @@ go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -826,6 +854,7 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1183,6 +1212,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= k8s.io/api v0.24.0/go.mod h1:5Jl90IUrJHUJYEMANRURMiVvJ0g7Ax7r3R1bqO8zx8I= k8s.io/api v0.25.4 h1:3YO8J4RtmG7elEgaWMb4HgmpS2CfY1QlaOz9nwB+ZSs= k8s.io/api v0.25.4/go.mod h1:IG2+RzyPQLllQxnhzD8KQNEu4c4YvyDTpSMztf4A0OQ= diff --git a/runner/read_inputs_test.go b/runner/read_inputs_test.go new file mode 100644 index 00000000..43704f7d --- /dev/null +++ b/runner/read_inputs_test.go @@ -0,0 +1,107 @@ +package runner + +import ( + "context" + . "github.com/onsi/gomega" + "testing" + + "github.com/go-logr/logr" + "github.com/hashicorp/hcl2/hcldec" + "github.com/hashicorp/hcl2/hclparse" + infrav1 "github.com/weaveworks/tf-controller/api/v1alpha1" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestReadInputsForGenerateVarsForTF(t *testing.T) { + g := NewGomegaWithT(t) + + terraform := &infrav1.Terraform{ + ObjectMeta: metav1.ObjectMeta{ + Name: "terraform-1", + Namespace: "default", + }, + Spec: infrav1.TerraformSpec{ + ReadInputsFromSecrets: []infrav1.ReadInputsFromSecretSpec{ + { + Name: "secret-1", + As: "secret_1", + }, + }, + }, + } + + hclParser := hclparse.NewParser() + file, diag := hclParser.ParseHCL([]byte(` +a = 42 +b = "str" +c = true +d = false +e = null +f = [1, 2, 3, 1] +g = [1, "a", true] +h = { a = 1, b = 2, c = 3 } +i = [1, 2, 3, 1] +j = { a = 1, b = 2, c = 3 } +`), "test.hcl") + g.Expect(diag.HasErrors()).To(BeFalse()) + spec := &hcldec.ObjectSpec{ + "a": &hcldec.AttrSpec{Name: "a", Type: cty.Number}, + "b": &hcldec.AttrSpec{Name: "b", Type: cty.String}, + "c": &hcldec.AttrSpec{Name: "c", Type: cty.Bool}, + "d": &hcldec.AttrSpec{Name: "d", Type: cty.Bool}, + "e": &hcldec.AttrSpec{Name: "e", Type: cty.DynamicPseudoType}, + "f": &hcldec.AttrSpec{Name: "f", Type: cty.List(cty.Number)}, + "g": &hcldec.AttrSpec{Name: "g", Type: cty.Tuple([]cty.Type{cty.Number, cty.String, cty.Bool})}, + "h": &hcldec.AttrSpec{Name: "h", Type: cty.Map(cty.Number)}, + "i": &hcldec.AttrSpec{Name: "i", Type: cty.Set(cty.Number)}, + "j": &hcldec.AttrSpec{Name: "j", Type: cty.Object(map[string]cty.Type{"a": cty.Number, "b": cty.Number, "c": cty.Number})}, + } + v, err := hcldec.Decode(file.Body, spec, nil) + g.Expect(err).To(BeNil()) + + data := map[string][]byte{} + for k, vv := range v.AsValueMap() { + if k == "b" { + data[k] = []byte(vv.AsString()) + continue + } + + tt, err := ctyjson.MarshalType(vv.Type()) + g.Expect(err).To(BeNil()) + data[k+".type"] = tt + raw, err := ctyjson.Marshal(vv, vv.Type()) + g.Expect(err).To(BeNil()) + data[k] = raw + } + + fixture := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "secret-1", + Namespace: "default", + }, + Data: data, + } + + cli := fake.NewClientBuilder().WithObjects(fixture).Build() + + inputs, err2 := readInputsForGenerateVarsForTF(context.TODO(), logr.Discard(), cli, terraform) + g.Expect(err2).To(BeNil()) + g.Expect(inputs["secret_1"]).To(Equal(map[string]interface{}{ + "a": float64(42), + "b": "str", + "c": true, + "d": false, + "e": nil, + "f": []interface{}{float64(1), float64(2), float64(3), float64(1)}, + "g": []interface{}{float64(1), "a", true}, + "h": map[string]interface{}{"a": float64(1), "b": float64(2), "c": float64(3)}, + "i": []interface{}{float64(1), float64(2), float64(3)}, + "j": map[string]interface{}{"a": float64(1), "b": float64(2), "c": float64(3)}, + })) + +} diff --git a/runner/server.go b/runner/server.go index a36ac6c4..5c1f7531 100644 --- a/runner/server.go +++ b/runner/server.go @@ -3,18 +3,8 @@ package runner import ( "bytes" "context" - "encoding/json" "errors" "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "reflect" - "strings" - "text/template" - - "github.com/Masterminds/sprig/v3" securejoin "github.com/cyphar/filepath-securejoin" "github.com/fluxcd/pkg/untar" "github.com/go-logr/logr" @@ -24,14 +14,19 @@ import ( "github.com/weaveworks/tf-controller/utils" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "io/ioutil" corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "os" + "os/exec" + "path/filepath" + "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "strings" ) const ( @@ -391,501 +386,6 @@ func (r *TerraformRunnerServer) SelectWorkspace(ctx context.Context, req *Worksp return &WorkspaceReply{Message: "ok"}, nil } -// GenerateVarsForTF renders the Terraform variables as a json file for the given inputs -// variables supplied in the varsFrom field will override those specified in the spec -func (r *TerraformRunnerServer) GenerateVarsForTF(ctx context.Context, req *GenerateVarsForTFRequest) (*GenerateVarsForTFReply, error) { - log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) - log.Info("setting up the input variables") - - // use from the cached object - terraform := *r.terraform - - vars := map[string]*apiextensionsv1.JSON{} - - inputs := map[string]interface{}{} - if len(terraform.Spec.ReadInputsFromSecrets) > 0 { - for _, readSpec := range terraform.Spec.ReadInputsFromSecrets { - secret := corev1.Secret{} - err := r.Get(ctx, types.NamespacedName{Namespace: terraform.Namespace, Name: readSpec.Name}, &secret) - if err != nil { - log.Error(err, "unable to get secret", "secret", readSpec.Name) - return nil, err - } - - // outputs are always strings - data := map[string]interface{}{} - for k, v := range secret.Data { - data[k] = string(v) - } - - inputs[readSpec.As] = data - } - } - - log.Info("mapping the Spec.Values") - if terraform.Spec.Values != nil { - tmpl, err := template. - New("values"). - Delims("${{", "}}"). - Parse(string(terraform.Spec.Values.Raw)) - if err != nil { - log.Error(err, "unable to parse values as template") - return nil, err - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, inputs); err != nil { - log.Error(err, "unable to execute values template") - return nil, err - } - - vars["values"] = &apiextensionsv1.JSON{Raw: buf.Bytes()} - } - - log.Info("mapping the Spec.Vars") - if len(terraform.Spec.Vars) > 0 { - for _, v := range terraform.Spec.Vars { - vars[v.Name] = v.Value - } - } - - log.Info("mapping the Spec.VarsFrom") - // varsFrom overwrite vars - for _, vf := range terraform.Spec.VarsFrom { - objectKey := types.NamespacedName{ - Namespace: terraform.Namespace, - Name: vf.Name, - } - if vf.Kind == "Secret" { - var s corev1.Secret - err := r.Get(ctx, objectKey, &s) - if err != nil && vf.Optional == false { - log.Error(err, "unable to get object key", "objectKey", objectKey, "secret", s.ObjectMeta.Name) - return nil, err - } - // if VarsKeys is null, use all - if vf.VarsKeys == nil { - for key, val := range s.Data { - vars[key], err = utils.JSONEncodeBytes(val) - if err != nil { - err := fmt.Errorf("failed to encode key %s with error: %w", key, err) - log.Error(err, "encoding failure") - return nil, err - } - } - } else { - for _, key := range vf.VarsKeys { - vars[key], err = utils.JSONEncodeBytes(s.Data[key]) - if err != nil { - err := fmt.Errorf("failed to encode key %s with error: %w", key, err) - log.Error(err, "encoding failure") - return nil, err - } - } - } - } else if vf.Kind == "ConfigMap" { - var cm corev1.ConfigMap - err := r.Get(ctx, objectKey, &cm) - if err != nil && vf.Optional == false { - log.Error(err, "unable to get object key", "objectKey", objectKey, "configmap", cm.ObjectMeta.Name) - return nil, err - } - - // if VarsKeys is null, use all - if vf.VarsKeys == nil { - for key, val := range cm.Data { - vars[key], err = utils.JSONEncodeBytes([]byte(val)) - if err != nil { - err := fmt.Errorf("failed to encode key %s with error: %w", key, err) - log.Error(err, "encoding failure") - return nil, err - } - } - for key, val := range cm.BinaryData { - vars[key], err = utils.JSONEncodeBytes(val) - if err != nil { - err := fmt.Errorf("failed to encode key %s with error: %w", key, err) - log.Error(err, "encoding failure") - return nil, err - } - } - } else { - for _, key := range vf.VarsKeys { - if val, ok := cm.Data[key]; ok { - vars[key], err = utils.JSONEncodeBytes([]byte(val)) - if err != nil { - err := fmt.Errorf("failed to encode key %s with error: %w", key, err) - log.Error(err, "encoding failure") - return nil, err - } - } - if val, ok := cm.BinaryData[key]; ok { - vars[key], err = utils.JSONEncodeBytes(val) - if err != nil { - log.Error(err, "encoding failure") - return nil, err - } - } - } - } - } - } - - jsonBytes, err := json.Marshal(vars) - if err != nil { - log.Error(err, "unable to marshal the data") - return nil, err - } - - varFilePath := filepath.Join(req.WorkingDir, "generated.auto.tfvars.json") - if err := os.WriteFile(varFilePath, jsonBytes, 0644); err != nil { - err = fmt.Errorf("error generating var file: %s", err) - log.Error(err, "unable to write the data to file", "filePath", varFilePath) - return nil, err - } - - return &GenerateVarsForTFReply{Message: "ok"}, nil -} - -func (r *TerraformRunnerServer) GenerateTemplate(ctx context.Context, req *GenerateTemplateRequest) (*GenerateTemplateReply, error) { - log := ctrl.LoggerFrom(ctx).WithName(loggerName) - log.Info("generating the template founds") - - workDir := req.WorkingDir - - // find main.tf.tpl file - mainTfTplPath := filepath.Join(workDir, "main.tf.tpl") - if _, err := os.Stat(mainTfTplPath); os.IsNotExist(err) { - log.Info("main.tf.tpl not found, skipping") - return &GenerateTemplateReply{Message: "ok"}, nil - } - - // marshal the vars - vars := make(map[string]interface{}) - - varFilePath := filepath.Join(req.WorkingDir, "generated.auto.tfvars.json") - jsonBytes, err := ioutil.ReadFile(varFilePath) - if err != nil { - log.Error(err, "unable to read the file", "filePath", varFilePath) - return nil, err - } - - if err := json.Unmarshal(jsonBytes, &vars); err != nil { - log.Error(err, "unable to unmarshal the data") - return nil, err - } - - // render the template - // we use Helm-like syntax for the template - tmpl, parseErr := template. - New("main.tf.tpl"). - Delims("{{", "}}"). - Funcs(sprig.TxtFuncMap()). - ParseFiles(mainTfTplPath) - if parseErr != nil { - log.Error(parseErr, "unable to parse the template", "filePath", mainTfTplPath) - return nil, parseErr - } - - mainTfPath := filepath.Join(workDir, "main.tf") - f, fileErr := os.Create(mainTfPath) - if fileErr != nil { - log.Error(fileErr, "unable to create the file", "filePath", mainTfPath) - return nil, fileErr - } - - // make it Helm compatible - vars["Values"] = vars["values"] - - if err := tmpl.Execute(f, vars); err != nil { - log.Error(err, "unable to execute the template") - return nil, err - } - - if err := f.Close(); err != nil { - return nil, err - } - - return &GenerateTemplateReply{Message: "ok"}, nil -} - -func (r *TerraformRunnerServer) Plan(ctx context.Context, req *PlanRequest) (*PlanReply, error) { - log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) - log.Info("creating a plan") - ctx, cancel := context.WithCancel(ctx) - go func() { - select { - case <-r.Done: - cancel() - case <-ctx.Done(): - } - }() - - if req.TfInstance != r.InstanceID { - err := fmt.Errorf("no TF instance found") - log.Error(err, "no terraform") - return nil, err - } - - var planOpt []tfexec.PlanOption - if req.Out != "" { - planOpt = append(planOpt, tfexec.Out(req.Out)) - } - - if req.Refresh == false { - planOpt = append(planOpt, tfexec.Refresh(req.Refresh)) - } - - if req.Destroy { - planOpt = append(planOpt, tfexec.Destroy(req.Destroy)) - } - - for _, target := range req.Targets { - planOpt = append(planOpt, tfexec.Target(target)) - } - - drifted, err := r.tf.Plan(ctx, planOpt...) - if err != nil { - st := status.New(codes.Internal, err.Error()) - var stateErr *tfexec.ErrStateLocked - - if errors.As(err, &stateErr) { - st, err = st.WithDetails(&PlanReply{Message: "not ok", StateLockIdentifier: stateErr.ID}) - - if err != nil { - return nil, err - } - } - - log.Error(err, "error creating the plan") - return nil, st.Err() - } - - return &PlanReply{Message: "ok", Drifted: drifted}, nil -} - -func (r *TerraformRunnerServer) ShowPlanFileRaw(ctx context.Context, req *ShowPlanFileRawRequest) (*ShowPlanFileRawReply, error) { - log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) - log.Info("show the raw plan file") - if req.TfInstance != r.InstanceID { - err := fmt.Errorf("no TF instance found") - log.Error(err, "no terraform") - return nil, err - } - - rawOutput, err := r.tf.ShowPlanFileRaw(ctx, req.Filename) - if err != nil { - log.Error(err, "unable to get the raw plan output") - return nil, err - } - - return &ShowPlanFileRawReply{RawOutput: rawOutput}, nil -} - -func (r *TerraformRunnerServer) ShowPlanFile(ctx context.Context, req *ShowPlanFileRequest) (*ShowPlanFileReply, error) { - log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) - log.Info("show the raw plan file") - if req.TfInstance != r.InstanceID { - err := fmt.Errorf("no TF instance found") - log.Error(err, "no terraform") - return nil, err - } - - plan, err := r.tf.ShowPlanFile(ctx, req.Filename) - if err != nil { - log.Error(err, "unable to get the json plan output") - return nil, err - } - - jsonBytes, err := json.Marshal(plan) - if err != nil { - log.Error(err, "unable to marshal the plan to json") - return nil, err - } - - return &ShowPlanFileReply{JsonOutput: jsonBytes}, nil -} - -func (r *TerraformRunnerServer) SaveTFPlan(ctx context.Context, req *SaveTFPlanRequest) (*SaveTFPlanReply, error) { - log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) - log.Info("save the plan") - if req.TfInstance != r.InstanceID { - err := fmt.Errorf("no TF instance found") - log.Error(err, "no terraform") - return nil, err - } - - var tfplan []byte - if req.BackendCompletelyDisable { - tfplan = []byte("dummy plan") - } else { - var err error - tfplan, err = ioutil.ReadFile(filepath.Join(r.tf.WorkingDir(), TFPlanName)) - if err != nil { - err = fmt.Errorf("error running Plan: %s", err) - log.Error(err, "unable to run the plan") - return nil, err - } - } - - planRev := strings.Replace(req.Revision, "/", "-", 1) - planName := "plan-" + planRev - if err := r.writePlanAsSecret(ctx, req.Name, req.Namespace, log, planName, tfplan, "", req.Uuid); err != nil { - return nil, err - } - - if r.terraform.Spec.StoreReadablePlan == "json" { - planObj, err := r.tf.ShowPlanFile(ctx, TFPlanName) - if err != nil { - log.Error(err, "unable to get the plan output for json") - return nil, err - } - jsonBytes, err := json.Marshal(planObj) - if err != nil { - log.Error(err, "unable to marshal the plan to json") - return nil, err - } - - if err := r.writePlanAsSecret(ctx, req.Name, req.Namespace, log, planName, jsonBytes, ".json", req.Uuid); err != nil { - return nil, err - } - } else if r.terraform.Spec.StoreReadablePlan == "human" { - rawOutput, err := r.tf.ShowPlanFileRaw(ctx, TFPlanName) - if err != nil { - log.Error(err, "unable to get the plan output for human") - return nil, err - } - - if err := r.writePlanAsConfigMap(ctx, req.Name, req.Namespace, log, planName, rawOutput, "", req.Uuid); err != nil { - return nil, err - } - } - - return &SaveTFPlanReply{Message: "ok"}, nil -} - -func (r *TerraformRunnerServer) writePlanAsSecret(ctx context.Context, name string, namespace string, log logr.Logger, planName string, tfplan []byte, suffix string, uuid string) error { - secretName := "tfplan-" + r.terraform.WorkspaceName() + "-" + name + suffix - tfplanObjectKey := types.NamespacedName{Name: secretName, Namespace: namespace} - var tfplanSecret corev1.Secret - tfplanSecretExists := true - - if err := r.Client.Get(ctx, tfplanObjectKey, &tfplanSecret); err != nil { - if apierrors.IsNotFound(err) { - tfplanSecretExists = false - } else { - err = fmt.Errorf("error getting tfplanSecret: %s", err) - log.Error(err, "unable to get the plan secret") - return err - } - } - - if tfplanSecretExists { - if err := r.Client.Delete(ctx, &tfplanSecret); err != nil { - err = fmt.Errorf("error deleting tfplanSecret: %s", err) - log.Error(err, "unable to delete the plan secret") - return err - } - } - - tfplan, err := utils.GzipEncode(tfplan) - if err != nil { - log.Error(err, "unable to encode the plan revision", "planName", planName) - return err - } - - tfplanData := map[string][]byte{TFPlanName: tfplan} - tfplanSecret = corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - Annotations: map[string]string{ - "encoding": "gzip", - SavedPlanSecretAnnotation: planName, - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: infrav1.GroupVersion.Group + "/" + infrav1.GroupVersion.Version, - Kind: infrav1.TerraformKind, - Name: name, - UID: types.UID(uuid), - }, - }, - }, - Type: corev1.SecretTypeOpaque, - Data: tfplanData, - } - - if err := r.Client.Create(ctx, &tfplanSecret); err != nil { - err = fmt.Errorf("error recording plan status: %s", err) - log.Error(err, "unable to create plan secret") - return err - } - - return nil -} - -func (r *TerraformRunnerServer) writePlanAsConfigMap(ctx context.Context, name string, namespace string, log logr.Logger, planName string, tfplan string, suffix string, uuid string) error { - configMapName := "tfplan-" + r.terraform.WorkspaceName() + "-" + name + suffix - tfplanObjectKey := types.NamespacedName{Name: configMapName, Namespace: namespace} - var tfplanCM corev1.ConfigMap - tfplanCMExists := true - - if err := r.Client.Get(ctx, tfplanObjectKey, &tfplanCM); err != nil { - if apierrors.IsNotFound(err) { - tfplanCMExists = false - } else { - err = fmt.Errorf("error getting tfplanSecret: %s", err) - log.Error(err, "unable to get the plan configmap") - return err - } - } - - if tfplanCMExists { - if err := r.Client.Delete(ctx, &tfplanCM); err != nil { - err = fmt.Errorf("error deleting tfplanSecret: %s", err) - log.Error(err, "unable to delete the plan configmap") - return err - } - } - - tfplanData := map[string]string{TFPlanName: tfplan} - tfplanCM = corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: configMapName, - Namespace: namespace, - Annotations: map[string]string{ - SavedPlanSecretAnnotation: planName, - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: infrav1.GroupVersion.Group + "/" + infrav1.GroupVersion.Version, - Kind: infrav1.TerraformKind, - Name: name, - UID: types.UID(uuid), - }, - }, - }, - Data: tfplanData, - } - - if err := r.Client.Create(ctx, &tfplanCM); err != nil { - err = fmt.Errorf("error recording plan status: %s", err) - log.Error(err, "unable to create plan configmap") - return err - } - - return nil -} - func (r *TerraformRunnerServer) LoadTFPlan(ctx context.Context, req *LoadTFPlanRequest) (*LoadTFPlanReply, error) { log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) log.Info("loading plan from secret") diff --git a/runner/server_generate_template.go b/runner/server_generate_template.go new file mode 100644 index 00000000..ef8a73c0 --- /dev/null +++ b/runner/server_generate_template.go @@ -0,0 +1,73 @@ +package runner + +import ( + "context" + "encoding/json" + "github.com/Masterminds/sprig/v3" + "io/ioutil" + "os" + "path/filepath" + "sigs.k8s.io/controller-runtime" + "text/template" +) + +func (r *TerraformRunnerServer) GenerateTemplate(ctx context.Context, req *GenerateTemplateRequest) (*GenerateTemplateReply, error) { + log := controllerruntime.LoggerFrom(ctx).WithName(loggerName) + log.Info("generating the template founds") + + workDir := req.WorkingDir + + // find main.tf.tpl file + mainTfTplPath := filepath.Join(workDir, "main.tf.tpl") + if _, err := os.Stat(mainTfTplPath); os.IsNotExist(err) { + log.Info("main.tf.tpl not found, skipping") + return &GenerateTemplateReply{Message: "ok"}, nil + } + + // marshal the vars + vars := make(map[string]interface{}) + + varFilePath := filepath.Join(req.WorkingDir, "generated.auto.tfvars.json") + jsonBytes, err := ioutil.ReadFile(varFilePath) + if err != nil { + log.Error(err, "unable to read the file", "filePath", varFilePath) + return nil, err + } + + if err := json.Unmarshal(jsonBytes, &vars); err != nil { + log.Error(err, "unable to unmarshal the data") + return nil, err + } + + // render the template + // we use Helm-like syntax for the template + tmpl, parseErr := template.New("main.tf.tpl"). + Delims("{{", "}}"). + Funcs(sprig.TxtFuncMap()). + ParseFiles(mainTfTplPath) + if parseErr != nil { + log.Error(parseErr, "unable to parse the template", "filePath", mainTfTplPath) + return nil, parseErr + } + + mainTfPath := filepath.Join(workDir, "main.tf") + f, fileErr := os.Create(mainTfPath) + if fileErr != nil { + log.Error(fileErr, "unable to create the file", "filePath", mainTfPath) + return nil, fileErr + } + + // make it Helm compatible + vars["Values"] = vars["values"] + + if err := tmpl.Execute(f, vars); err != nil { + log.Error(err, "unable to execute the template") + return nil, err + } + + if err := f.Close(); err != nil { + return nil, err + } + + return &GenerateTemplateReply{Message: "ok"}, nil +} diff --git a/runner/server_generate_tf_vars.go b/runner/server_generate_tf_vars.go new file mode 100644 index 00000000..87ddc82d --- /dev/null +++ b/runner/server_generate_tf_vars.go @@ -0,0 +1,348 @@ +package runner + +import ( + "bytes" + "context" + json2 "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/go-logr/logr" + "github.com/weaveworks/tf-controller/api/v1alpha1" + "github.com/weaveworks/tf-controller/utils" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/json" + "k8s.io/api/core/v1" + v12 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func ctyValueToGoValue(ctyValue cty.Value) (interface{}, error) { + var goValue interface{} + + if ctyValue.IsNull() { + return nil, nil + } + + if ctyValue.Type() == cty.String { + goValue = ctyValue.AsString() + return goValue, nil + } + + if ctyValue.Type() == cty.Number { + goValue, _ = ctyValue.AsBigFloat().Float64() + return goValue, nil + } + + if ctyValue.Type() == cty.Bool { + goValue = ctyValue.True() + return goValue, nil + } + + if ctyValue.Type().IsListType() { + list := make([]interface{}, 0, ctyValue.LengthInt()) + for it := ctyValue.ElementIterator(); it.Next(); { + _, v := it.Element() + goVal, err := ctyValueToGoValue(v) + if err != nil { + return nil, err + } + list = append(list, goVal) + } + goValue = list + return goValue, nil + } + + if ctyValue.Type().IsSetType() { + result := make([]interface{}, 0, ctyValue.LengthInt()) + set := make(map[interface{}]struct{}) + for it := ctyValue.ElementIterator(); it.Next(); { + _, v := it.Element() + goVal, err := ctyValueToGoValue(v) + if err != nil { + return nil, err + } + + if _, exist := set[goVal]; !exist { + set[goVal] = struct{}{} + result = append(result, goVal) + } + } + goValue = result + return goValue, nil + } + + if ctyValue.Type().IsMapType() { + m := make(map[string]interface{}) + for it := ctyValue.ElementIterator(); it.Next(); { + k, v := it.Element() + goKey, err := ctyValueToGoValue(k) + if err != nil { + return nil, err + } + key, ok := goKey.(string) + if !ok { + return nil, fmt.Errorf("map key must be string, got %T", goKey) + } + goVal, err := ctyValueToGoValue(v) + if err != nil { + return nil, err + } + m[key] = goVal + } + goValue = m + return goValue, nil + } + + if ctyValue.Type().IsTupleType() { + t := make([]interface{}, 0, ctyValue.LengthInt()) + for it := ctyValue.ElementIterator(); it.Next(); { + _, v := it.Element() + goVal, err := ctyValueToGoValue(v) + if err != nil { + return nil, err + } + t = append(t, goVal) + } + goValue = t + return goValue, nil + } + + if ctyValue.Type().IsObjectType() { + o := make(map[string]interface{}) + for it := ctyValue.ElementIterator(); it.Next(); { + k, v := it.Element() + goKey, err := ctyValueToGoValue(k) + if err != nil { + return nil, err + } + key, ok := goKey.(string) + if !ok { + return nil, fmt.Errorf("object key must be string, got %T", goKey) + } + goVal, err := ctyValueToGoValue(v) + if err != nil { + return nil, err + } + o[key] = goVal + } + goValue = o + return goValue, nil + } + + return nil, fmt.Errorf("unsupported type: %s", ctyValue.Type().FriendlyName()) +} + +func convertSecretDataToInputs(log logr.Logger, secret *v1.Secret) (map[string]interface{}, error) { + var keys []string + for k := range secret.Data { + if strings.HasSuffix(k, ".type") { + continue + } + keys = append(keys, k) + } + + data := map[string]interface{}{} + for _, key := range keys { + typeInfo, exist := secret.Data[key+".type"] + if exist { + raw := secret.Data[key] + ct, err := json.UnmarshalType(typeInfo) + if err != nil { + log.Error(err, "unable to unmarshal type", "type", string(typeInfo), "key", key) + return nil, err + } + + cv, err := json.Unmarshal(raw, ct) + if err != nil { + log.Error(err, "unable to unmarshal value", "raw", string(raw), "key", key) + return nil, err + } + + result, err := ctyValueToGoValue(cv) + if err != nil { + return nil, err + } + data[key] = result + } else { // this is string + data[key] = string(secret.Data[key]) + } + } + + return data, nil +} + +func getSecretForReadInputs(ctx context.Context, log logr.Logger, r client.Client, objectKey client.ObjectKey) (*v1.Secret, error) { + secret := &v1.Secret{} + err := r.Get(ctx, objectKey, secret) + if err != nil { + log.Error(err, "unable to get secret", "secret", objectKey) + return secret, err + } + return secret, nil +} + +func readInputsForGenerateVarsForTF(ctx context.Context, log logr.Logger, c client.Client, terraform *v1alpha1.Terraform) (map[string]interface{}, error) { + inputs := map[string]interface{}{} + if len(terraform.Spec.ReadInputsFromSecrets) > 0 { + for _, readSpec := range terraform.Spec.ReadInputsFromSecrets { + objectKey := types.NamespacedName{Namespace: terraform.Namespace, Name: readSpec.Name} + secret, err := getSecretForReadInputs(ctx, log, c, objectKey) + if err != nil { + return nil, err + } + data, err := convertSecretDataToInputs(log, secret) + if err != nil { + return nil, err + } + inputs[readSpec.As] = data + } + } + return inputs, nil +} + +// GenerateVarsForTF renders the Terraform variables as a json file for the given inputs +// variables supplied in the varsFrom field will override those specified in the spec +func (r *TerraformRunnerServer) GenerateVarsForTF(ctx context.Context, req *GenerateVarsForTFRequest) (*GenerateVarsForTFReply, error) { + log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) + log.Info("setting up the input variables") + + // use from the cached object + terraform := *r.terraform + + vars := map[string]*v12.JSON{} + + //inputs := map[string]interface{}{} + inputs, err := readInputsForGenerateVarsForTF(ctx, log, r.Client, &terraform) + if err != nil { + return nil, err + } + + log.Info("mapping the Spec.Values") + if terraform.Spec.Values != nil { + tmpl, err := template.New("values"). + Delims("${{", "}}"). + Parse(string(terraform.Spec.Values.Raw)) + if err != nil { + log.Error(err, "unable to parse values as template") + return nil, err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, inputs); err != nil { + log.Error(err, "unable to execute values template") + return nil, err + } + + vars["values"] = &v12.JSON{Raw: buf.Bytes()} + } + + log.Info("mapping the Spec.Vars") + if len(terraform.Spec.Vars) > 0 { + for _, v := range terraform.Spec.Vars { + vars[v.Name] = v.Value + } + } + + log.Info("mapping the Spec.VarsFrom") + // varsFrom overwrite vars + for _, vf := range terraform.Spec.VarsFrom { + objectKey := types.NamespacedName{ + Namespace: terraform.Namespace, + Name: vf.Name, + } + if vf.Kind == "Secret" { + var s v1.Secret + err := r.Get(ctx, objectKey, &s) + if err != nil && vf.Optional == false { + log.Error(err, "unable to get object key", "objectKey", objectKey, "secret", s.ObjectMeta.Name) + return nil, err + } + // if VarsKeys is null, use all + if vf.VarsKeys == nil { + for key, val := range s.Data { + vars[key], err = utils.JSONEncodeBytes(val) + if err != nil { + err := fmt.Errorf("failed to encode key %s with error: %w", key, err) + log.Error(err, "encoding failure") + return nil, err + } + } + } else { + for _, key := range vf.VarsKeys { + vars[key], err = utils.JSONEncodeBytes(s.Data[key]) + if err != nil { + err := fmt.Errorf("failed to encode key %s with error: %w", key, err) + log.Error(err, "encoding failure") + return nil, err + } + } + } + } else if vf.Kind == "ConfigMap" { + var cm v1.ConfigMap + err := r.Get(ctx, objectKey, &cm) + if err != nil && vf.Optional == false { + log.Error(err, "unable to get object key", "objectKey", objectKey, "configmap", cm.ObjectMeta.Name) + return nil, err + } + + // if VarsKeys is null, use all + if vf.VarsKeys == nil { + for key, val := range cm.Data { + vars[key], err = utils.JSONEncodeBytes([]byte(val)) + if err != nil { + err := fmt.Errorf("failed to encode key %s with error: %w", key, err) + log.Error(err, "encoding failure") + return nil, err + } + } + for key, val := range cm.BinaryData { + vars[key], err = utils.JSONEncodeBytes(val) + if err != nil { + err := fmt.Errorf("failed to encode key %s with error: %w", key, err) + log.Error(err, "encoding failure") + return nil, err + } + } + } else { + for _, key := range vf.VarsKeys { + if val, ok := cm.Data[key]; ok { + vars[key], err = utils.JSONEncodeBytes([]byte(val)) + if err != nil { + err := fmt.Errorf("failed to encode key %s with error: %w", key, err) + log.Error(err, "encoding failure") + return nil, err + } + } + if val, ok := cm.BinaryData[key]; ok { + vars[key], err = utils.JSONEncodeBytes(val) + if err != nil { + log.Error(err, "encoding failure") + return nil, err + } + } + } + } + } + } + + jsonBytes, err := json2.Marshal(vars) + if err != nil { + log.Error(err, "unable to marshal the data") + return nil, err + } + + varFilePath := filepath.Join(req.WorkingDir, "generated.auto.tfvars.json") + if err := os.WriteFile(varFilePath, jsonBytes, 0644); err != nil { + err = fmt.Errorf("error generating var file: %s", err) + log.Error(err, "unable to write the data to file", "filePath", varFilePath) + return nil, err + } + + return &GenerateVarsForTFReply{Message: "ok"}, nil +} diff --git a/runner/server_plan.go b/runner/server_plan.go new file mode 100644 index 00000000..b33d38ae --- /dev/null +++ b/runner/server_plan.go @@ -0,0 +1,66 @@ +package runner + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-exec/tfexec" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "sigs.k8s.io/controller-runtime" +) + +func (r *TerraformRunnerServer) Plan(ctx context.Context, req *PlanRequest) (*PlanReply, error) { + log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) + log.Info("creating a plan") + ctx, cancel := context.WithCancel(ctx) + go func() { + select { + case <-r.Done: + cancel() + case <-ctx.Done(): + } + }() + + if req.TfInstance != r.InstanceID { + err := fmt.Errorf("no TF instance found") + log.Error(err, "no terraform") + return nil, err + } + + var planOpt []tfexec.PlanOption + if req.Out != "" { + planOpt = append(planOpt, tfexec.Out(req.Out)) + } + + if req.Refresh == false { + planOpt = append(planOpt, tfexec.Refresh(req.Refresh)) + } + + if req.Destroy { + planOpt = append(planOpt, tfexec.Destroy(req.Destroy)) + } + + for _, target := range req.Targets { + planOpt = append(planOpt, tfexec.Target(target)) + } + + drifted, err := r.tf.Plan(ctx, planOpt...) + if err != nil { + st := status.New(codes.Internal, err.Error()) + var stateErr *tfexec.ErrStateLocked + + if errors.As(err, &stateErr) { + st, err = st.WithDetails(&PlanReply{Message: "not ok", StateLockIdentifier: stateErr.ID}) + + if err != nil { + return nil, err + } + } + + log.Error(err, "error creating the plan") + return nil, st.Err() + } + + return &PlanReply{Message: "ok", Drifted: drifted}, nil +} diff --git a/runner/server_save_tfplan.go b/runner/server_save_tfplan.go new file mode 100644 index 00000000..d707c5d2 --- /dev/null +++ b/runner/server_save_tfplan.go @@ -0,0 +1,199 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-logr/logr" + "github.com/weaveworks/tf-controller/api/v1alpha1" + "github.com/weaveworks/tf-controller/utils" + "io/ioutil" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "path/filepath" + "sigs.k8s.io/controller-runtime" + "strings" +) + +func (r *TerraformRunnerServer) SaveTFPlan(ctx context.Context, req *SaveTFPlanRequest) (*SaveTFPlanReply, error) { + log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) + log.Info("save the plan") + if req.TfInstance != r.InstanceID { + err := fmt.Errorf("no TF instance found") + log.Error(err, "no terraform") + return nil, err + } + + var tfplan []byte + if req.BackendCompletelyDisable { + tfplan = []byte("dummy plan") + } else { + var err error + tfplan, err = ioutil.ReadFile(filepath.Join(r.tf.WorkingDir(), TFPlanName)) + if err != nil { + err = fmt.Errorf("error running Plan: %s", err) + log.Error(err, "unable to run the plan") + return nil, err + } + } + + planRev := strings.Replace(req.Revision, "/", "-", 1) + planName := "plan-" + planRev + if err := r.writePlanAsSecret(ctx, req.Name, req.Namespace, log, planName, tfplan, "", req.Uuid); err != nil { + return nil, err + } + + if r.terraform.Spec.StoreReadablePlan == "json" { + planObj, err := r.tf.ShowPlanFile(ctx, TFPlanName) + if err != nil { + log.Error(err, "unable to get the plan output for json") + return nil, err + } + jsonBytes, err := json.Marshal(planObj) + if err != nil { + log.Error(err, "unable to marshal the plan to json") + return nil, err + } + + if err := r.writePlanAsSecret(ctx, req.Name, req.Namespace, log, planName, jsonBytes, ".json", req.Uuid); err != nil { + return nil, err + } + + } else if r.terraform.Spec.StoreReadablePlan == "human" { + rawOutput, err := r.tf.ShowPlanFileRaw(ctx, TFPlanName) + if err != nil { + log.Error(err, "unable to get the plan output for human") + return nil, err + } + + if err := r.writePlanAsConfigMap(ctx, req.Name, req.Namespace, log, planName, rawOutput, "", req.Uuid); err != nil { + return nil, err + } + } + + return &SaveTFPlanReply{Message: "ok"}, nil +} + +func (r *TerraformRunnerServer) writePlanAsSecret(ctx context.Context, name string, namespace string, log logr.Logger, planName string, tfplan []byte, suffix string, uuid string) error { + secretName := "tfplan-" + r.terraform.WorkspaceName() + "-" + name + suffix + tfplanObjectKey := types.NamespacedName{Name: secretName, Namespace: namespace} + var tfplanSecret v1.Secret + tfplanSecretExists := true + + if err := r.Client.Get(ctx, tfplanObjectKey, &tfplanSecret); err != nil { + if errors.IsNotFound(err) { + tfplanSecretExists = false + } else { + err = fmt.Errorf("error getting tfplanSecret: %s", err) + log.Error(err, "unable to get the plan secret") + return err + } + } + + if tfplanSecretExists { + if err := r.Client.Delete(ctx, &tfplanSecret); err != nil { + err = fmt.Errorf("error deleting tfplanSecret: %s", err) + log.Error(err, "unable to delete the plan secret") + return err + } + } + + tfplan, err := utils.GzipEncode(tfplan) + if err != nil { + log.Error(err, "unable to encode the plan revision", "planName", planName) + return err + } + + tfplanData := map[string][]byte{TFPlanName: tfplan} + tfplanSecret = v1.Secret{ + TypeMeta: v12.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: v12.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + "encoding": "gzip", + SavedPlanSecretAnnotation: planName, + }, + OwnerReferences: []v12.OwnerReference{ + { + APIVersion: v1alpha1.GroupVersion.Group + "/" + v1alpha1.GroupVersion.Version, + Kind: v1alpha1.TerraformKind, + Name: name, + UID: types.UID(uuid), + }, + }, + }, + Type: v1.SecretTypeOpaque, + Data: tfplanData, + } + + if err := r.Client.Create(ctx, &tfplanSecret); err != nil { + err = fmt.Errorf("error recording plan status: %s", err) + log.Error(err, "unable to create plan secret") + return err + } + + return nil +} + +func (r *TerraformRunnerServer) writePlanAsConfigMap(ctx context.Context, name string, namespace string, log logr.Logger, planName string, tfplan string, suffix string, uuid string) error { + configMapName := "tfplan-" + r.terraform.WorkspaceName() + "-" + name + suffix + tfplanObjectKey := types.NamespacedName{Name: configMapName, Namespace: namespace} + var tfplanCM v1.ConfigMap + tfplanCMExists := true + + if err := r.Client.Get(ctx, tfplanObjectKey, &tfplanCM); err != nil { + if errors.IsNotFound(err) { + tfplanCMExists = false + } else { + err = fmt.Errorf("error getting tfplanSecret: %s", err) + log.Error(err, "unable to get the plan configmap") + return err + } + } + + if tfplanCMExists { + if err := r.Client.Delete(ctx, &tfplanCM); err != nil { + err = fmt.Errorf("error deleting tfplanSecret: %s", err) + log.Error(err, "unable to delete the plan configmap") + return err + } + } + + tfplanData := map[string]string{TFPlanName: tfplan} + tfplanCM = v1.ConfigMap{ + TypeMeta: v12.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: v12.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + Annotations: map[string]string{ + SavedPlanSecretAnnotation: planName, + }, + OwnerReferences: []v12.OwnerReference{ + { + APIVersion: v1alpha1.GroupVersion.Group + "/" + v1alpha1.GroupVersion.Version, + Kind: v1alpha1.TerraformKind, + Name: name, + UID: types.UID(uuid), + }, + }, + }, + Data: tfplanData, + } + + if err := r.Client.Create(ctx, &tfplanCM); err != nil { + err = fmt.Errorf("error recording plan status: %s", err) + log.Error(err, "unable to create plan configmap") + return err + } + + return nil +} diff --git a/runner/server_show_plan.go b/runner/server_show_plan.go new file mode 100644 index 00000000..b7d64fc8 --- /dev/null +++ b/runner/server_show_plan.go @@ -0,0 +1,50 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + "sigs.k8s.io/controller-runtime" +) + +func (r *TerraformRunnerServer) ShowPlanFileRaw(ctx context.Context, req *ShowPlanFileRawRequest) (*ShowPlanFileRawReply, error) { + log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) + log.Info("show the raw plan file") + if req.TfInstance != r.InstanceID { + err := fmt.Errorf("no TF instance found") + log.Error(err, "no terraform") + return nil, err + } + + rawOutput, err := r.tf.ShowPlanFileRaw(ctx, req.Filename) + if err != nil { + log.Error(err, "unable to get the raw plan output") + return nil, err + } + + return &ShowPlanFileRawReply{RawOutput: rawOutput}, nil +} + +func (r *TerraformRunnerServer) ShowPlanFile(ctx context.Context, req *ShowPlanFileRequest) (*ShowPlanFileReply, error) { + log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName) + log.Info("show the raw plan file") + if req.TfInstance != r.InstanceID { + err := fmt.Errorf("no TF instance found") + log.Error(err, "no terraform") + return nil, err + } + + plan, err := r.tf.ShowPlanFile(ctx, req.Filename) + if err != nil { + log.Error(err, "unable to get the json plan output") + return nil, err + } + + jsonBytes, err := json.Marshal(plan) + if err != nil { + log.Error(err, "unable to marshal the plan to json") + return nil, err + } + + return &ShowPlanFileReply{JsonOutput: jsonBytes}, nil +}