From 8d6534ccfc517352356d79c9450d916bf7fd2621 Mon Sep 17 00:00:00 2001 From: John Houston Date: Wed, 6 Mar 2024 22:36:33 -0500 Subject: [PATCH] add tests for manifest_encode --- .../framework/provider/functions/encode.go | 106 +++++++-- .../functions/manifest_encode_test.go | 214 ++++++++++++++++++ 2 files changed, 300 insertions(+), 20 deletions(-) create mode 100644 internal/framework/provider/functions/manifest_encode_test.go diff --git a/internal/framework/provider/functions/encode.go b/internal/framework/provider/functions/encode.go index 26b28ee117..65bfde1314 100644 --- a/internal/framework/provider/functions/encode.go +++ b/internal/framework/provider/functions/encode.go @@ -2,6 +2,7 @@ package functions import ( "fmt" + "strings" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -27,18 +28,24 @@ func encodeValue(v attr.Value) (any, error) { return encodeObject(vv) case basetypes.TupleValue: return encodeTuple(vv) - // FIXME: we should support map, list here too + case basetypes.MapValue: + return encodeMap(vv) + case basetypes.ListValue: + return encodeList(vv) + case basetypes.SetValue: + return encodeSet(vv) default: return nil, fmt.Errorf("tried to encode unsupported type: %T: %v", v, vv) } } -func encodeTuple(t basetypes.TupleValue) ([]any, error) { - size := len(t.Elements()) +func encodeSet(sv basetypes.SetValue) ([]any, error) { + elems := sv.Elements() + size := len(elems) l := make([]any, size) for i := 0; i < size; i++ { var err error - l[i], err = encodeValue(t.Elements()[i]) + l[i], err = encodeValue(elems[i]) if err != nil { return nil, err } @@ -46,9 +53,51 @@ func encodeTuple(t basetypes.TupleValue) ([]any, error) { return l, nil } -func encodeObject(o basetypes.ObjectValue) (map[string]any, error) { - m := map[string]any{} - for k, v := range o.Attributes() { +func encodeList(lv basetypes.ListValue) ([]any, error) { + elems := lv.Elements() + size := len(elems) + l := make([]any, size) + for i := 0; i < size; i++ { + var err error + l[i], err = encodeValue(elems[i]) + if err != nil { + return nil, err + } + } + return l, nil +} + +func encodeTuple(tv basetypes.TupleValue) ([]any, error) { + elems := tv.Elements() + size := len(elems) + l := make([]any, size) + for i := 0; i < size; i++ { + var err error + l[i], err = encodeValue(elems[i]) + if err != nil { + return nil, err + } + } + return l, nil +} + +func encodeObject(ov basetypes.ObjectValue) (map[string]any, error) { + attrs := ov.Attributes() + m := make(map[string]any, len(attrs)) + for k, v := range attrs { + var err error + m[k], err = encodeValue(v) + if err != nil { + return nil, err + } + } + return m, nil +} + +func encodeMap(mv basetypes.MapValue) (map[string]any, error) { + elems := mv.Elements() + m := make(map[string]any, len(elems)) + for k, v := range elems { var err error m[k], err = encodeValue(v) if err != nil { @@ -58,27 +107,44 @@ func encodeObject(o basetypes.ObjectValue) (map[string]any, error) { return m, nil } -func encode(v attr.Value) (string, diag.Diagnostics) { +func marshal(m map[string]any) (encoded string, diags diag.Diagnostics) { + if err := validateKubernetesManifest(m); err != nil { + diags.Append(diag.NewErrorDiagnostic("Invalid Kubernetes manifest", err.Error())) + return + } + b, err := yaml.Marshal(m) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("Error marshalling yaml", err.Error())) + return + } + return string(b), nil +} + +func encode(v attr.Value) (encoded string, diags diag.Diagnostics) { val, err := encodeValue(v) if err != nil { return "", diag.Diagnostics{diag.NewErrorDiagnostic("Error decoding manifest", err.Error())} } - encoded := []byte{} - if l, ok := val.([]any); ok { + + if m, ok := val.(map[string]any); ok { + return marshal(m) + } else if l, ok := val.([]any); ok { for _, vv := range l { - e, err := yaml.Marshal(vv) - if err != nil { - return "", diag.Diagnostics{diag.NewErrorDiagnostic("Error marshalling yaml", err.Error())} + m, ok := vv.(map[string]any) + if !ok { + diags.Append(diag.NewErrorDiagnostic( + "List of manifests contained an invalid resource", fmt.Sprintf("value doesn't seem to be a manifest: %#v", vv))) + } + s, diags := marshal(m) + if diags.HasError() { + return "", diags } - encoded = append(encoded, []byte("---\n")...) - encoded = append(encoded, e...) + encoded = strings.Join([]string{encoded, s}, "---\n") } return string(encoded), nil } - encoded, err = yaml.Marshal(val) - if err != nil { - return "", diag.Diagnostics{diag.NewErrorDiagnostic("Error marshalling yaml", err.Error())} - } - return string(encoded), nil + diags.Append(diag.NewErrorDiagnostic( + "Invalid manifest", fmt.Sprintf("value doesn't seem to be a manifest: %#v", val))) + return } diff --git a/internal/framework/provider/functions/manifest_encode_test.go b/internal/framework/provider/functions/manifest_encode_test.go new file mode 100644 index 0000000000..0ead8f1bb9 --- /dev/null +++ b/internal/framework/provider/functions/manifest_encode_test.go @@ -0,0 +1,214 @@ +package functions_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestManifestEncode(t *testing.T) { + t.Parallel() + + outputName := "test" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testManifestEncodeConfig(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput(outputName, `apiVersion: v1 +data: + test: test +kind: ConfigMap +metadata: + name: test +`), + ), + }, + }, + }) +} + +func TestManifestEncodeMulti(t *testing.T) { + t.Parallel() + + outputName := "test" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testManifestEncodeMultiConfig(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput(outputName, `--- +apiVersion: v1 +data: + test: test +immutable: false +kind: ConfigMap +metadata: + name: test +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: + k8s-app: fluentd-logging + name: fluentd-elasticsearch2 + namespace: kube-system +spec: + selector: + matchLabels: + name: fluentd-elasticsearch + template: + metadata: + labels: + name: fluentd-elasticsearch + something: helloworld + spec: + containers: + - image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2 + name: fluentd-elasticsearch + resources: + limits: + cpu: 1.5 + memory: 200Mi + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - mountPath: /var/log + name: varlog + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + volumes: + - hostPath: + path: /var/log + name: varlog +`), + ), + }, + }, + }) +} + +func testManifestEncodeConfig() string { + return ` +locals { + single_manifest = { + apiVersion = "v1" + kind = "ConfigMap" + metadata = { + name = "test" + } + data = { + "test" = "test" + } + } +} + +output "test" { + value = provider::kubernetes::manifest_encode(local.single_manifest) +}` +} + +func testManifestEncodeMultiConfig() string { + return ` +locals { + multi_manifest = [ + { + apiVersion = "v1" + kind = "ConfigMap" + metadata = { + name = "test" + } + data = { + "test" = "test" + } + immutable = false + }, + { + apiVersion = "apps/v1" + kind = "DaemonSet" + metadata = { + name = "fluentd-elasticsearch2" + namespace = "kube-system" + labels = { + "k8s-app" = "fluentd-logging" + } + } + spec = { + selector = { + matchLabels = { + name = "fluentd-elasticsearch" + } + } + template = { + metadata = { + labels = { + "something" = "helloworld" + "name" = "fluentd-elasticsearch" + } + } + spec = { + tolerations = [ + { + key = "node-role.kubernetes.io/control-plane" + operator = "Exists" + effect = "NoSchedule" + }, + { + key = "node-role.kubernetes.io/master" + operator = "Exists" + effect = "NoSchedule" + } + ] + containers = [ + { + name = "fluentd-elasticsearch" + image = "quay.io/fluentd_elasticsearch/fluentd:v2.5.2" + resources = { + limits = { + cpu = 1.5 + memory = "200Mi" + } + requests = { + cpu = "100m" + memory = "200Mi" + } + } + volumeMounts = [ + { + mountPath = "/var/log" + name = "varlog" + } + ] + } + ] + terminationGracePeriodSeconds = 30 + volumes = [ + { + name = "varlog" + hostPath = { + path = "/var/log" + } + } + ] + } + } + } + } + ] +} + +output "test" { + value = provider::kubernetes::manifest_encode(local.multi_manifest) +}` +}