From 91d7c5f695f0ebd6402381e46bafee63c5516854 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Fri, 17 May 2024 08:44:33 +0200 Subject: [PATCH] add shoot matcher --- internal/testing/shoot/matcher.go | 132 +++++++++++++ internal/testing/shoot/matcher_test.go | 177 ++++++++++++++++++ internal/testing/shoot/suite_test.go | 13 ++ .../testing/shoot/testdata/dev_azure.yaml | 118 ++++++++++++ 4 files changed, 440 insertions(+) create mode 100644 internal/testing/shoot/matcher.go create mode 100644 internal/testing/shoot/matcher_test.go create mode 100644 internal/testing/shoot/suite_test.go create mode 100644 internal/testing/shoot/testdata/dev_azure.yaml diff --git a/internal/testing/shoot/matcher.go b/internal/testing/shoot/matcher.go new file mode 100644 index 00000000..1d53f0ad --- /dev/null +++ b/internal/testing/shoot/matcher.go @@ -0,0 +1,132 @@ +package shoot + +import ( + "fmt" + "reflect" + "strings" + + "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + "sigs.k8s.io/yaml" +) + +var ( + errInvalidType = fmt.Errorf("invalid type") +) + +type ShootMatcher struct { + toMatch interface{} + fails []string +} + +func Matcher(i interface{}) types.GomegaMatcher { + return &ShootMatcher{ + toMatch: i, + } +} + +func getShoot(i interface{}) (shoot v1beta1.Shoot, err error) { + if i == nil { + return v1beta1.Shoot{}, fmt.Errorf("invalid value nil") + } + + switch v := i.(type) { + + case string: + err = yaml.Unmarshal([]byte(v), &shoot) + return shoot, err + + case v1beta1.Shoot: + return v, nil + + case *v1beta1.Shoot: + return *v, nil + + default: + return v1beta1.Shoot{}, fmt.Errorf(`%w: %s`, errInvalidType, reflect.TypeOf(v)) + } +} + +type matcher struct { + types.GomegaMatcher + path string + actual interface{} +} + +func (m *ShootMatcher) Match(actual interface{}) (success bool, err error) { + aShoot, err := getShoot(actual) + if err != nil { + return false, err + } + + eShoot, err := getShoot(m.toMatch) + if err != nil { + return false, err + } + + for _, matcher := range []matcher{ + { + GomegaMatcher: gomega.Equal(eShoot.TypeMeta), + actual: aShoot.TypeMeta, + }, + { + GomegaMatcher: gomega.Equal(eShoot.Name), + actual: aShoot.Name, + path: "metadata/name", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Namespace), + actual: aShoot.Namespace, + path: "metadata/namespace", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Labels), + actual: aShoot.Labels, + path: "metadata/labels", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Annotations), + actual: aShoot.Annotations, + path: "metadata/annotations", + }, + { + GomegaMatcher: gomega.Equal(eShoot.OwnerReferences), + actual: aShoot.OwnerReferences, + path: "metadata/ownerReferences", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Finalizers), + actual: aShoot.Finalizers, + path: "metadata/finalizers", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Spec), + actual: aShoot.Spec, + path: "spec", + }, + } { + ok, err := matcher.Match(matcher.actual) + if err != nil { + return false, err + } + + if !ok { + msg := matcher.FailureMessage(matcher.actual) + if matcher.path != "" { + msg = fmt.Sprintf("%s: %s", matcher.path, msg) + } + m.fails = append(m.fails, msg) + } + } + + return len(m.fails) == 0, nil +} + +func (m *ShootMatcher) NegatedFailureMessage(actual interface{}) string { + return "expected should not equal actual" +} + +func (m *ShootMatcher) FailureMessage(actualuu interface{}) string { + return strings.Join(m.fails, "\n") +} diff --git a/internal/testing/shoot/matcher_test.go b/internal/testing/shoot/matcher_test.go new file mode 100644 index 00000000..de460c2c --- /dev/null +++ b/internal/testing/shoot/matcher_test.go @@ -0,0 +1,177 @@ +package shoot_test + +import ( + "os" + + "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/kyma-project/infrastructure-manager/internal/testing/shoot" + "gopkg.in/yaml.v2" + corev1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/onsi/ginkgo/v2" //nolint:revive + . "github.com/onsi/gomega" //nolint:reviv +) + +type deepCpOpts = func(*v1beta1.Shoot) + +func withName(name string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Name = name + } +} + +func withNamespace(namespace string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Namespace = namespace + } +} + +func withLabels(labels map[string]string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Labels = labels + } +} + +func withFinalizers(finalizers []string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Finalizers = finalizers + } +} + +func withOwnerReferences(ownerReferences []corev1.OwnerReference) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.OwnerReferences = ownerReferences + } +} + +func withAnnotations(annotations map[string]string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Annotations = annotations + } +} + +func withShootSpec(spec v1beta1.ShootSpec) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Spec = spec + } +} + +func deepCp(s v1beta1.Shoot, opts ...deepCpOpts) v1beta1.Shoot { + cp := s.DeepCopy() + + for _, opt := range opts { + opt(cp) + } + + return *cp +} + +func loadAsString(path string, out *string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + (*out) = string(data) + return nil +} + +func testInvalidArgs(actual, expected interface{}) { + matcher := shoot.Matcher(expected) + _, err := matcher.Match(actual) + Expect(err).To(HaveOccurred()) +} + +func testResults(actual, expected interface{}, expectedMatch bool) { + matcher := shoot.Matcher(expected) + actualMatch, err := matcher.Match(actual) + Expect(err).ShouldNot(HaveOccurred()) + Expect(actualMatch).Should(Equal(expectedMatch), matcher.FailureMessage(actual)) +} + +var _ = Describe(":: shoot matcher :: ", func() { + var empty v1beta1.Shoot + + var azure string + Expect(loadAsString("testdata/dev_azure.yaml", &azure)). + ShouldNot(HaveOccurred()) + + var shoot3 v1beta1.Shoot + Expect(yaml.Unmarshal([]byte(azure), &shoot3)).NotTo(HaveOccurred()) + + DescribeTable( + "checking invalid args :: ", + testInvalidArgs, + Entry("when actual is nil", nil, empty), + Entry("when expected is nil", "", nil), + ) + + DescribeTable( + "checking results :: ", + testResults, + Entry( + "should match empty and zero values", + "", + empty, + true, + ), + Entry( + "should match copies of the same instance", + deepCp(empty), + deepCp(empty), + true, + ), + Entry( + "should detect name difference", + deepCp(empty, withName("test1")), + deepCp(empty, withName("test2")), + false, + ), + Entry( + "should detect namespace difference", + deepCp(empty, withNamespace("test1")), + deepCp(empty, withNamespace("test2")), + false, + ), + Entry( + "should detect difference in labels", + deepCp(empty, withLabels(map[string]string{"test": "me"})), + deepCp(empty, withLabels(map[string]string{})), + false, + ), + Entry( + "should detect difference in annotations", + deepCp(empty, withAnnotations(map[string]string{"test": "me"})), + deepCp(empty, withAnnotations(map[string]string{"test": "it"})), + false, + ), + Entry( + "should detect differences in finalizers", + deepCp(empty, withFinalizers([]string{"test", "me"})), + deepCp(empty, withFinalizers([]string{"test", "me 2"})), + false, + ), + Entry( + "should detect differences in owner references", + deepCp(empty, withOwnerReferences([]corev1.OwnerReference{ + {Name: "test1", UID: "1"}, + {Name: "test2", UID: "2"}, + })), + deepCp(empty, withOwnerReferences([]corev1.OwnerReference{ + {Name: "test1", UID: "1"}, + {Name: "test3", UID: "3"}, + })), + false, + ), + Entry( + "should detect differences in spec", + deepCp(empty, withShootSpec(v1beta1.ShootSpec{ + Region: "test1", + })), + deepCp(empty, withShootSpec(v1beta1.ShootSpec{ + Region: "test2", + })), + false, + ), + ) +}) diff --git a/internal/testing/shoot/suite_test.go b/internal/testing/shoot/suite_test.go new file mode 100644 index 00000000..d1c79df5 --- /dev/null +++ b/internal/testing/shoot/suite_test.go @@ -0,0 +1,13 @@ +package shoot_test + +import ( + "testing" + + "github.com/onsi/ginkgo/v2" //nolint:revive + "github.com/onsi/gomega" //nolint:revive +) + +func TestMatcher(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "shoot matcher") +} diff --git a/internal/testing/shoot/testdata/dev_azure.yaml b/internal/testing/shoot/testdata/dev_azure.yaml new file mode 100644 index 00000000..0d7616f1 --- /dev/null +++ b/internal/testing/shoot/testdata/dev_azure.yaml @@ -0,0 +1,118 @@ +metadata: + annotations: + compass.provisioner.kyma-project.io/operation-id: bd4c0281-f43d-47d4-9887-6fe96ed0c2e4 + compass.provisioner.kyma-project.io/runtime-id: 43f0b98c-9851-484b-be1b-cda9e953de16 + kcp.provisioner.kyma-project.io/operation-id: bd4c0281-f43d-47d4-9887-6fe96ed0c2e4 + kcp.provisioner.kyma-project.io/runtime-id: 43f0b98c-9851-484b-be1b-cda9e953de16 + creationTimestamp: null + labels: + account: 461f6292-8085-41c8-af0c-e185f39b5e18 + subaccount: pg-test-su3 + name: c-361112f + namespace: garden-kyma-dev +spec: + cloudProfileName: az + dns: + domain: c-361112f.dev.kyma.ondemand.com + providers: + - domains: + include: + - c-361112f.dev.kyma.ondemand.com + primary: true + secretName: aws-route53-secret-dev + type: aws-route53 + extensions: + - providerConfig: + apiVersion: service.dns.extensions.gardener.cloud/v1alpha1 + dnsProviderReplication: + enabled: true + kind: DNSConfig + type: shoot-dns-service + - providerConfig: + apiVersion: service.cert.extensions.gardener.cloud/v1alpha1 + kind: CertConfig + shootIssuers: + enabled: true + type: shoot-cert-service + - disabled: false + type: shoot-networking-filter + kubernetes: + enableStaticTokenKubeconfig: false + kubeAPIServer: + auditConfig: + auditPolicy: + configMapRef: + name: audit-policy + oidcConfig: + clientID: 9bd05ed7-a930-44e6-8c79-e6defeb7dec9 + groupsClaim: groups + issuerURL: https://kymatest.accounts400.ondemand.com + signingAlgs: + - RS256 + usernameClaim: sub + usernamePrefix: '-' + version: "1.27" + maintenance: + autoUpdate: + kubernetesVersion: true + machineImageVersion: false + networking: + nodes: 10.250.0.0/22 + type: calico + provider: + controlPlaneConfig: + apiVersion: azure.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + infrastructureConfig: + apiVersion: azure.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + vnet: + cidr: 10.250.0.0/22 + zones: + - cidr: 10.250.0.0/25 + name: 2 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 + - cidr: 10.250.0.128/25 + name: 1 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 + - cidr: 10.250.1.0/25 + name: 3 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 + zoned: true + type: azure + workers: + - machine: + image: + name: gardenlinux + version: 1312.3.0 + type: Standard_D2s_v5 + maxSurge: 3 + maxUnavailable: 0 + maximum: 20 + minimum: 3 + name: cpu-worker-0 + volume: + size: 50Gi + type: Standard_LRS + zones: + - "2" + - "1" + - "3" + purpose: development + region: westeurope + secretBindingName: sap-skr-dev-cust-00002-kyma-integration +status: + gardener: + id: "" + name: "" + version: "" + hibernated: false + technicalID: "" + uid: ""