From 79255db40f6f628aaac3de2343257352ea5141c2 Mon Sep 17 00:00:00 2001 From: Suraj Deshmukh Date: Tue, 22 Sep 2020 15:11:05 +0530 Subject: [PATCH] prometheus-operator: Add conversion tests - This commit adds new conversion test to verify if the external_url works fine. - This commit adds test cases in TestRenderManifest to verify if the prometheus external_url and prometheus ingress host matches. Signed-off-by: Suraj Deshmukh --- pkg/components/internal/testutil/common.go | 61 +++++++++++ pkg/components/internal/testutil/doc.go | 16 +++ pkg/components/internal/testutil/jsonpath.go | 91 ++++++++++++++++ .../prometheus-operator/component_test.go | 101 ++++++++++++++++++ .../pkg/runtime/serializer/yaml/yaml.go | 46 ++++++++ vendor/modules.txt | 1 + 6 files changed, 316 insertions(+) create mode 100644 pkg/components/internal/testutil/common.go create mode 100644 pkg/components/internal/testutil/doc.go create mode 100644 pkg/components/internal/testutil/jsonpath.go create mode 100644 vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/yaml.go diff --git a/pkg/components/internal/testutil/common.go b/pkg/components/internal/testutil/common.go new file mode 100644 index 000000000..b16f27f5f --- /dev/null +++ b/pkg/components/internal/testutil/common.go @@ -0,0 +1,61 @@ +// Copyright 2020 The Lokomotive Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutil + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + + "github.com/kinvolk/lokomotive/pkg/components" + "github.com/kinvolk/lokomotive/pkg/components/util" +) + +// ConfigFromMap takes a map and a key and returns the value associated with the key. If the key +// does not exists in that map it fails. +func ConfigFromMap(t *testing.T, m map[string]string, k string) string { + ret, ok := m[k] + if !ok { + t.Fatalf("Config not found with filename: %q", k) + } + + return ret +} + +// RenderManifests converts a component into YAML manifests. +func RenderManifests( + t *testing.T, component components.Component, componentName string, hclConfig string, +) map[string]string { + body, diagnostics := util.GetComponentBody(hclConfig, componentName) + if diagnostics.HasErrors() { + t.Fatalf("Getting component body: %v", diagnostics.Errs()) + } + + diagnostics = component.LoadConfig(body, &hcl.EvalContext{}) + if diagnostics.HasErrors() { + t.Fatalf("Loading configuration: %v", diagnostics) + } + + ret, err := component.RenderManifests() + if err != nil { + t.Fatalf("Rendering manifests: %v", err) + } + + if len(ret) == 0 { + t.Fatalf("Rendered manifests shouldn't be empty") + } + + return ret +} diff --git a/pkg/components/internal/testutil/doc.go b/pkg/components/internal/testutil/doc.go new file mode 100644 index 000000000..8dfd9f8f6 --- /dev/null +++ b/pkg/components/internal/testutil/doc.go @@ -0,0 +1,16 @@ +// Copyright 2020 The Lokomotive Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package testutil contains helper functions for component unit tests. +package testutil diff --git a/pkg/components/internal/testutil/jsonpath.go b/pkg/components/internal/testutil/jsonpath.go new file mode 100644 index 000000000..f72c4eb64 --- /dev/null +++ b/pkg/components/internal/testutil/jsonpath.go @@ -0,0 +1,91 @@ +// Copyright 2020 The Lokomotive Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutil + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + yamlserializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "k8s.io/client-go/util/jsonpath" +) + +// unstructredObj accepts a Kubernetes manifest in YAML format and returns an object of type +// `unstructured.Unstructured`. This object has many methods that can be used by the consumer to +// extract metadata from the Kubernetes manifest. +func unstructredObj(t *testing.T, yamlObj string) *unstructured.Unstructured { + u := &unstructured.Unstructured{} + + // Decode YAML into `unstructured.Unstructured`. + dec := yamlserializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + if _, _, err := dec.Decode([]byte(yamlObj), nil, u); err != nil { + t.Fatalf("Converting config to unstructured.Unstructured: %v", err) + } + + return u +} + +// valFromObject takes a JSON path as a string and an object of type `unstructured.Unstructured`. +// This function returns an object of type `reflect.Value` at that JSON path. +func valFromObject(t *testing.T, jp string, obj *unstructured.Unstructured) reflect.Value { + jPath := jsonpath.New("parse") + if err := jPath.Parse(jp); err != nil { + t.Fatalf("Parsing JSONPath: %v", err) + } + + v, err := jPath.FindResults(obj.Object) + if err != nil { + t.Fatalf("Finding results using JSONPath in the YAML file: %v", err) + } + + if len(v) == 0 || len(v[0]) == 0 { + t.Fatalf("No result found") + } + + return v[0][0] +} + +// jsonPathValue extracts an object at a JSON path from a YAML config, and returns an interface +// object. +func jsonPathValue(t *testing.T, yamlConfig string, jsonPath string) interface{} { + u := unstructredObj(t, yamlConfig) + got := valFromObject(t, jsonPath, u) + + switch got.Kind() { //nolint:exhaustive + case reflect.Interface: + // TODO: Add type switch here for concrete types. + return got.Interface() + default: + t.Fatalf("Extracted object has an unknown type: %v", got.Kind()) + } + + return nil +} + +// MatchJSONPathStringValue is a helper function for component unit tests. It compares the string at +// a JSON path in a YAML config to the expected string. +func MatchJSONPathStringValue(t *testing.T, yamlConfig string, jsonPath string, expected string) { + obj := jsonPathValue(t, yamlConfig, jsonPath) + + got, ok := obj.(string) + if !ok { + t.Fatalf("Value is not string: %#v", obj) + } + + if got != expected { + t.Fatalf("Expected: %s, Got: %s", expected, got) + } +} diff --git a/pkg/components/prometheus-operator/component_test.go b/pkg/components/prometheus-operator/component_test.go index 8de992256..03c17782c 100644 --- a/pkg/components/prometheus-operator/component_test.go +++ b/pkg/components/prometheus-operator/component_test.go @@ -17,6 +17,7 @@ package prometheus import ( "testing" + "github.com/kinvolk/lokomotive/pkg/components/internal/testutil" "github.com/kinvolk/lokomotive/pkg/components/util" ) @@ -62,6 +63,34 @@ component "prometheus-operator" { }`, wantErr: true, }, + { + desc: "prometheus ingress and external_url given and are different", + hcl: ` +component "prometheus-operator" { + prometheus { + external_url = "https://prometheus.notmydomain.net" + ingress { + host = "prometheus.mydomain.net" + } + } +} +`, + wantErr: true, + }, + { + desc: "prometheus ingress and external_url given and are same", + hcl: ` +component "prometheus-operator" { + prometheus { + external_url = "https://prometheus.mydomain.net" + ingress { + host = "prometheus.mydomain.net" + } + } +} +`, + wantErr: false, + }, } for _, tc := range tests { @@ -98,3 +127,75 @@ component "prometheus-operator" { }) } } + +//nolint:funlen +func TestConversion(t *testing.T) { + testCases := []struct { + name string + inputConfig string + expectedManifestName string + expected string + jsonPath string + }{ + { + name: "use external_url param", + inputConfig: ` +component "prometheus-operator" { + prometheus { + external_url = "https://prometheus.externalurl.net" + } +} +`, + expectedManifestName: "prometheus-operator/templates/prometheus/prometheus.yaml", + expected: "https://prometheus.externalurl.net", + jsonPath: "{.spec.externalUrl}", + }, + { + name: "no external_url param", + inputConfig: ` + component "prometheus-operator" { + prometheus { + ingress { + host = "prometheus.mydomain.net" + class = "contour" + certmanager_cluster_issuer = "letsencrypt-production" + } + } + } + `, + expectedManifestName: "prometheus-operator/templates/prometheus/prometheus.yaml", + expected: "https://prometheus.mydomain.net", + jsonPath: "{.spec.externalUrl}", + }, + { + name: "ingress creation for prometheus", + inputConfig: ` + component "prometheus-operator" { + prometheus { + ingress { + host = "prometheus.mydomain.net" + class = "contour" + certmanager_cluster_issuer = "letsencrypt-production" + } + } + } + `, + expectedManifestName: "prometheus-operator/templates/prometheus/ingress.yaml", + expected: "prometheus.mydomain.net", + jsonPath: "{.spec.rules[0].host}", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + component := newComponent() + m := testutil.RenderManifests(t, component, name, tc.inputConfig) + gotConfig := testutil.ConfigFromMap(t, m, tc.expectedManifestName) + + testutil.MatchJSONPathStringValue(t, gotConfig, tc.jsonPath, tc.expected) + }) + } +} diff --git a/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/yaml.go b/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/yaml.go new file mode 100644 index 000000000..2fdd1d43d --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/yaml.go @@ -0,0 +1,46 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yaml + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/yaml" +) + +// yamlSerializer converts YAML passed to the Decoder methods to JSON. +type yamlSerializer struct { + // the nested serializer + runtime.Serializer +} + +// yamlSerializer implements Serializer +var _ runtime.Serializer = yamlSerializer{} + +// NewDecodingSerializer adds YAML decoding support to a serializer that supports JSON. +func NewDecodingSerializer(jsonSerializer runtime.Serializer) runtime.Serializer { + return &yamlSerializer{jsonSerializer} +} + +func (c yamlSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + out, err := yaml.ToJSON(data) + if err != nil { + return nil, nil, err + } + data = out + return c.Serializer.Decode(data, gvk, into) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 67b66a5ae..c218ad503 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -575,6 +575,7 @@ k8s.io/apimachinery/pkg/runtime/serializer/protobuf k8s.io/apimachinery/pkg/runtime/serializer/recognizer k8s.io/apimachinery/pkg/runtime/serializer/streaming k8s.io/apimachinery/pkg/runtime/serializer/versioning +k8s.io/apimachinery/pkg/runtime/serializer/yaml k8s.io/apimachinery/pkg/selection k8s.io/apimachinery/pkg/types k8s.io/apimachinery/pkg/util/cache