package managedfields_test import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" arv1 "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/yaml" "github.com/hanzoai/cd/gitops-engine/pkg/utils/kube/scheme" "github.com/hanzoai/cd/util/cd/managedfields" "github.com/hanzoai/cd/util/cd/testdata" ) func TestNormalize(t *testing.T) { t.Parallel() parser := scheme.StaticParser() t.Run("will remove conflicting fields if managed by trusted managers", func(t *testing.T) { // given t.Parallel() desiredState := StrToUnstructured(testdata.DesiredDeploymentYaml) liveState := StrToUnstructured(testdata.LiveDeploymentWithManagedReplicaYaml) trustedManagers := []string{"kube-controller-manager", "revision-history-manager"} pt := parser.Type("io.k8s.api.apps.v1.Deployment") // when liveResult, desiredResult, err := managedfields.Normalize(liveState, desiredState, trustedManagers, &pt) // then require.NoError(t, err) require.NotNil(t, liveResult) require.NotNil(t, desiredResult) desiredReplicas, ok, err := unstructured.NestedFloat64(desiredResult.Object, "spec", "replicas") assert.False(t, ok) require.NoError(t, err) liveReplicas, ok, err := unstructured.NestedFloat64(liveResult.Object, "spec", "replicas") assert.False(t, ok) require.NoError(t, err) assert.Zero(t, desiredReplicas) assert.Zero(t, liveReplicas) liveRevisionHistory, ok, err := unstructured.NestedFloat64(liveResult.Object, "spec", "revisionHistoryLimit") assert.False(t, ok) require.NoError(t, err) desiredRevisionHistory, ok, err := unstructured.NestedFloat64(desiredResult.Object, "spec", "revisionHistoryLimit") assert.False(t, ok) require.NoError(t, err) assert.Zero(t, desiredRevisionHistory) assert.Zero(t, liveRevisionHistory) }) t.Run("will keep conflicting fields if not from trusted manager", func(t *testing.T) { // given t.Parallel() desiredState := StrToUnstructured(testdata.DesiredDeploymentYaml) liveState := StrToUnstructured(testdata.LiveDeploymentWithManagedReplicaYaml) trustedManagers := []string{"another-manager"} pt := parser.Type("io.k8s.api.apps.v1.Deployment") // when liveResult, desiredResult, err := managedfields.Normalize(liveState, desiredState, trustedManagers, &pt) // then require.NoError(t, err) validateNestedFloat64(t, float64(3), desiredResult, "spec", "replicas") validateNestedFloat64(t, float64(1), desiredResult, "spec", "revisionHistoryLimit") validateNestedFloat64(t, float64(2), liveResult, "spec", "replicas") validateNestedFloat64(t, float64(3), liveResult, "spec", "revisionHistoryLimit") }) t.Run("no-op if live state is nil", func(t *testing.T) { // given t.Parallel() desiredState := StrToUnstructured(testdata.DesiredDeploymentYaml) trustedManagers := []string{"kube-controller-manager"} pt := parser.Type("io.k8s.api.apps.v1.Deployment") // when liveResult, desiredResult, err := managedfields.Normalize(nil, desiredState, trustedManagers, &pt) // then require.NoError(t, err) assert.Nil(t, liveResult) assert.Nil(t, desiredResult) validateNestedFloat64(t, float64(3), desiredState, "spec", "replicas") validateNestedFloat64(t, float64(1), desiredState, "spec", "revisionHistoryLimit") }) t.Run("no-op if desired state is nil", func(t *testing.T) { // given t.Parallel() liveState := StrToUnstructured(testdata.LiveDeploymentWithManagedReplicaYaml) trustedManagers := []string{"kube-controller-manager"} pt := parser.Type("io.k8s.api.apps.v1.Deployment") // when liveResult, desiredResult, err := managedfields.Normalize(liveState, nil, trustedManagers, &pt) // then require.NoError(t, err) assert.Nil(t, liveResult) assert.Nil(t, desiredResult) validateNestedFloat64(t, float64(2), liveState, "spec", "replicas") validateNestedFloat64(t, float64(3), liveState, "spec", "revisionHistoryLimit") }) t.Run("no-op if trusted manager list is empty", func(t *testing.T) { // given t.Parallel() desiredState := StrToUnstructured(testdata.DesiredDeploymentYaml) liveState := StrToUnstructured(testdata.LiveDeploymentWithManagedReplicaYaml) pt := parser.Type("io.k8s.api.apps.v1.Deployment") // when liveResult, desiredResult, err := managedfields.Normalize(liveState, desiredState, []string{}, &pt) // then require.NoError(t, err) assert.Nil(t, liveResult) assert.Nil(t, desiredResult) validateNestedFloat64(t, float64(3), desiredState, "spec", "replicas") validateNestedFloat64(t, float64(1), desiredState, "spec", "revisionHistoryLimit") validateNestedFloat64(t, float64(2), liveState, "spec", "replicas") validateNestedFloat64(t, float64(3), liveState, "spec", "revisionHistoryLimit") }) t.Run("will normalize successfully inside a list", func(t *testing.T) { // given t.Parallel() desiredState := StrToUnstructured(testdata.DesiredValidatingWebhookYaml) liveState := StrToUnstructured(testdata.LiveValidatingWebhookYaml) trustedManagers := []string{"external-secrets"} pt := parser.Type("io.k8s.api.admissionregistration.v1.ValidatingWebhookConfiguration") // when liveResult, desiredResult, err := managedfields.Normalize(liveState, desiredState, trustedManagers, &pt) // then require.NoError(t, err) require.NotNil(t, liveResult) require.NotNil(t, desiredResult) var vwcLive arv1.ValidatingWebhookConfiguration err = runtime.DefaultUnstructuredConverter.FromUnstructured(liveResult.Object, &vwcLive) require.NoError(t, err) assert.Len(t, vwcLive.Webhooks, 1) assert.Empty(t, string(vwcLive.Webhooks[0].ClientConfig.CABundle)) var vwcConfig arv1.ValidatingWebhookConfiguration err = runtime.DefaultUnstructuredConverter.FromUnstructured(desiredResult.Object, &vwcConfig) require.NoError(t, err) assert.Len(t, vwcConfig.Webhooks, 1) assert.Empty(t, string(vwcConfig.Webhooks[0].ClientConfig.CABundle)) }) t.Run("does not fail if object fails validation schema", func(t *testing.T) { t.Parallel() desiredState := StrToUnstructured(testdata.DesiredDeploymentYaml) require.NoError(t, unstructured.SetNestedField(desiredState.Object, "spec", "hello", "world")) liveState := StrToUnstructured(testdata.LiveDeploymentWithManagedReplicaYaml) pt := parser.Type("io.k8s.api.apps.v1.Deployment") _, _, err := managedfields.Normalize(liveState, desiredState, []string{}, &pt) require.NoError(t, err) }) t.Run("does not panic when pt is nil", func(t *testing.T) { // ResolveParseableType can return nil when a CRD's GVK isn't in the // cluster's openapi schema; Normalize must fall back to the deduced // type rather than dereferencing nil. t.Parallel() desiredState := StrToUnstructured(testdata.DesiredDeploymentYaml) liveState := StrToUnstructured(testdata.LiveDeploymentWithManagedReplicaYaml) trustedManagers := []string{"kube-controller-manager"} liveResult, desiredResult, err := managedfields.Normalize(liveState, desiredState, trustedManagers, nil) require.NoError(t, err) require.NotNil(t, liveResult) require.NotNil(t, desiredResult) }) } func validateNestedFloat64(t *testing.T, expected float64, obj *unstructured.Unstructured, fields ...string) { t.Helper() current := getNestedFloat64(t, obj, fields...) assert.InEpsilon(t, expected, current, 0.0001) } func getNestedFloat64(t *testing.T, obj *unstructured.Unstructured, fields ...string) float64 { t.Helper() current, ok, err := unstructured.NestedFloat64(obj.Object, fields...) assert.True(t, ok, "nested field not found") require.NoError(t, err) return current } func StrToUnstructured(jsonStr string) *unstructured.Unstructured { obj := make(map[string]any) err := yaml.Unmarshal([]byte(jsonStr), &obj) if err != nil { panic(err) } return &unstructured.Unstructured{Object: obj} }