From e55707b09d3821bccf2eca63b54288f632212081 Mon Sep 17 00:00:00 2001 From: Patrick Seidensal Date: Fri, 21 Mar 2025 11:48:32 +0100 Subject: [PATCH 1/3] Fix --take-ownership If a resource exists in the cluster and is to be adopted by helm install --take-ownership, it is left unchanged while helm reports the installation to have succeeded. This is due to CRs and CRDs being merged without three-way-merge, which results in an empty patch. By using a three-way-merge transparently when --take-ownership is used, the helm behaves as expected without breaking previous behavior. Fixes #30622 Signed-off-by: Patrick Seidensal --- pkg/action/install.go | 6 +- pkg/kube/client.go | 59 ++++++++++---- pkg/kube/client_test.go | 169 +++++++++++++++++++++++++++++++++++++++- pkg/kube/fake/fake.go | 8 ++ pkg/kube/interface.go | 8 ++ 5 files changed, 234 insertions(+), 16 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 735b8ac17..6a5e9589d 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -465,7 +465,11 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource if len(toBeAdopted) == 0 && len(resources) > 0 { _, err = i.cfg.KubeClient.Create(resources) } else if len(resources) > 0 { - _, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force) + if i.TakeOwnership { + _, err = i.cfg.KubeClient.(kube.InterfaceThreeWayMerge).UpdateThreeWayMerge(toBeAdopted, resources, i.Force) + } else { + _, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force) + } } if err != nil { return rel, err diff --git a/pkg/kube/client.go b/pkg/kube/client.go index a62b83b3e..695bda3ef 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -43,6 +43,8 @@ import ( metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/jsonmergepatch" + "k8s.io/apimachinery/pkg/util/mergepatch" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" @@ -399,14 +401,7 @@ func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, erro return result, scrubValidationError(err) } -// Update takes the current list of objects and target list of objects and -// creates resources that don't already exist, updates resources that have been -// modified in the target configuration, and deletes resources from the current -// configuration that are not present in the target configuration. If an error -// occurs, a Result will still be returned with the error, containing all -// resource updates, creations, and deletions that were attempted. These can be -// used for cleanup or other logging purposes. -func (c *Client) Update(original, target ResourceList, force bool) (*Result, error) { +func (c *Client) update(original, target ResourceList, force, threeWayMerge bool) (*Result, error) { updateErrors := []string{} res := &Result{} @@ -441,7 +436,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err return errors.Errorf("no %s with the name %q found", kind, info.Name) } - if err := updateResource(c, info, originalInfo.Object, force); err != nil { + if err := updateResource(c, info, originalInfo.Object, force, threeWayMerge); err != nil { c.Log("error updating the resource %q:\n\t %v", info.Name, err) updateErrors = append(updateErrors, err.Error()) } @@ -482,6 +477,31 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err return res, nil } +// Update takes the current list of objects and target list of objects and +// creates resources that don't already exist, updates resources that have been +// modified in the target configuration, and deletes resources from the current +// configuration that are not present in the target configuration. If an error +// occurs, a Result will still be returned with the error, containing all +// resource updates, creations, and deletions that were attempted. These can be +// used for cleanup or other logging purposes. +// +// The difference to Update is that UpdateThreeWayMerge does a three-way-merge +// for unstructured objects. +func (c *Client) UpdateThreeWayMerge(original, target ResourceList, force bool) (*Result, error) { + return c.update(original, target, force, true) +} + +// Update takes the current list of objects and target list of objects and +// creates resources that don't already exist, updates resources that have been +// modified in the target configuration, and deletes resources from the current +// configuration that are not present in the target configuration. If an error +// occurs, a Result will still be returned with the error, containing all +// resource updates, creations, and deletions that were attempted. These can be +// used for cleanup or other logging purposes. +func (c *Client) Update(original, target ResourceList, force bool) (*Result, error) { + return c.update(original, target, force, false) +} + // Delete deletes Kubernetes resources specified in the resources list with // background cascade deletion. It will attempt to delete all resources even // if one or more fail and collect any errors. All successfully deleted items @@ -591,7 +611,7 @@ func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) erro }) } -func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) { +func createPatch(target *resource.Info, current runtime.Object, threeWayMergeForUnstructured bool) ([]byte, types.PatchType, error) { oldData, err := json.Marshal(current) if err != nil { return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration") @@ -619,7 +639,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P // Unstructured objects, such as CRDs, may not have a not registered error // returned from ConvertToVersion. Anything that's unstructured should - // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported + // use generic JSON merge patch. Strategic Merge Patch is not supported // on objects like CRDs. _, isUnstructured := versionedObject.(runtime.Unstructured) @@ -627,6 +647,19 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P _, isCRD := versionedObject.(*apiextv1beta1.CustomResourceDefinition) if isUnstructured || isCRD { + if threeWayMergeForUnstructured { + // from https://github.com/kubernetes/kubectl/blob/b83b2ec7d15f286720bccf7872b5c72372cb8e80/pkg/cmd/apply/patcher.go#L129 + preconditions := []mergepatch.PreconditionFunc{ + mergepatch.RequireKeyUnchanged("apiVersion"), + mergepatch.RequireKeyUnchanged("kind"), + mergepatch.RequireMetadataKeyUnchanged("name"), + } + patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(oldData, newData, currentData, preconditions...) + if err != nil && mergepatch.IsPreconditionFailed(err) { + err = fmt.Errorf("%w: at least one field was changed: apiVersion, kind or name", err) + } + return patch, types.MergePatchType, err + } // fall back to generic JSON merge patch patch, err := jsonpatch.CreateMergePatch(oldData, newData) return patch, types.MergePatchType, err @@ -641,7 +674,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P return patch, types.StrategicMergePatchType, err } -func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error { +func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force, threeWayMergeForUnstructured bool) error { var ( obj runtime.Object helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) @@ -657,7 +690,7 @@ func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, } c.Log("Replaced %q with kind %s for kind %s", target.Name, currentObj.GetObjectKind().GroupVersionKind().Kind, kind) } else { - patch, patchType, err := createPatch(target, currentObj) + patch, patchType, err := createPatch(target, currentObj, threeWayMergeForUnstructured) if err != nil { return errors.Wrap(err, "failed to create patch") } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 8ae1df238..2238b34c0 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -27,8 +27,13 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" 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" + jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/resource" k8sfake "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" @@ -208,7 +213,7 @@ func TestCreate(t *testing.T) { }) } -func TestUpdate(t *testing.T) { +func testUpdate(t *testing.T, threeWayMerge bool) { listA := newPodList("starfish", "otter", "squid") listB := newPodList("starfish", "otter", "dolphin") listC := newPodList("starfish", "otter", "dolphin") @@ -279,7 +284,12 @@ func TestUpdate(t *testing.T) { t.Fatal(err) } - result, err := c.Update(first, second, false) + var result *Result + if threeWayMerge { + result, err = c.UpdateThreeWayMerge(first, second, false) + } else { + result, err = c.Update(first, second, false) + } if err != nil { t.Fatal(err) } @@ -328,6 +338,14 @@ func TestUpdate(t *testing.T) { } } +func TestUpdate(t *testing.T) { + testUpdate(t, false) +} + +func TestUpdateThreeWayMerge(t *testing.T) { + testUpdate(t, true) +} + func TestBuild(t *testing.T) { tests := []struct { name string @@ -913,3 +931,150 @@ spec: var resourceQuotaConflict = []byte(` {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Operation cannot be fulfilled on resourcequotas \"quota\": the object has been modified; please apply your changes to the latest version and try again","reason":"Conflict","details":{"name":"quota","kind":"resourcequotas"},"code":409}`) + +type createPatchTestCase struct { + name string + + // The target state. + target *unstructured.Unstructured + // The current state as it exists in the release. + current *unstructured.Unstructured + // The actual state as it exists in the cluster. + actual *unstructured.Unstructured + + threeWayMergeForUnstructured bool + // The patch is supposed to transfer the current state to the target state, + // thereby preserving the actual state, wherever possible. + expectedPatch string + expectedPatchType types.PatchType +} + +func (c createPatchTestCase) run(t *testing.T) { + scheme := runtime.NewScheme() + v1.AddToScheme(scheme) + encoder := jsonserializer.NewSerializerWithOptions( + jsonserializer.DefaultMetaFactory, scheme, scheme, jsonserializer.SerializerOptions{ + Yaml: false, Pretty: false, Strict: true, + }, + ) + objBody := func(obj runtime.Object) io.ReadCloser { + return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, obj)))) + } + header := make(http.Header) + header.Set("Content-Type", runtime.ContentTypeJSON) + restClient := &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Resp: &http.Response{ + StatusCode: 200, + Body: objBody(c.actual), + Header: header, + }, + } + + targetInfo := &resource.Info{ + Client: restClient, + Namespace: "default", + Name: "test-obj", + Object: c.target, + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{ + Group: "crd.com", + Version: "v1", + Resource: "datas", + }, + Scope: meta.RESTScopeNamespace, + }, + } + + patch, patchType, err := createPatch(targetInfo, c.current, c.threeWayMergeForUnstructured) + if err != nil { + t.Fatalf("Failed to create patch: %v", err) + } + + if c.expectedPatch != string(patch) { + t.Errorf("Unexpected patch.\nTarget:\n%s\nCurrent:\n%s\nActual:\n%s\n\nExpected:\n%s\nGot:\n%s", + c.target, + c.current, + c.actual, + c.expectedPatch, + string(patch), + ) + } + + if patchType != types.MergePatchType { + t.Errorf("Expected patch type %s, got %s", types.MergePatchType, patchType) + } +} + +func newTestCustomResourceData(metadata map[string]string, spec map[string]interface{}) *unstructured.Unstructured { + if metadata == nil { + metadata = make(map[string]string) + } + if _, ok := metadata["name"]; !ok { + metadata["name"] = "test-obj" + } + if _, ok := metadata["namespace"]; !ok { + metadata["namespace"] = "default" + } + o := map[string]interface{}{ + "apiVersion": "crd.com/v1", + "kind": "Data", + "metadata": metadata, + } + if len(spec) > 0 { + o["spec"] = spec + } + return &unstructured.Unstructured{ + Object: o, + } +} + +func TestCreatePatchCustomResourceMetadata(t *testing.T) { + target := newTestCustomResourceData(map[string]string{ + "meta.helm.sh/release-name": "foo-simple", + "meta.helm.sh/release-namespace": "default", + "objectset.rio.cattle.io/id": "default-foo-simple", + }, nil) + testCase := createPatchTestCase{ + name: "take ownership of resource", + target: target, + current: target, + actual: newTestCustomResourceData(nil, map[string]interface{}{ + "color": "red", + }), + threeWayMergeForUnstructured: true, + expectedPatch: `{"metadata":{"meta.helm.sh/release-name":"foo-simple","meta.helm.sh/release-namespace":"default","objectset.rio.cattle.io/id":"default-foo-simple"}}`, + expectedPatchType: types.MergePatchType, + } + t.Run(testCase.name, testCase.run) + + // Previous behavior. + testCase.threeWayMergeForUnstructured = false + testCase.expectedPatch = `{}` + t.Run(testCase.name, testCase.run) +} + +func TestCreatePatchCustomResourceSpec(t *testing.T) { + target := newTestCustomResourceData(nil, map[string]interface{}{ + "color": "red", + "size": "large", + }) + testCase := createPatchTestCase{ + name: "merge with spec of existing custom resource", + target: target, + current: target, + actual: newTestCustomResourceData(nil, map[string]interface{}{ + "color": "red", + "weight": "heavy", + }), + threeWayMergeForUnstructured: true, + expectedPatch: `{"spec":{"size":"large"}}`, + expectedPatchType: types.MergePatchType, + } + t.Run(testCase.name, testCase.run) + + // Previous behavior. + testCase.threeWayMergeForUnstructured = false + testCase.expectedPatch = `{}` + t.Run(testCase.name, testCase.run) +} diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index f868afa1a..6ca272968 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -123,6 +123,14 @@ func (f *FailingKubeClient) Update(r, modified kube.ResourceList, ignoreMe bool) return f.PrintingKubeClient.Update(r, modified, ignoreMe) } +// Update returns the configured error if set or prints +func (f *FailingKubeClient) UpdateThreeWayMerge(r, modified kube.ResourceList, ignoreMe bool) (*kube.Result, error) { + if f.UpdateError != nil { + return &kube.Result{}, f.UpdateError + } + return f.PrintingKubeClient.Update(r, modified, ignoreMe) +} + // Build returns the configured error if set or prints func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error) { if f.BuildError != nil { diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index f68367dcd..6b945088e 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -53,6 +53,13 @@ type Interface interface { GetWaiter(ws WaitStrategy) (Waiter, error) } +// InterfaceThreeWayMerge was introduced to avoid breaking backwards compatibility for Interface implementers. +// +// TODO Helm 4: Remove InterfaceThreeWayMerge and integrate its method(s) into the Interface. +type InterfaceThreeWayMerge interface { + UpdateThreeWayMerge(original, target ResourceList, force bool) (*Result, error) +} + // Waiter defines methods related to waiting for resource states. type Waiter interface { // Wait waits up to the given timeout for the specified resources to be ready. @@ -118,6 +125,7 @@ type InterfaceResources interface { } var _ Interface = (*Client)(nil) +var _ InterfaceThreeWayMerge = (*Client)(nil) var _ InterfaceLogs = (*Client)(nil) var _ InterfaceDeletionPropagation = (*Client)(nil) var _ InterfaceResources = (*Client)(nil) From 7c37a109f20ab77086c804e61315f43891c7b066 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Tue, 15 Apr 2025 11:24:12 +0100 Subject: [PATCH 2/3] Add install test for TakeOwnership flag Signed-off-by: Evans Mungai --- pkg/action/action_test.go | 7 ++- pkg/action/install_test.go | 120 +++++++++++++++++++++++++++++++++++++ pkg/kube/fake/fake.go | 4 ++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index ec6e261db..fd5825990 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -26,6 +26,7 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/registry" release "helm.sh/helm/v4/pkg/release/v1" @@ -37,6 +38,10 @@ import ( var verbose = flag.Bool("test.log", false, "enable test logging") func actionConfigFixture(t *testing.T) *Configuration { + return actionConfigFixtureWithDummyResources(t, nil) +} + +func actionConfigFixtureWithDummyResources(t *testing.T, dummyResources kube.ResourceList) *Configuration { t.Helper() registryClient, err := registry.NewClient() @@ -46,7 +51,7 @@ func actionConfigFixture(t *testing.T) *Configuration { return &Configuration{ Releases: storage.Init(driver.NewMemory()), - KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}}, + KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: dummyResources}, Capabilities: chartutil.DefaultCapabilities, RegistryClient: registryClient, Log: func(format string, v ...interface{}) { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index aafda86c2..b7d12db3f 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "io" + "net/http" "os" "path/filepath" "regexp" @@ -31,6 +32,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kuberuntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest/fake" "helm.sh/helm/v4/internal/test" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -48,6 +57,62 @@ type nameTemplateTestCase struct { expectedErrorStr string } +func createDummyResourceList(owned bool) kube.ResourceList { + obj := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummyName", + Namespace: "spaced", + }, + } + + if owned { + obj.Labels = map[string]string{ + "app.kubernetes.io/managed-by": "Helm", + } + obj.Annotations = map[string]string{ + "meta.helm.sh/release-name": "test-install-release", + "meta.helm.sh/release-namespace": "spaced", + } + } + + resInfo := resource.Info{ + Name: "dummyName", + Namespace: "spaced", + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, + GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Scope: meta.RESTScopeNamespace, + }, + Object: obj, + } + body := io.NopCloser(bytes.NewReader([]byte(kuberuntime.EncodeOrDie(appsv1Codec, obj)))) + + resInfo.Client = &fake.RESTClient{ + GroupVersion: schema.GroupVersion{Group: "apps", Version: "v1"}, + NegotiatedSerializer: scheme.Codecs.WithoutConversion(), + Client: fake.CreateHTTPClient(func(_ *http.Request) (*http.Response, error) { + header := http.Header{} + header.Set("Content-Type", kuberuntime.ContentTypeJSON) + return &http.Response{ + StatusCode: http.StatusOK, + Header: header, + Body: body, + }, nil + }), + } + var resourceList kube.ResourceList + resourceList.Append(&resInfo) + return resourceList +} + +func installActionWithConfig(config *Configuration) *Install { + instAction := NewInstall(config) + instAction.Namespace = "spaced" + instAction.ReleaseName = "test-install-release" + + return instAction +} + func installAction(t *testing.T) *Install { config := actionConfigFixture(t) instAction := NewInstall(config) @@ -93,6 +158,61 @@ func TestInstallRelease(t *testing.T) { is.Equal(lastRelease.Info.Status, release.StatusDeployed) } +func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) { + // This test will test checking ownership of a resource + // returned by the fake client. If the resource is not + // owned by the chart, ownership is taken. + // To verify ownership has been taken, the fake client + // needs to store state which is a bigger rewrite. + // TODO: Ensure fake kube client stores state. Maybe using + // "k8s.io/client-go/kubernetes/fake" could be sufficient? i.e + // "Client{Namespace: namespace, kubeClient: k8sfake.NewClientset()}" + + is := assert.New(t) + + // Resource list from cluster is NOT owned by helm chart + config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false)) + instAction := installActionWithConfig(config) + instAction.TakeOwnership = true + res, err := instAction.Run(buildChart(), nil) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + is.Equal(rel.Info.Description, "Install complete") +} + +func TestInstallReleaseWithTakeOwnership_ResourceOwned(t *testing.T) { + is := assert.New(t) + + // Resource list from cluster is owned by helm chart + config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true)) + instAction := installActionWithConfig(config) + instAction.TakeOwnership = false + res, err := instAction.Run(buildChart(), nil) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + is.Equal(rel.Info.Description, "Install complete") +} + +func TestInstallReleaseWithTakeOwnership_ResourceOwnedNoFlag(t *testing.T) { + is := assert.New(t) + + // Resource list from cluster is NOT owned by helm chart + config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false)) + instAction := installActionWithConfig(config) + _, err := instAction.Run(buildChart(), nil) + is.Error(err) + is.Contains(err.Error(), "Unable to continue with install") +} + func TestInstallReleaseWithValues(t *testing.T) { is := assert.New(t) instAction := installAction(t) diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index 6ca272968..a543a0f73 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -41,6 +41,7 @@ type FailingKubeClient struct { BuildError error BuildTableError error BuildDummy bool + DummyResources kube.ResourceList BuildUnstructuredError error WaitError error WaitForDeleteError error @@ -136,6 +137,9 @@ func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error if f.BuildError != nil { return []*resource.Info{}, f.BuildError } + if f.DummyResources != nil { + return f.DummyResources, nil + } if f.BuildDummy { return createDummyResourceList(), nil } From 1f5605a4051262b37063ab19ca170dc6b0c820fc Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Thu, 17 Apr 2025 15:33:21 +0100 Subject: [PATCH 3/3] fix formatting errors Signed-off-by: Evans Mungai --- pkg/kube/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 583c24a3e..ff0f05e88 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -433,7 +433,7 @@ func (c *Client) update(original, target ResourceList, force, threeWayMerge bool } if err := updateResource(c, info, originalInfo.Object, force, threeWayMerge); err != nil { - slog.Debug("error updating the resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) + slog.Debug("error updating the resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) updateErrors = append(updateErrors, err.Error()) } // Because we check for errors later, append the info regardless