Ctrl K
ComunidadGuías…

pkg/k8score/workload_update_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package k8score

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"testing"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	dynamicfake "k8s.io/client-go/dynamic/fake"
	clienttesting "k8s.io/client-go/testing"
)

const clusterPolicyYAML = `
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
  resourceVersion: "12345"
  uid: 5e8a1b2c-3d4f-5a6b-7c8d-9e0f1a2b3c4d
  managedFields:
  - manager: kyverno
    operation: Update
spec:
  validationFailureAction: Audit
status:
  ready: true
`

func newFakeDynamicWithClusterPolicy(t *testing.T) (*dynamicfake.FakeDynamicClient, schema.GroupVersionResource) {
	t.Helper()
	gvr := schema.GroupVersionResource{Group: "kyverno.io", Version: "v1", Resource: "clusterpolicies"}
	scheme := runtime.NewScheme()
	listKinds := map[schema.GroupVersionResource]string{gvr: "ClusterPolicyList"}
	return dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds), gvr
}

// stubDiscovery installs a single Kind→GVR mapping so UpdateResource skips the
// real discovery client. ResourceDiscovery's exported API doesn't allow direct
// seeding, but the unexported maps are package-internal — fine for tests.
func stubDiscovery(t *testing.T, kind string, gvr schema.GroupVersionResource) *ResourceDiscovery {
	t.Helper()
	rd := &ResourceDiscovery{
		resourceMap: map[string]APIResource{
			strings.ToLower(kind): {Kind: kind, Name: gvr.Resource, Group: gvr.Group, Version: gvr.Version, Namespaced: false},
			gvr.Resource:          {Kind: kind, Name: gvr.Resource, Group: gvr.Group, Version: gvr.Version, Namespaced: false},
		},
		gvrMap: map[string]schema.GroupVersionResource{
			strings.ToLower(kind): gvr,
			gvr.Resource:          gvr,
		},
	}
	return rd
}

// TestUpdateResource_UsesServerSideApply pins the SSA wire shape: PATCH with
// ApplyPatchType, FieldManager=radar, Force=true, and server-managed metadata
// stripped from the body. Editor flows submit YAML without a resourceVersion,
// which PUT rejects — SSA is the contract.
func TestUpdateResource_UsesServerSideApply(t *testing.T) {
	dyn, gvr := newFakeDynamicWithClusterPolicy(t)
	disc := stubDiscovery(t, "ClusterPolicy", gvr)
	mgr := NewWorkloadManager(dyn, disc)

	var captured clienttesting.PatchAction
	dyn.PrependReactor("patch", "clusterpolicies", func(a clienttesting.Action) (bool, runtime.Object, error) {
		captured = a.(clienttesting.PatchAction)
		// Return a minimal object so the call succeeds.
		return true, nil, nil
	})

	_, err := mgr.UpdateResource(context.Background(), UpdateResourceOptions{
		Kind:  "clusterpolicies",
		Name:  "restrict-image-registries",
		YAML:  clusterPolicyYAML,
		Force: true,
	})
	if err != nil {
		t.Fatalf("UpdateResource failed: %v", err)
	}

	if captured == nil {
		t.Fatal("expected a PATCH action; got none")
	}
	if got := captured.GetPatchType(); got != types.ApplyPatchType {
		t.Errorf("patch type = %v, want %v (server-side apply)", got, types.ApplyPatchType)
	}

	impl, ok := captured.(clienttesting.PatchActionImpl)
	if !ok {
		t.Fatalf("captured action is %T, want clienttesting.PatchActionImpl", captured)
	}
	if impl.PatchOptions.FieldManager != "radar" {
		t.Errorf("FieldManager = %q, want %q", impl.PatchOptions.FieldManager, "radar")
	}
	if impl.PatchOptions.FieldValidation != metav1.FieldValidationStrict {
		t.Errorf("FieldValidation = %q, want %q", impl.PatchOptions.FieldValidation, metav1.FieldValidationStrict)
	}
	if impl.PatchOptions.Force == nil || !*impl.PatchOptions.Force {
		t.Errorf("Force = %v, want *true", impl.PatchOptions.Force)
	}

	var body map[string]any
	if err := json.Unmarshal(captured.GetPatch(), &body); err != nil {
		t.Fatalf("patch body is not JSON: %v", err)
	}
	meta, _ := body["metadata"].(map[string]any)
	for _, banned := range []string{"resourceVersion", "uid", "managedFields", "generation", "creationTimestamp", "selfLink"} {
		if _, present := meta[banned]; present {
			t.Errorf("patch body still contains metadata.%s; SSA expects these stripped", banned)
		}
	}
	// status must NOT be stripped: CRDs without a status subresource treat it
	// as a user-writable field, and stripping silently discards user edits.
	// For subresourced kinds, the apiserver ignores status on /apply anyway.
	if _, present := body["status"]; !present {
		t.Error("status was stripped from the patch body; CRDs without status subresource need it preserved")
	}
}

