From c9d296aadebd8a3dfe8f85c32afc6a7703acd6f5 Mon Sep 17 00:00:00 2001 From: marcin witalis <45931826+m00g3n@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:51:36 +0200 Subject: [PATCH] Add raw extension comparator (#413) --- hack/shoot-comparator/pkg/errors/errors.go | 9 ++ .../pkg/runtime/raw_extension_matcher.go | 63 ++++++++++++++ .../pkg/runtime/raw_extension_matcher_test.go | 66 +++++++++++++++ .../runtime/testdata/infra_cfg_azure_11.yaml | 21 +++++ .../runtime/testdata/infra_cfg_azure_12.yaml | 21 +++++ .../runtime/testdata/infra_cfg_azure_21.yaml | 11 +++ .../testdata/infra_cfg_openstack_11.yaml | 6 ++ .../pkg/shoot/extensionmatcher.go | 3 +- hack/shoot-comparator/pkg/shoot/matcher.go | 70 +++++++--------- .../pkg/shoot/provider_matcher.go | 82 +++++++++++++++++++ hack/shoot-comparator/pkg/utilz/utilz.go | 34 ++++++++ 11 files changed, 345 insertions(+), 41 deletions(-) create mode 100644 hack/shoot-comparator/pkg/errors/errors.go create mode 100644 hack/shoot-comparator/pkg/runtime/raw_extension_matcher.go create mode 100644 hack/shoot-comparator/pkg/runtime/raw_extension_matcher_test.go create mode 100644 hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_11.yaml create mode 100644 hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_12.yaml create mode 100644 hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_21.yaml create mode 100644 hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_openstack_11.yaml create mode 100644 hack/shoot-comparator/pkg/shoot/provider_matcher.go create mode 100644 hack/shoot-comparator/pkg/utilz/utilz.go diff --git a/hack/shoot-comparator/pkg/errors/errors.go b/hack/shoot-comparator/pkg/errors/errors.go new file mode 100644 index 00000000..04284fac --- /dev/null +++ b/hack/shoot-comparator/pkg/errors/errors.go @@ -0,0 +1,9 @@ +package errors + +import "fmt" + +var ( + ErrInvalidType = fmt.Errorf("invalid type") + ErrInvalidValue = fmt.Errorf("invalid value") + ErrNilValue = fmt.Errorf("%w: nil", ErrInvalidValue) +) diff --git a/hack/shoot-comparator/pkg/runtime/raw_extension_matcher.go b/hack/shoot-comparator/pkg/runtime/raw_extension_matcher.go new file mode 100644 index 00000000..77734c78 --- /dev/null +++ b/hack/shoot-comparator/pkg/runtime/raw_extension_matcher.go @@ -0,0 +1,63 @@ +package runtime + +import ( + "sort" + + "github.com/kyma-project/infrastructure-manager/hack/shoot-comparator/pkg/utilz" + "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + "k8s.io/apimachinery/pkg/runtime" +) + +type RawExtensionMatcher struct { + toMatch interface{} +} + +func NewRawExtensionMatcher(v any) types.GomegaMatcher { + return &RawExtensionMatcher{ + toMatch: v, + } +} + +func (m *RawExtensionMatcher) Match(actual interface{}) (bool, error) { + if actual == nil && m.toMatch == nil { + return true, nil + } + + aRawExtension, err := utilz.Get[runtime.RawExtension](actual) + if err != nil { + return false, err + } + + eRawExtension, err := utilz.Get[runtime.RawExtension](m.toMatch) + if err != nil { + return false, err + } + + sort.Sort(sortBytes(aRawExtension.Raw)) + sort.Sort(sortBytes(eRawExtension.Raw)) + + return gomega.BeComparableTo(aRawExtension.Raw).Match(eRawExtension.Raw) +} + +func (m *RawExtensionMatcher) NegatedFailureMessage(_ interface{}) string { + return "expected should not equal actual" +} + +func (m *RawExtensionMatcher) FailureMessage(_ interface{}) string { + return "expected should equal actual" +} + +type sortBytes []byte + +func (s sortBytes) Less(i, j int) bool { + return s[i] < s[j] +} + +func (s sortBytes) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s sortBytes) Len() int { + return len(s) +} diff --git a/hack/shoot-comparator/pkg/runtime/raw_extension_matcher_test.go b/hack/shoot-comparator/pkg/runtime/raw_extension_matcher_test.go new file mode 100644 index 00000000..49262dbb --- /dev/null +++ b/hack/shoot-comparator/pkg/runtime/raw_extension_matcher_test.go @@ -0,0 +1,66 @@ +package runtime + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" +) + +func loadTestdata(t *testing.T, filePath string) []byte { + data, err := os.ReadFile(filePath) + require.NoError(t, err) + require.Greater(t, len(data), 0, "test data is empty") + return data +} + +func TestMatcher(t *testing.T) { + testCases := []struct { + actual interface{} + expected interface{} + isOK bool + hasErr bool + }{ + { + actual: string(loadTestdata(t, "testdata/infra_cfg_azure_11.yaml")), + expected: string(loadTestdata(t, "testdata/infra_cfg_azure_12.yaml")), + isOK: true, + hasErr: false, + }, + { + actual: runtime.RawExtension{}, + expected: runtime.RawExtension{}, + isOK: true, + hasErr: false, + }, + { + actual: runtime.RawExtension{}, + expected: nil, + isOK: false, + hasErr: true, + }, + { + actual: string(loadTestdata(t, "testdata/infra_cfg_azure_11.yaml")), + expected: string(loadTestdata(t, "testdata/infra_cfg_azure_21.yaml")), + isOK: false, + hasErr: false, + }, + { + actual: []byte("invalid type"), + expected: []byte("invalid type"), + isOK: false, + hasErr: true, + }, + } + + for _, tc := range testCases { + matcher := NewRawExtensionMatcher(tc.actual) + ok, err := matcher.Match(tc.expected) + // THEN + assert.Equal(t, tc.hasErr, err != nil, err) + assert.Equal(t, tc.isOK, ok, matcher.FailureMessage(nil)) + } + +} diff --git a/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_11.yaml b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_11.yaml new file mode 100644 index 00000000..511694cc --- /dev/null +++ b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_11.yaml @@ -0,0 +1,21 @@ +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: 3 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 + - cidr: 10.250.1.0/25 + name: 1 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 diff --git a/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_12.yaml b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_12.yaml new file mode 100644 index 00000000..b7be94d9 --- /dev/null +++ b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_12.yaml @@ -0,0 +1,21 @@ +apiVersion: azure.provider.extensions.gardener.cloud/v1alpha1 +kind: InfrastructureConfig +networks: + vnet: + cidr: 10.250.0.0/22 + zones: + - cidr: 10.250.0.128/25 + name: 3 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 + - cidr: 10.250.1.0/25 + name: 1 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 + - cidr: 10.250.0.0/25 + name: 2 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 diff --git a/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_21.yaml b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_21.yaml new file mode 100644 index 00000000..d67b8ee9 --- /dev/null +++ b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_azure_21.yaml @@ -0,0 +1,11 @@ +apiVersion: azure.provider.extensions.gardener.cloud/v1alpha1 +kind: InfrastructureConfig +networks: + vnet: + cidr: 10.250.0.0/22 + zones: + - cidr: 10.250.1.0/25 + name: 1 + natGateway: + enabled: true + idleConnectionTimeoutMinutes: 4 diff --git a/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_openstack_11.yaml b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_openstack_11.yaml new file mode 100644 index 00000000..70e8bd27 --- /dev/null +++ b/hack/shoot-comparator/pkg/runtime/testdata/infra_cfg_openstack_11.yaml @@ -0,0 +1,6 @@ +apiVersion: openstack.provider.extensions.gardener.cloud/v1alpha1 +floatingPoolName: FloatingIP-external-kyma-01 +kind: InfrastructureConfig +networks: + worker: '' + workers: 10.250.0.0/22 diff --git a/hack/shoot-comparator/pkg/shoot/extensionmatcher.go b/hack/shoot-comparator/pkg/shoot/extensionmatcher.go index c71f995d..36cc5c98 100644 --- a/hack/shoot-comparator/pkg/shoot/extensionmatcher.go +++ b/hack/shoot-comparator/pkg/shoot/extensionmatcher.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/kyma-project/infrastructure-manager/hack/shoot-comparator/pkg/errors" "github.com/onsi/gomega" "github.com/onsi/gomega/types" ) @@ -45,7 +46,7 @@ func getExtension(i interface{}) ([]v1beta1.Extension, error) { return v, nil default: - return []v1beta1.Extension{}, fmt.Errorf(`%w: %s`, errInvalidType, reflect.TypeOf(v)) + return []v1beta1.Extension{}, fmt.Errorf(`%w: %s`, errors.ErrInvalidType, reflect.TypeOf(v)) } } diff --git a/hack/shoot-comparator/pkg/shoot/matcher.go b/hack/shoot-comparator/pkg/shoot/matcher.go index d4d3bd71..bfaebff4 100644 --- a/hack/shoot-comparator/pkg/shoot/matcher.go +++ b/hack/shoot-comparator/pkg/shoot/matcher.go @@ -6,15 +6,12 @@ import ( "strings" "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/kyma-project/infrastructure-manager/hack/shoot-comparator/pkg/errors" "github.com/onsi/gomega" "github.com/onsi/gomega/types" "sigs.k8s.io/yaml" ) -var ( - errInvalidType = fmt.Errorf("invalid type") -) - type Matcher struct { toMatch interface{} fails []string @@ -43,16 +40,10 @@ func getShoot(i interface{}) (shoot v1beta1.Shoot, err error) { return *v, nil default: - return v1beta1.Shoot{}, fmt.Errorf(`%w: %s`, errInvalidType, reflect.TypeOf(v)) + return v1beta1.Shoot{}, fmt.Errorf(`%w: %s`, errors.ErrInvalidType, reflect.TypeOf(v)) } } -type matcher struct { - types.GomegaMatcher - path string - expected interface{} -} - func (m *Matcher) Match(actual interface{}) (success bool, err error) { aShoot, err := getShoot(actual) if err != nil { @@ -66,7 +57,7 @@ func (m *Matcher) Match(actual interface{}) (success bool, err error) { // Note: we define separate matchers for each field to make input more readable // Annotations are not matched as they are not relevant for the comparison ; both KIM, and Provisioner have different set of annotations - for _, matcher := range []matcher{ + for _, matcher := range []propertyMatcher{ // We need to skip comparing type meta as Provisioner doesn't set it. // It is simpler to skip it than to make fix in the Provisioner. //{ @@ -91,118 +82,117 @@ func (m *Matcher) Match(actual interface{}) (success bool, err error) { { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Addons), expected: aShoot.Spec.Addons, - path: "spec/Addons", + path: "spec/addons", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.CloudProfileName), expected: aShoot.Spec.CloudProfileName, - path: "spec/CloudProfileName", + path: "spec/cloudProfileName", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.DNS), expected: aShoot.Spec.DNS, - path: "spec/DNS", + path: "spec/dns", }, { GomegaMatcher: NewExtensionMatcher(eShoot.Spec.Extensions), expected: aShoot.Spec.Extensions, - path: "spec/Extensions", + path: "spec/extensions", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Hibernation), expected: aShoot.Spec.Hibernation, - path: "spec/Hibernation", + path: "spec/hibernation", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Kubernetes), expected: aShoot.Spec.Kubernetes, - path: "spec/Kubernetes", + path: "spec/kubernetes", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Networking), expected: aShoot.Spec.Networking, - path: "spec/Networking", + path: "spec/networking", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Maintenance), expected: aShoot.Spec.Maintenance, - path: "spec/Maintenance", + path: "spec/maintenance", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Monitoring), expected: aShoot.Spec.Monitoring, - path: "spec/Monitoring", + path: "spec/monitoring", }, - { - GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Provider), - expected: aShoot.Spec.Provider, - path: "spec/Provider", - }, - { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Purpose), expected: aShoot.Spec.Purpose, - path: "spec/Purpose", + path: "spec/purpose", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Region), expected: aShoot.Spec.Region, - path: "spec/Region", + path: "spec/region", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.SecretBindingName), expected: aShoot.Spec.SecretBindingName, - path: "spec/SecretBindingName", + path: "spec/secretBindingName", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.SeedName), expected: aShoot.Spec.SeedName, - path: "spec/SeedName", + path: "spec/seedName", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.SeedSelector), expected: aShoot.Spec.SeedSelector, - path: "spec/SeedSelector", + path: "spec/seedSelector", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Resources), expected: aShoot.Spec.Resources, - path: "spec/Resources", + path: "spec/resources", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.Tolerations), expected: aShoot.Spec.Tolerations, - path: "spec/Tolerations", + path: "spec/tolerations", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.ExposureClassName), expected: aShoot.Spec.ExposureClassName, - path: "spec/ExposureClassName", + path: "spec/exposureClassName", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.SystemComponents), expected: aShoot.Spec.SystemComponents, - path: "spec/SystemComponents", + path: "spec/systemComponents", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.ControlPlane), expected: aShoot.Spec.ControlPlane, - path: "spec/ControlPlane", + path: "spec/controlPlane", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.SchedulerName), expected: aShoot.Spec.SchedulerName, - path: "spec/SchedulerName", + path: "spec/schedulerName", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.CloudProfile), expected: aShoot.Spec.CloudProfile, - path: "spec/CloudProfile", + path: "spec/cloudProfile", }, { GomegaMatcher: gomega.BeComparableTo(eShoot.Spec.CredentialsBindingName), expected: aShoot.Spec.CredentialsBindingName, - path: "spec/CredentialsBindingName", + path: "spec/credentialsBindingName", + }, + { + GomegaMatcher: NewProviderMatcher(eShoot.Spec.Provider, "spec/provider"), + expected: aShoot.Spec.Provider, + path: "spec/provider", }, } { ok, err := matcher.Match(matcher.expected) diff --git a/hack/shoot-comparator/pkg/shoot/provider_matcher.go b/hack/shoot-comparator/pkg/shoot/provider_matcher.go new file mode 100644 index 00000000..cfd0c1d8 --- /dev/null +++ b/hack/shoot-comparator/pkg/shoot/provider_matcher.go @@ -0,0 +1,82 @@ +package shoot + +import ( + "fmt" + "strings" + + "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/kyma-project/infrastructure-manager/hack/shoot-comparator/pkg/runtime" + "github.com/kyma-project/infrastructure-manager/hack/shoot-comparator/pkg/utilz" + "github.com/onsi/gomega/types" +) + +func NewProviderMatcher(v any, path string) types.GomegaMatcher { + return &ProviderMatcher{ + toMatch: v, + rootPath: path, + } +} + +type ProviderMatcher struct { + toMatch interface{} + fails []string + rootPath string +} + +func (m *ProviderMatcher) getPath(p string) string { + return fmt.Sprintf("%s/%s", m.rootPath, p) +} + +func (m *ProviderMatcher) Match(actual interface{}) (success bool, err error) { + aProvider, err := utilz.Get[v1beta1.Provider](actual) + if err != nil { + return false, err + } + + eProvider, err := utilz.Get[v1beta1.Provider](m.toMatch) + if err != nil { + return false, err + } + + for _, matcher := range []propertyMatcher{ + { + path: m.getPath("controlPlaneConfig"), + GomegaMatcher: runtime.NewRawExtensionMatcher(eProvider.ControlPlaneConfig), + expected: aProvider.ControlPlaneConfig, + }, + { + path: m.getPath("infrastructureConfig"), + GomegaMatcher: runtime.NewRawExtensionMatcher(eProvider.InfrastructureConfig), + expected: aProvider.InfrastructureConfig, + }, + } { + ok, err := matcher.Match(matcher.expected) + if err != nil { + return false, err + } + + if !ok { + msg := matcher.FailureMessage(matcher.expected) + 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 *ProviderMatcher) NegatedFailureMessage(_ interface{}) string { + return "expected should not equal actual" +} + +func (m *ProviderMatcher) FailureMessage(_ interface{}) string { + return strings.Join(m.fails, "\n") +} + +type propertyMatcher = struct { + types.GomegaMatcher + path string + expected interface{} +} diff --git a/hack/shoot-comparator/pkg/utilz/utilz.go b/hack/shoot-comparator/pkg/utilz/utilz.go new file mode 100644 index 00000000..a85e0066 --- /dev/null +++ b/hack/shoot-comparator/pkg/utilz/utilz.go @@ -0,0 +1,34 @@ +package utilz + +import ( + "fmt" + "reflect" + + "github.com/kyma-project/infrastructure-manager/hack/shoot-comparator/pkg/errors" + "sigs.k8s.io/yaml" +) + +func Get[T any](v interface{}) (T, error) { + var result T + if v == nil { + return result, errors.ErrNilValue + } + + switch typedV := v.(type) { + case string: + err := yaml.Unmarshal([]byte(typedV), &result) + return result, err + + case T: + return result, nil + + case *T: + if typedV == nil { + return result, nil + } + return *typedV, nil + + default: + return result, fmt.Errorf(`%w: %s`, errors.ErrInvalidType, reflect.TypeOf(typedV)) + } +}