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
+}