// TestUpdateResource_ForcePlumbedToPatchOptions verifies the caller's Force
// choice reaches the SSA PatchOptions (the editor's Force checkbox opts out).
func TestUpdateResource_ForcePlumbedToPatchOptions(t *testing.T) {
	dyn, gvr := newFakeDynamicWithClusterPolicy(t)
	disc := stubDiscovery(t, "ClusterPolicy", gvr)
	mgr := NewWorkloadManager(dyn, disc)

	var captured clienttesting.PatchAction
	dyn.PrependReactor("patch", "clusterpolicies", func(a clienttesting.Action) (bool, runtime.Object, error) {
		captured = a.(clienttesting.PatchAction)
		return true, nil, nil
	})

	_, err := mgr.UpdateResource(context.Background(), UpdateResourceOptions{
		Kind:  "ClusterPolicy",
		Name:  "restrict-image-registries",
		YAML:  clusterPolicyYAML,
		Force: false,
	})
	if err != nil {
		t.Fatalf("UpdateResource failed: %v", err)
	}
	impl, ok := captured.(clienttesting.PatchActionImpl)
	if !ok {
		t.Fatalf("captured action is %T, want clienttesting.PatchActionImpl", captured)
	}
	if impl.PatchOptions.Force == nil || *impl.PatchOptions.Force {
		t.Errorf("Force = %v, want *false", impl.PatchOptions.Force)
	}
}

// TestUpdateResource_RejectsMismatchedName guards the existing safety check
// (caller's URL params must match the YAML body).
func TestUpdateResource_RejectsMismatchedName(t *testing.T) {
	dyn, gvr := newFakeDynamicWithClusterPolicy(t)
	disc := stubDiscovery(t, "ClusterPolicy", gvr)
	mgr := NewWorkloadManager(dyn, disc)

	_, err := mgr.UpdateResource(context.Background(), UpdateResourceOptions{
		Kind: "ClusterPolicy",
		Name: "different-name",
		YAML: clusterPolicyYAML,
	})
	if err == nil || !strings.Contains(err.Error(), "name mismatch") {
		t.Fatalf("expected name mismatch error, got: %v", err)
	}
}

func TestUpdateResource_RejectsMismatchedKind(t *testing.T) {
	dyn, gvr := newFakeDynamicWithClusterPolicy(t)
	disc := stubDiscovery(t, "ClusterPolicy", gvr)
	podGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
	disc.resources = append(disc.resources,
		APIResource{Kind: "ClusterPolicy", Name: gvr.Resource, Group: gvr.Group, Version: gvr.Version},
		APIResource{Kind: "Pod", Name: podGVR.Resource, Group: podGVR.Group, Version: podGVR.Version},
	)
	disc.resourceMap["pod"] = APIResource{Kind: "Pod", Name: podGVR.Resource, Group: podGVR.Group, Version: podGVR.Version}
	disc.resourceMap[podGVR.Resource] = APIResource{Kind: "Pod", Name: podGVR.Resource, Group: podGVR.Group, Version: podGVR.Version}
	disc.gvrMap["pod"] = podGVR
	disc.gvrMap[podGVR.Resource] = podGVR
	mgr := NewWorkloadManager(dyn, disc)

	_, err := mgr.UpdateResource(context.Background(), UpdateResourceOptions{
		Kind: "Pod",
		Name: "restrict-image-registries",
		YAML: clusterPolicyYAML,
	})
	if err == nil || !strings.Contains(err.Error(), "kind mismatch") {
		t.Fatalf("expected kind mismatch error, got: %v", err)
	}
}

