Files
deploy/util/cd/managedfields/managed_fields_test.go
zeekayandHanzo Dev 1ab6e799c1 cd: the module is github.com/hanzoai/cd, where it is actually hosted
go.mod still declared `module github.com/hanzoai/deploy` after the repo was
renamed. A Go module path is not cosmetic — it is the resolvable location. So
`go get github.com/hanzoai/cd` could not work, and anything importing this
module had to spell a name that no longer exists at that address. The build log
of v3.6.0 is where it surfaced: the compiler was still emitting
github.com/hanzoai/deploy/cmd/cd/commands.

3541 import paths across 690 .go, 19 .proto, 4 .yaml, 3 .sh, 2 .mod, Procfile
and Makefile. Plus the 62 ghcr.io/hanzoai/deploy image references, moved to
ghcr.io/hanzoai/cd to match hanzo.yml's build target — a distinct string from
the module path, replaced separately so neither could clobber the other.

go mod tidy clean, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./... clean.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 21:16:24 -07:00

203 lines
7.7 KiB
Go

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}
}