diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 6e09fceac..2ca5f1399 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -575,7 +575,32 @@ func (c *Client) update(originals, targets ResourceList, createApplyFunc CreateA original := originals.Get(target) if original == nil { kind := target.Mapping.GroupVersionKind.Kind - return fmt.Errorf("original object %s with the name %q not found", kind, target.Name) + + slog.Warn("resource exists on cluster but not in original release, using cluster state as baseline", + "namespace", target.Namespace, "name", target.Name, "kind", kind) + + currentObj, err := helper.Get(target.Namespace, target.Name) + if err != nil { + return fmt.Errorf("failed to get current resource state: %w", err) + } + + // Create a temporary Info with the current cluster state to use as "original" + currentInfo := &resource.Info{ + Client: target.Client, + Mapping: target.Mapping, + Namespace: target.Namespace, + Name: target.Name, + Object: currentObj, + } + + if err := updateApplyFunc(currentInfo, target); err != nil { + updateErrors = append(updateErrors, err) + } + + // Because we check for errors later, append the info regardless + res.Updated = append(res.Updated, target) + + return nil } if err := updateApplyFunc(original, target); err != nil { @@ -631,7 +656,9 @@ func (c *Client) update(originals, targets ResourceList, createApplyFunc CreateA slog.Any("error", err), ) if !apierrors.IsNotFound(err) { - updateErrors = append(updateErrors, fmt.Errorf("failed to delete resource %s: %w", info.Name, err)) + updateErrors = append(updateErrors, fmt.Errorf( + "failed to delete resource namespace=%s, name=%s, kind=%s: %w", + info.Namespace, info.Name, info.Mapping.GroupVersionKind.Kind, err)) } continue } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index f3a797246..f5cbd95ec 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -411,7 +411,25 @@ func TestUpdate(t *testing.T) { "/namespaces/default/pods/forbidden:GET", "/namespaces/default/pods/forbidden:DELETE", ), - ExpectedError: "failed to delete resource forbidden:", + ExpectedError: "failed to delete resource namespace=default, name=forbidden, kind=Pod:", + }, + "rollback after failed upgrade with removed resource": { + // Simulates rollback scenario: + // - Revision 1 had "newpod" + // - Revision 2 removed "newpod" but upgrade failed (OriginalPods is empty) + // - Cluster still has "newpod" from Revision 1 + // - Rolling back to Revision 1 (TargetPods with "newpod") should succeed + OriginalPods: v1.PodList{}, // Revision 2 (failed) - resource was removed + TargetPods: newPodList("newpod"), // Revision 1 - rolling back to this + ThreeWayMergeForUnstructured: false, + ServerSideApply: true, + ExpectedActions: []string{ + "/namespaces/default/pods/newpod:GET", // Check if resource exists + "/namespaces/default/pods/newpod:GET", // Get current state (first call in update path) + "/namespaces/default/pods/newpod:GET", // Get current cluster state to use as baseline + "/namespaces/default/pods/newpod:PATCH", // Update using cluster state as baseline + }, + ExpectedError: "", }, } @@ -428,6 +446,10 @@ func TestUpdate(t *testing.T) { p, m := req.URL.Path, req.Method switch { + case p == "/namespaces/default/pods/newpod" && m == http.MethodGet: + return newResponse(http.StatusOK, &listTarget.Items[0]) + case p == "/namespaces/default/pods/newpod" && m == http.MethodPatch: + return newResponse(http.StatusOK, &listTarget.Items[0]) case p == "/namespaces/default/pods/starfish" && m == http.MethodGet: return newResponse(http.StatusOK, &listOriginal.Items[0]) case p == "/namespaces/default/pods/otter" && m == http.MethodGet: @@ -519,9 +541,23 @@ func TestUpdate(t *testing.T) { require.NoError(t, err) } - assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) - assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated)) - assert.Len(t, result.Deleted, 1, "expected 1 resource deleted, got %d", len(result.Deleted)) + // Special handling for the rollback test case + if name == "rollback after failed upgrade with removed resource" { + assert.Len(t, result.Created, 0, "expected 0 resource created, got %d", len(result.Created)) + assert.Len(t, result.Updated, 1, "expected 1 resource updated, got %d", len(result.Updated)) + assert.Len(t, result.Deleted, 0, "expected 0 resource deleted, got %d", len(result.Deleted)) + } else { + assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) + assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated)) + assert.Len(t, result.Deleted, 1, "expected 1 resource deleted, got %d", len(result.Deleted)) + } + + if tc.ExpectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.ExpectedError) + } else { + require.NoError(t, err) + } actions := []string{} for _, action := range client.Actions {