func TestPreviewUpdateResource_DryRunsSamePatchWithResourceVersion(t *testing.T) {
	dyn, gvr := newFakeDynamicWithClusterPolicy(t)
	disc := stubDiscovery(t, "ClusterPolicy", gvr)
	mgr := NewWorkloadManager(dyn, disc)
	live := &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "kyverno.io/v1",
		"kind":       "ClusterPolicy",
		"metadata": map[string]any{
			"name":            "restrict-image-registries",
			"resourceVersion": "42",
		},
	}}
	getCount := 0
	dyn.PrependReactor("get", "clusterpolicies", func(clienttesting.Action) (bool, runtime.Object, error) {
		getCount++
		return true, live.DeepCopy(), nil
	})
	var captured clienttesting.PatchActionImpl
	dyn.PrependReactor("patch", "clusterpolicies", func(a clienttesting.Action) (bool, runtime.Object, error) {
		captured = a.(clienttesting.PatchActionImpl)
		return true, live.DeepCopy(), nil
	})

	result, err := mgr.PreviewUpdateResource(context.Background(), UpdateResourceOptions{
		Kind: "ClusterPolicy",
		Name: "restrict-image-registries",
		YAML: clusterPolicyYAML,
	})
	if err != nil {
		t.Fatalf("PreviewUpdateResource failed: %v", err)
	}
	if result.ResourceVersion != "42" {
		t.Fatalf("ResourceVersion = %q, want 42", result.ResourceVersion)
	}
	if getCount != 1 {
		t.Fatalf("GET count = %d, want one live read", getCount)
	}
	if len(captured.PatchOptions.DryRun) != 1 || captured.PatchOptions.DryRun[0] != metav1.DryRunAll {
		t.Fatalf("DryRun = %v, want [%q]", captured.PatchOptions.DryRun, metav1.DryRunAll)
	}
	if captured.PatchOptions.FieldValidation != metav1.FieldValidationStrict {
		t.Fatalf("FieldValidation = %q, want %q", captured.PatchOptions.FieldValidation, metav1.FieldValidationStrict)
	}
	var body map[string]any
	if err := json.Unmarshal(captured.GetPatch(), &body); err != nil {
		t.Fatalf("patch body is not JSON: %v", err)
	}
	meta := body["metadata"].(map[string]any)
	if meta["resourceVersion"] != "42" {
		t.Fatalf("metadata.resourceVersion = %v, want 42", meta["resourceVersion"])
	}
}

func TestUpdateResource_RejectsStaleReviewedVersionBeforePatch(t *testing.T) {
	dyn, gvr := newFakeDynamicWithClusterPolicy(t)
	disc := stubDiscovery(t, "ClusterPolicy", gvr)
	mgr := NewWorkloadManager(dyn, disc)
	dyn.PrependReactor("get", "clusterpolicies", func(clienttesting.Action) (bool, runtime.Object, error) {
		return true, &unstructured.Unstructured{Object: map[string]any{
			"metadata": map[string]any{"name": "restrict-image-registries", "resourceVersion": "new"},
		}}, nil
	})
	patched := false
	dyn.PrependReactor("patch", "clusterpolicies", func(clienttesting.Action) (bool, runtime.Object, error) {
		patched = true
		return true, nil, nil
	})

	_, err := mgr.UpdateResource(context.Background(), UpdateResourceOptions{
		Kind:                    "ClusterPolicy",
		Name:                    "restrict-image-registries",
		YAML:                    clusterPolicyYAML,
		ExpectedResourceVersion: "reviewed",
	})
	if !apierrors.IsConflict(err) {
		t.Fatalf("error = %v, want conflict", err)
	}
	if patched {
		t.Fatal("stale review reached PATCH")
	}
}

