package e2e import ( "fmt" "path/filepath" "regexp" "testing" synccommon "github.com/hanzoai/cd/gitops-engine/pkg/sync/common" "github.com/hanzoai/cd/gitops-engine/pkg/utils/kube" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" applicationpkg "github.com/hanzoai/cd/pkg/apiclient/application" "github.com/hanzoai/cd/pkg/apis/application" . "github.com/hanzoai/cd/pkg/apis/application/v1alpha1" "github.com/hanzoai/cd/test/e2e/fixture" . "github.com/hanzoai/cd/test/e2e/fixture/app" "github.com/hanzoai/cd/test/e2e/testdata" "github.com/hanzoai/cd/util/errors" utilio "github.com/hanzoai/cd/util/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/hanzoai/cd/gitops-engine/pkg/health" ) // Values of `.data` & `.stringData“ fields in Secret resources are masked in UI/CLI // Optionally, values of `.metadata.annotations` can also be masked, if needed. func TestMaskSecretValues(t *testing.T) { sensitiveData := regexp.MustCompile(`SECRETVAL|NEWSECRETVAL|U0VDUkVUVkFM`) ctx := Given(t) ctx.Path("empty-dir"). When(). SetParamInSettingConfigMap("resource.sensitive.mask.annotations", "token"). // hide sensitive annotation AddFile("secrets.yaml", `apiVersion: v1 kind: Secret metadata: name: secret annotations: token: SECRETVAL app: test stringData: username: SECRETVAL data: password: U0VDUkVUVkFM `). CreateApp(). Sync(). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(HealthIs(health.HealthStatusHealthy)). // sensitive data should be masked in manifests output And(func(app *Application) { mnfs, _ := fixture.RunCli("app", "manifests", app.Name) assert.False(t, sensitiveData.MatchString(mnfs)) }). When(). PatchFile("secrets.yaml", `[{"op": "replace", "path": "/stringData/username", "value": "NEWSECRETVAL"}]`). PatchFile("secrets.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"token": "NEWSECRETVAL"}}]`). PatchFile("secrets.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"something": "else"}}]`). Refresh(RefreshTypeHard). Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). And(func(app *Application) { localRepoRoot := fixture.LocalRepoRoot() appPath := filepath.Join(localRepoRoot, app.Spec.Source.Path) // Normal diff should show a diff with the sensitive value masked diff, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--exit-code=false") require.NoError(t, err) assert.False(t, sensitiveData.MatchString(diff)) assert.Contains(t, diff, "===== /Secret") // Revision specific diff should show no differences diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--revision", app.Status.Sync.Revision, "--exit-code=false") require.NoError(t, err) assert.False(t, sensitiveData.MatchString(diff)) assert.Contains(t, diff, "===== /Secret") // Server-Side diff should show a diff with the sensitive value masked diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--exit-code=false") require.NoError(t, err) assert.Contains(t, diff, "===== /Secret") // Local diff with server-side-generate should show a diff with the sensitive value masked diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", localRepoRoot, "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Contains(t, diff, "===== /Secret") // Local diff should exclude secret resources completely diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", appPath, "--local-repo-root", localRepoRoot, "--exit-code=false") require.NoError(t, err) assert.Empty(t, diff, "Secret kind should not be displayed in CLI diff output") }) } // Secret values shouldn't be exposed in error messages and the diff view // when invalid secret is synced. func TestMaskValuesInInvalidSecret(t *testing.T) { sensitiveData := regexp.MustCompile(`SECRETVAL|U0VDUkVUVkFM|12345`) ctx := Given(t) ctx.Path("empty-dir"). When(). // valid secret AddFile("secrets.yaml", `apiVersion: v1 kind: Secret metadata: name: secret annotations: app: test stringData: username: SECRETVAL data: password: U0VDUkVUVkFM `). CreateApp(). Sync(). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(HealthIs(health.HealthStatusHealthy)). When(). // invalidate secret PatchFile("secrets.yaml", `[{"op": "replace", "path": "/data/password", "value": 12345}]`). Refresh(RefreshTypeHard). IgnoreErrors(). Sync(). Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). Expect(OperationPhaseIs(synccommon.OperationFailed)). // secret data shouldn't be exposed in manifests, diff & error output for invalid secret And(func(app *Application) { mnfs, _ := fixture.RunCli("app", "manifests", app.Name) assert.False(t, sensitiveData.MatchString(mnfs)) localRepoRoot := fixture.LocalRepoRoot() appPath := filepath.Join(localRepoRoot, app.Spec.Source.Path) // Normal diff should show a diff with the sensitive value masked diff, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--exit-code=false") require.NoError(t, err) assert.False(t, sensitiveData.MatchString(diff)) assert.Contains(t, diff, "===== /Secret") // Revision specific diff should show no differences diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--revision", app.Status.Sync.Revision, "--exit-code=false") require.NoError(t, err) assert.False(t, sensitiveData.MatchString(diff)) assert.Contains(t, diff, "===== /Secret") // Server-Side diff should show a diff with the sensitive value masked diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--exit-code=false") require.NoError(t, err) assert.Contains(t, diff, "===== /Secret") // Local diff with server-side-generate should show a diff with the sensitive value masked diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", localRepoRoot, "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Contains(t, diff, "===== /Secret") // Local diff should exclude secret resources completely diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", appPath, "--local-repo-root", localRepoRoot, "--exit-code=false") require.NoError(t, err) assert.Empty(t, diff, "Secret kind should not be displayed in CLI diff output") msg := app.Status.OperationState.Message assert.False(t, sensitiveData.MatchString(msg)) }) } // make sure that hooks do not appear in "cd app diff" func TestHookDiff(t *testing.T) { ctx := Given(t) ctx. Path("hook"). When(). CreateApp(). Then(). And(func(_ *Application) { output, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--exit-code=false") require.NoError(t, err) assert.Contains(t, output, "name: pod") assert.NotContains(t, output, "name: hook") output, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--exit-code=false") require.NoError(t, err) assert.Contains(t, output, "name: pod") assert.NotContains(t, output, "name: hook") }) } func TestDiff_DuplicatedResourcesAndNamespaceNormalization(t *testing.T) { testDiffResultsAreEmptyWithoutChanges(t, "duplicated-resources", fixture.TestNamespace()) } func TestDiff_CRDNoAnnotationTracking(t *testing.T) { testDiffResultsAreEmptyWithoutChanges(t, "crd-creation", fixture.TestNamespace()) } func TestDiff_AppNamespace_DuplicatedResourcesAndNamespaceNormalization(t *testing.T) { testDiffResultsAreEmptyWithoutChanges(t, "duplicated-resources", fixture.AppNamespace()) } func TestDiff_AppNamespace_CRDNoAnnotationTracking(t *testing.T) { testDiffResultsAreEmptyWithoutChanges(t, "crd-creation", fixture.AppNamespace()) } func testDiffResultsAreEmptyWithoutChanges(t *testing.T, appPath string, appNamespace string) { t.Helper() ctx := Given(t) ctx.SetTrackingMethod(string(TrackingMethodAnnotation)). Path(appPath). SetAppNamespace(appNamespace). When(). CreateApp(). Sync(). Then(). Expect(OperationPhaseIs(synccommon.OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(HealthIs(health.HealthStatusHealthy)). And(func(app *Application) { localRepoRoot := fixture.LocalRepoRoot() appPath := filepath.Join(localRepoRoot, app.Spec.Source.Path) diff, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--exit-code=false") require.NoError(t, err) assert.Empty(t, diff) diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--revision", app.Status.Sync.Revision, "--exit-code=false") require.NoError(t, err) assert.Empty(t, diff) diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--exit-code=false") require.NoError(t, err) assert.Empty(t, diff) diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", localRepoRoot, "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Empty(t, diff) diff, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", appPath, "--local-repo-root", localRepoRoot, "--exit-code=false") require.NoError(t, err) assert.Empty(t, diff) }) } func TestHelmRepoDiffLocal(t *testing.T) { fixture.SkipOnEnv(t, "HELM") helmTmp := t.TempDir() ctx := Given(t) ctx.CustomCACertAdded(). HelmRepoAdded("custom-repo"). RepoURLType(fixture.RepoURLTypeHelm). Chart("helm"). Revision("1.0.0"). When(). CreateApp(). Then(). When(). Sync(). Then(). Expect(OperationPhaseIs(synccommon.OperationSucceeded)). Expect(HealthIs(health.HealthStatusHealthy)). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { appPath := "testdata/helm" t.Setenv("XDG_CONFIG_HOME", helmTmp) errors.NewHandler(t).FailOnErr(fixture.Run("", "helm", "repo", "add", "custom-repo", fixture.GetEnvWithDefault("CD_E2E_HELM_SERVICE", fixture.RepoURL(fixture.RepoURLTypeHelm)), "--username", fixture.GitUsername, "--password", fixture.GitPassword, "--cert-file", "../fixture/certs/cd-test-client.crt", "--key-file", "../fixture/certs/cd-test-client.key", "--ca-file", "../fixture/certs/cd-test-ca.crt", )) diffOutput, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", appPath, "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Empty(t, diffOutput) diffOutput, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--local", appPath, "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Empty(t, diffOutput) }) } // make sure we can sync and diff with --local func TestCustomToolSyncAndDiffLocal(t *testing.T) { testdataPath := fixture.LocalRepoRoot() appPath := filepath.Join(testdataPath, "config-map") ctx := Given(t) ctx. RunningCMPServer("./testdata/cmp-kustomize"). // does not matter what the path is Path("config-map"). When(). CreateApp("--config-management-plugin", "cmp-kustomize-v1.0"). Sync("--local", appPath, "--local-repo-root", testdataPath). Then(). Expect(OperationPhaseIs(synccommon.OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(HealthIs(health.HealthStatusHealthy)). And(func(_ *Application) { _, err := fixture.RunCli("app", "sync", ctx.AppName(), "--local", appPath, "--local-repo-root", testdataPath) require.NoError(t, err) }). Expect(OperationPhaseIs(synccommon.OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(HealthIs(health.HealthStatusHealthy)). And(func(_ *Application) { diffOutput, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", appPath, "--local-repo-root", testdataPath, "--exit-code=false") require.NoError(t, err) assert.Empty(t, diffOutput) diffOutput, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", testdataPath, "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Empty(t, diffOutput) diffOutput, err = fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--local", testdataPath, "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Empty(t, diffOutput) }) } // TestServerSideDiffWithLocal tests server-side diff with --local flag func TestServerSideDiffWithLocal(t *testing.T) { ctx := Given(t) ctx. Path("guestbook"). When(). CreateApp(). Sync(). Then(). Expect(OperationPhaseIs(synccommon.OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { // Modify the live deployment in the cluster to create differences // Apply patches to the deployment _, err := fixture.KubeClientset.AppsV1().Deployments(ctx.DeploymentNamespace()).Patch(t.Context(), "guestbook-ui", types.JSONPatchType, []byte(`[ {"op": "add", "path": "/spec/template/spec/containers/0/env", "value": [{"name": "LOCAL_CHANGE", "value": "true"}]}, {"op": "replace", "path": "/spec/replicas", "value": 2} ]`), metav1.PatchOptions{}) require.NoError(t, err) // Verify the patch was applied by reading back the deployment modifiedDeployment, err := fixture.KubeClientset.AppsV1().Deployments(ctx.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) require.NoError(t, err) assert.Equal(t, int32(2), *modifiedDeployment.Spec.Replicas, "Replica count should be updated to 2") assert.Len(t, modifiedDeployment.Spec.Template.Spec.Containers[0].Env, 1, "Should have one environment variable") assert.Equal(t, "LOCAL_CHANGE", modifiedDeployment.Spec.Template.Spec.Containers[0].Env[0].Name) }). When(). Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). And(func(_ *Application) { // Test regular diff with --local (add --server-side-generate to avoid deprecation warning) regularOutput, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata", "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Contains(t, regularOutput, "===== apps/Deployment") assert.Contains(t, regularOutput, "guestbook-ui") assert.Contains(t, regularOutput, "replicas:") // Test server-side diff with --local (add --server-side-generate for consistency) serverSideOutput, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--local", "testdata", "--server-side-generate", "--exit-code=false") require.NoError(t, err) assert.Contains(t, serverSideOutput, "===== apps/Deployment") assert.Contains(t, serverSideOutput, "guestbook-ui") assert.Contains(t, serverSideOutput, "replicas:") // Both outputs should show similar differences assert.Contains(t, regularOutput, "replicas: 2") assert.Contains(t, serverSideOutput, "replicas: 2") }) } func TestServerSideDiffWithLocalValidation(t *testing.T) { ctx := Given(t) ctx.Path("guestbook"). When(). CreateApp(). Sync(). Then(). Expect(OperationPhaseIs(synccommon.OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { // Test that --server-side-diff with --local without --server-side-generate fails with proper error _, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--server-side-diff", "--local", "testdata") require.Error(t, err) assert.Contains(t, err.Error(), "--server-side-diff with --local requires --server-side-generate") }) } // func assetSecretDataHidden(t *testing.T, manifest string) { t.Helper() secret, err := UnmarshalToUnstructured(manifest) require.NoError(t, err) _, hasStringData, err := unstructured.NestedMap(secret.Object, "stringData") require.NoError(t, err) assert.False(t, hasStringData) secretData, hasData, err := unstructured.NestedMap(secret.Object, "data") require.NoError(t, err) assert.True(t, hasData) for _, v := range secretData { assert.Regexp(t, `[*]*`, v) } var lastAppliedConfigAnnotation string annotations := secret.GetAnnotations() if annotations != nil { lastAppliedConfigAnnotation = annotations[corev1.LastAppliedConfigAnnotation] } if lastAppliedConfigAnnotation != "" { assetSecretDataHidden(t, lastAppliedConfigAnnotation) } } func TestAppWithSecrets(t *testing.T) { closer, client, err := fixture.ArgoCDClientset.NewApplicationClient() require.NoError(t, err) defer utilio.Close(closer) ctx := Given(t) ctx.Path("secrets"). When(). CreateApp(). Sync(). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(app *Application) { res := errors.NewHandler(t).FailOnErr(client.GetResource(t.Context(), &applicationpkg.ApplicationResourceRequest{ Namespace: &app.Spec.Destination.Namespace, Kind: new(kube.SecretKind), Group: new(""), Name: &app.Name, Version: new("v1"), ResourceName: new("test-secret"), })).(*applicationpkg.ApplicationResourceResponse) assetSecretDataHidden(t, res.GetManifest()) manifests, err := client.GetManifests(t.Context(), &applicationpkg.ApplicationManifestQuery{Name: &app.Name}) require.NoError(t, err) for _, manifest := range manifests.Manifests { assetSecretDataHidden(t, manifest) } diffOutput := errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "diff", ctx.AppQualifiedName())).(string) assert.Empty(t, diffOutput) // make sure resource update error does not print secret details _, err = fixture.RunCli("app", "patch-resource", app.GetName(), "--resource-name", "test-secret", "--kind", "Secret", "--patch", `{"op": "add", "path": "/data", "value": "hello"}'`, "--patch-type", "application/json-patch+json") require.ErrorContains(t, err, fmt.Sprintf("failed to patch Secret %s/test-secret", ctx.DeploymentNamespace())) assert.NotContains(t, err.Error(), "username") assert.NotContains(t, err.Error(), "password") // patch secret and make sure app is out of sync and diff detects the change errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().Secrets(ctx.DeploymentNamespace()).Patch(t.Context(), "test-secret", types.JSONPatchType, []byte(`[ {"op": "remove", "path": "/data/username"}, {"op": "add", "path": "/stringData", "value": {"password": "foo"}} ]`), metav1.PatchOptions{})) }). When(). Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). And(func(app *Application) { diffOutput, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName()) require.Error(t, err) assert.Contains(t, diffOutput, "username: ++++++++") assert.Contains(t, diffOutput, "password: ++++++++++++") // ignore missing field and make sure diff shows no difference app.Spec.IgnoreDifferences = []ResourceIgnoreDifferences{{ Kind: kube.SecretKind, JSONPointers: []string{"/data"}, }} errors.NewHandler(t).FailOnErr(client.UpdateSpec(t.Context(), &applicationpkg.ApplicationUpdateSpecRequest{Name: &app.Name, Spec: &app.Spec})) }). When(). Refresh(RefreshTypeNormal). Then(). Expect(OperationPhaseIs(synccommon.OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { diffOutput := errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "diff", ctx.AppQualifiedName())).(string) assert.Empty(t, diffOutput) }). // verify not committed secret also ignore during diffing When(). WriteFile("secret3.yaml", ` apiVersion: v1 kind: Secret metadata: name: test-secret3 stringData: username: test-username`). Then(). And(func(_ *Application) { diffOutput := errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata", "--server-side-generate")).(string) assert.Empty(t, diffOutput) }) } func TestResourceDiffing(t *testing.T) { ctx := Given(t) ctx.Path(guestbookPath). When(). CreateApp(). Sync(). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { // Patch deployment _, err := fixture.KubeClientset.AppsV1().Deployments(ctx.DeploymentNamespace()).Patch(t.Context(), "guestbook-ui", types.JSONPatchType, []byte(`[{ "op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "test" }]`), metav1.PatchOptions{}) require.NoError(t, err) }). When(). Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). And(func(_ *Application) { diffOutput, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata", "--server-side-generate") require.Error(t, err) assert.Contains(t, diffOutput, fmt.Sprintf("===== apps/Deployment %s/guestbook-ui ======", ctx.DeploymentNamespace())) }). Given(). ResourceOverrides(map[string]ResourceOverride{"apps/Deployment": { IgnoreDifferences: OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers/0/image"}}, }}). When(). Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { diffOutput, err := fixture.RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata", "--server-side-generate") require.NoError(t, err) assert.Empty(t, diffOutput) }). Given(). When(). // Now we migrate from client-side apply to server-side apply // This is necessary, as starting with kubectl 1.26, all previously // client-side owned fields have ownership migrated to the manager from // the first ssa. // More details: https://github.com/kubernetes/kubectl/issues/1337 PatchApp(`[{ "op": "add", "path": "/spec/syncPolicy", "value": { "syncOptions": ["ServerSideApply=true"] } }]`). Sync(). And(func() { output, err := fixture.RunWithStdin(testdata.SSARevisionHistoryDeployment, "", "kubectl", "apply", "-n", ctx.DeploymentNamespace(), "--server-side=true", "--field-manager=revision-history-manager", "--validate=false", "--force-conflicts", "-f", "-") require.NoError(t, err) assert.Contains(t, output, "serverside-applied") }). Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). Given(). ResourceOverrides(map[string]ResourceOverride{"apps/Deployment": { IgnoreDifferences: OverrideIgnoreDiff{ ManagedFieldsManagers: []string{"revision-history-manager"}, JSONPointers: []string{"/spec/template/spec/containers/0/image"}, }, }}). When(). Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). Given(). When(). Sync(). PatchApp(`[{ "op": "add", "path": "/spec/syncPolicy", "value": { "syncOptions": ["RespectIgnoreDifferences=true"] } }]`). And(func() { deployment, err := fixture.KubeClientset.AppsV1().Deployments(ctx.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) require.NoError(t, err) assert.Equal(t, int32(3), *deployment.Spec.RevisionHistoryLimit) }). And(func() { output, err := fixture.RunWithStdin(testdata.SSARevisionHistoryDeployment, "", "kubectl", "apply", "-n", ctx.DeploymentNamespace(), "--server-side=true", "--field-manager=revision-history-manager", "--validate=false", "--force-conflicts", "-f", "-") require.NoError(t, err) assert.Contains(t, output, "serverside-applied") }). Then(). When().Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { deployment, err := fixture.KubeClientset.AppsV1().Deployments(ctx.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) require.NoError(t, err) assert.Equal(t, int32(1), *deployment.Spec.RevisionHistoryLimit) }). When().Sync().Then().Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(_ *Application) { deployment, err := fixture.KubeClientset.AppsV1().Deployments(ctx.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) require.NoError(t, err) assert.Equal(t, int32(1), *deployment.Spec.RevisionHistoryLimit) }) } func TestKnownTypesInCRDDiffing(t *testing.T) { dummiesGVR := schema.GroupVersionResource{Group: application.Group, Version: "v1alpha1", Resource: "dummies"} ctx := Given(t) ctx.Path("crd-creation"). When().CreateApp().Sync().Then(). Expect(OperationPhaseIs(synccommon.OperationSucceeded)).Expect(SyncStatusIs(SyncStatusCodeSynced)). When(). And(func() { dummyResIf := fixture.DynamicClientset.Resource(dummiesGVR).Namespace(ctx.DeploymentNamespace()) patchData := []byte(`{"spec":{"cpu": "2"}}`) errors.NewHandler(t).FailOnErr(dummyResIf.Patch(t.Context(), "dummy-crd-instance", types.MergePatchType, patchData, metav1.PatchOptions{})) }).Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). When(). And(func() { require.NoError(t, fixture.SetResourceOverrides(map[string]ResourceOverride{ "apps.hanzo.ai/Dummy": { KnownTypeFields: []KnownTypeField{{ Field: "spec", Type: "core/v1/ResourceList", }}, }, })) }). Refresh(RefreshTypeNormal). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)) }