func TestApplyResource_RejectsStaleReviewedVersionBeforePatch(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	pre := mkWorkload(t, "Deployment", nil)
	pre.SetResourceVersion("new")
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
		pre,
	)
	patched := false
	dyn.PrependReactor("patch", "deployments", func(clienttesting.Action) (bool, runtime.Object, error) {
		patched = true
		return true, nil, nil
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	_, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		ExpectedResourceVersion: "reviewed",
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if !apierrors.IsConflict(err) {
		t.Fatalf("error = %v, want conflict", err)
	}
	if patched {
		t.Fatal("stale review reached PATCH")
	}
}

func TestApplyResource_RejectsResourceCreatedAfterReview(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	createdAfterReview := mkWorkload(t, "Deployment", nil)
	createdAfterReview.SetResourceVersion("1")
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
		createdAfterReview,
	)
	patched := false
	dyn.PrependReactor("patch", "deployments", func(clienttesting.Action) (bool, runtime.Object, error) {
		patched = true
		return true, nil, nil
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	_, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		ExpectedResourceAbsent: true,
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if !apierrors.IsConflict(err) {
		t.Fatalf("error = %v, want conflict", err)
	}
	if patched {
		t.Fatal("resource created after review reached PATCH")
	}
}

func TestApplyResource_ReviewedCreateIsAtomic(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
	)
	patched := false
	dyn.PrependReactor("get", "deployments", func(clienttesting.Action) (bool, runtime.Object, error) {
		return true, nil, apierrors.NewNotFound(gvr.GroupResource(), "w")
	})
	dyn.PrependReactor("create", "deployments", func(action clienttesting.Action) (bool, runtime.Object, error) {
		create := action.(clienttesting.CreateActionImpl)
		if create.GetCreateOptions().FieldManager != "radar" {
			t.Fatalf("field manager = %q, want radar", create.GetCreateOptions().FieldManager)
		}
		if create.GetCreateOptions().FieldValidation != metav1.FieldValidationStrict {
			t.Fatalf("field validation = %q, want %q", create.GetCreateOptions().FieldValidation, metav1.FieldValidationStrict)
		}
		return true, nil, apierrors.NewAlreadyExists(gvr.GroupResource(), "w")
	})
	dyn.PrependReactor("patch", "deployments", func(clienttesting.Action) (bool, runtime.Object, error) {
		patched = true
		return true, nil, nil
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	_, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		ExpectedResourceAbsent: true,
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if !apierrors.IsAlreadyExists(err) {
		t.Fatalf("error = %v, want already exists", err)
	}
	if patched {
		t.Fatal("reviewed create reached PATCH")
	}
}

func TestApplyResource_ReviewedCreateReportsCreated(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
	)
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	result, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		ExpectedResourceAbsent: true,
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if err != nil {
		t.Fatalf("ApplyResource failed: %v", err)
	}
	if !result.Created {
		t.Fatal("Created = false, want true")
	}
	if result.Action != "create" {
		t.Fatalf("Action = %q, want create", result.Action)
	}
	if result.APIVersion != "apps/v1" {
		t.Fatalf("APIVersion = %q, want apps/v1", result.APIVersion)
	}
}

func TestApplyResource_PreviewUsesCreateForAbsentResource(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
	)
	var captured clienttesting.CreateActionImpl
	patched := false
	dyn.PrependReactor("create", "deployments", func(action clienttesting.Action) (bool, runtime.Object, error) {
		captured = action.(clienttesting.CreateActionImpl)
		return false, nil, nil
	})
	dyn.PrependReactor("patch", "deployments", func(clienttesting.Action) (bool, runtime.Object, error) {
		patched = true
		return true, nil, nil
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	result, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		DryRun:             true,
		UseCreateForAbsent: true,
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if err != nil {
		t.Fatalf("ApplyResource failed: %v", err)
	}
	if patched {
		t.Fatal("absent-resource preview reached PATCH")
	}
	if len(captured.CreateOptions.DryRun) != 1 || captured.CreateOptions.DryRun[0] != metav1.DryRunAll {
		t.Fatalf("DryRun = %v, want [%q]", captured.CreateOptions.DryRun, metav1.DryRunAll)
	}
	if captured.CreateOptions.FieldManager != "radar" {
		t.Fatalf("FieldManager = %q, want radar", captured.CreateOptions.FieldManager)
	}
	if captured.CreateOptions.FieldValidation != metav1.FieldValidationStrict {
		t.Fatalf("FieldValidation = %q, want %q", captured.CreateOptions.FieldValidation, metav1.FieldValidationStrict)
	}
	if !result.Created || result.Action != "create" {
		t.Fatalf("result = %+v, want created action", result)
	}
}

func TestApplyResource_RetriesOnlyRadarOwnershipConflictWithForce(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	existing := &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "apps/v1",
		"kind":       "Deployment",
		"metadata": map[string]any{
			"name":            "w",
			"namespace":       "ns",
			"resourceVersion": "1",
		},
	}}
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
		existing,
	)
	patches := make([]clienttesting.PatchActionImpl, 0, 3)
	dyn.PrependReactor("patch", "deployments", func(action clienttesting.Action) (bool, runtime.Object, error) {
		patch := action.(clienttesting.PatchActionImpl)
		patches = append(patches, patch)
		if patch.PatchOptions.Force == nil || !*patch.PatchOptions.Force {
			return true, nil, &apierrors.StatusError{ErrStatus: metav1.Status{
				Status: metav1.StatusFailure,
				Reason: metav1.StatusReasonConflict,
				Code:   409,
				Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{{
					Type:    metav1.CauseTypeFieldManagerConflict,
					Field:   ".spec.replicas",
					Message: `conflict with "radar" using apps/v1`,
				}}},
			}}
		}
		return true, existing.DeepCopy(), nil
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	result, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if err != nil {
		t.Fatalf("ApplyResource failed: %v", err)
	}
	if len(patches) != 3 {
		t.Fatalf("patch count = %d, want 3", len(patches))
	}
	if patches[0].PatchOptions.Force == nil || *patches[0].PatchOptions.Force {
		t.Fatalf("first force = %v, want false", patches[0].PatchOptions.Force)
	}
	if patches[1].PatchOptions.Force == nil || *patches[1].PatchOptions.Force {
		t.Fatalf("verified retry force = %v, want false", patches[1].PatchOptions.Force)
	}
	if patches[2].PatchOptions.Force == nil || !*patches[2].PatchOptions.Force {
		t.Fatalf("reclaim force = %v, want true", patches[2].PatchOptions.Force)
	}
	for index, patch := range patches[1:] {
		var body unstructured.Unstructured
		if err := json.Unmarshal(patch.GetPatch(), &body.Object); err != nil {
			t.Fatalf("decode patch %d: %v", index+1, err)
		}
		if body.GetResourceVersion() != "1" {
			t.Fatalf("patch %d resourceVersion = %q, want 1", index+1, body.GetResourceVersion())
		}
	}
	if len(result.Warnings) == 0 || !strings.Contains(result.Warnings[0], "Radar reclaimed field ownership") {
		t.Fatalf("warnings = %v, want Radar ownership warning", result.Warnings)
	}
}

func TestApplyResource_DoesNotReclaimWhenExternalConflictAppears(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	existing := &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "apps/v1",
		"kind":       "Deployment",
		"metadata": map[string]any{
			"name":            "w",
			"namespace":       "ns",
			"resourceVersion": "1",
		},
	}}
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
		existing,
	)
	patchCount := 0
	dyn.PrependReactor("patch", "deployments", func(action clienttesting.Action) (bool, runtime.Object, error) {
		patchCount++
		patch := action.(clienttesting.PatchActionImpl)
		if patch.PatchOptions.Force != nil && *patch.PatchOptions.Force {
			t.Fatal("external conflict reached forced retry")
		}
		manager := "radar"
		if patchCount == 2 {
			manager = "argocd"
		}
		return true, nil, &apierrors.StatusError{ErrStatus: metav1.Status{
			Status: metav1.StatusFailure,
			Reason: metav1.StatusReasonConflict,
			Code:   409,
			Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{{
				Type:    metav1.CauseTypeFieldManagerConflict,
				Field:   ".spec.replicas",
				Message: fmt.Sprintf("conflict with %q using apps/v1", manager),
			}}},
		}}
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	_, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if !apierrors.IsConflict(err) {
		t.Fatalf("error = %v, want external-manager conflict", err)
	}
	if patchCount != 2 {
		t.Fatalf("patch count = %d, want 2", patchCount)
	}
}

func TestPreviewUpdateResource_ReclaimsRadarOwnershipWithWarning(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	existing := &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "apps/v1",
		"kind":       "Deployment",
		"metadata": map[string]any{
			"name":            "w",
			"namespace":       "ns",
			"resourceVersion": "1",
		},
	}}
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
		existing,
	)
	patchCount := 0
	dyn.PrependReactor("patch", "deployments", func(action clienttesting.Action) (bool, runtime.Object, error) {
		patchCount++
		patch := action.(clienttesting.PatchActionImpl)
		if patch.PatchOptions.Force == nil || !*patch.PatchOptions.Force {
			return true, nil, &apierrors.StatusError{ErrStatus: metav1.Status{
				Status: metav1.StatusFailure,
				Reason: metav1.StatusReasonConflict,
				Code:   409,
				Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{{
					Type:    metav1.CauseTypeFieldManagerConflict,
					Field:   ".spec.replicas",
					Message: `conflict with "radar" using apps/v1`,
				}}},
			}}
		}
		return true, existing.DeepCopy(), nil
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	result, err := mgr.PreviewUpdateResource(context.Background(), UpdateResourceOptions{
		Kind:      "Deployment",
		Namespace: "ns",
		Name:      "w",
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
`,
	})
	if err != nil {
		t.Fatalf("PreviewUpdateResource failed: %v", err)
	}
	if patchCount != 2 {
		t.Fatalf("patch count = %d, want 2", patchCount)
	}
	if len(result.Warnings) == 0 || !strings.Contains(result.Warnings[0], "Radar reclaimed field ownership") {
		t.Fatalf("warnings = %v, want Radar ownership warning", result.Warnings)
	}
}

func TestIsOnlyRadarFieldManagerConflict(t *testing.T) {
	statusConflict := func(messages ...string) error {
		causes := make([]metav1.StatusCause, 0, len(messages))
		for _, message := range messages {
			causes = append(causes, metav1.StatusCause{
				Type:    metav1.CauseTypeFieldManagerConflict,
				Message: message,
			})
		}
		return &apierrors.StatusError{ErrStatus: metav1.Status{
			Reason:  metav1.StatusReasonConflict,
			Details: &metav1.StatusDetails{Causes: causes},
		}}
	}

	if !isOnlyRadarFieldManagerConflict(statusConflict(`conflict with "radar" using apps/v1`)) {
		t.Fatal("Radar-only conflict was not recognized")
	}
	if isOnlyRadarFieldManagerConflict(statusConflict(`conflict with "helm" using apps/v1`)) {
		t.Fatal("external-manager conflict was recognized as Radar-only")
	}
	if isOnlyRadarFieldManagerConflict(statusConflict(
		`conflict with "radar" using apps/v1`,
		`conflict with "argocd" using apps/v1`,
	)) {
		t.Fatal("mixed-manager conflict was recognized as Radar-only")
	}
	if isOnlyRadarFieldManagerConflict(apierrors.NewConflict(
		schema.GroupResource{Group: "apps", Resource: "deployments"},
		"w",
		fmt.Errorf("resource version changed"),
	)) {
		t.Fatal("generic conflict was recognized as Radar field ownership")
	}
}

func TestApplyResource_DryRunWarningsUseAdmittedObject(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
	pre := mkWorkload(t, "Deployment", map[string]any{
		"nodeSelector": map[string]any{"disk": "ssd"},
		"containers":   []any{map[string]any{"name": "app", "image": "example:v1"}},
	})
	dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		map[schema.GroupVersionResource]string{gvr: "DeploymentList"},
		pre,
	)
	var captured clienttesting.PatchActionImpl
	dyn.PrependReactor("patch", "deployments", func(action clienttesting.Action) (bool, runtime.Object, error) {
		captured = action.(clienttesting.PatchActionImpl)
		return true, pre.DeepCopy(), nil
	})
	discovery := &ResourceDiscovery{
		resources: []APIResource{{
			Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true,
		}},
		resourceMap: map[string]APIResource{},
		gvrMap:      map[string]schema.GroupVersionResource{},
	}
	mgr := NewWorkloadManager(dyn, discovery)

	result, err := mgr.ApplyResource(context.Background(), ApplyResourceOptions{
		DryRun: true,
		YAML: `apiVersion: apps/v1
kind: Deployment
metadata:
  name: w
  namespace: ns
spec:
  template:
    spec:
      containers:
      - name: app
        image: example:v2
`,
	})
	if err != nil {
		t.Fatalf("ApplyResource failed: %v", err)
	}
	if len(captured.PatchOptions.DryRun) != 1 || captured.PatchOptions.DryRun[0] != metav1.DryRunAll {
		t.Fatalf("DryRun = %v, want [%q]", captured.PatchOptions.DryRun, metav1.DryRunAll)
	}
	if captured.PatchOptions.FieldValidation != metav1.FieldValidationStrict {
		t.Fatalf("FieldValidation = %q, want %q", captured.PatchOptions.FieldValidation, metav1.FieldValidationStrict)
	}
	if len(result.Warnings) != 1 || !strings.Contains(result.Warnings[0], "nodeSelector") {
		t.Fatalf("Warnings = %v, want retained nodeSelector warning", result.Warnings)
	}
}