diff --git a/pkg/resourcekeeper/statekeep.go b/pkg/resourcekeeper/statekeep.go index 6a0bdf9f4..b97131d9a 100644 --- a/pkg/resourcekeeper/statekeep.go +++ b/pkg/resourcekeeper/statekeep.go @@ -80,6 +80,9 @@ func (h *resourceKeeper) StateKeep(ctx context.Context) error { if err != nil { return errors.Wrapf(err, "failed to apply once resource %s from resourcetracker %s", mr.ResourceKey(), rt.Name) } + if manifest == nil { + return nil + } ao := []apply.ApplyOption{apply.MustBeControlledByApp(h.app)} if h.isShared(manifest) { ao = append([]apply.ApplyOption{apply.SharedByApp(h.app)}, ao...) @@ -117,6 +120,9 @@ func ApplyStrategies(ctx context.Context, h *resourceKeeper, manifest *unstructu err := h.Get(ctx, types.NamespacedName{Name: manifest.GetName(), Namespace: manifest.GetNamespace()}, un) if err != nil { if kerrors.IsNotFound(err) { + if matchedAffectStage == v1alpha1.ApplyOnceStrategyOnAppStateKeep { + return nil, nil + } return manifest, nil } return nil, err diff --git a/pkg/resourcekeeper/statekeep_suite_test.go b/pkg/resourcekeeper/statekeep_suite_test.go index 4c310e23e..59f163461 100644 --- a/pkg/resourcekeeper/statekeep_suite_test.go +++ b/pkg/resourcekeeper/statekeep_suite_test.go @@ -25,6 +25,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + kerrors "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" @@ -170,6 +171,57 @@ var _ = Describe("Test ResourceKeeper StateKeep", func() { Expect(err.Error()).Should(ContainSubstring("failed to re-apply")) }) + It("Test StateKeep apply-once does not re-create externally deleted resource", func() { + cli := testClient + + // Resource tracked in the resource tracker but not present in the cluster, + // simulating a resource that was externally deleted (e.g. Job garbage-collected by TTL) + // With an apply-once policy matching this resource, state-keep should skip it + cm := createConfigMap("cm-apply-once-gc", "value") + cm.SetLabels(map[string]string{ + oam.LabelAppName: "app-apply-once-gc", + oam.LabelAppNamespace: "default", + oam.LabelAppComponent: "my-task", + }) + cmRaw, err := json.Marshal(cm) + Expect(err).Should(Succeed()) + + app := &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "app-apply-once-gc", Namespace: "default"}} + h := &resourceKeeper{ + Client: cli, + app: app, + applicator: apply.NewAPIApplicator(cli), + cache: newResourceCache(cli, app), + applyOncePolicy: &v1alpha1.ApplyOncePolicySpec{ + Enable: true, + Rules: []v1alpha1.ApplyOncePolicyRule{{ + Selector: v1alpha1.ResourcePolicyRuleSelector{ + CompNames: []string{"my-task"}, + }, + Strategy: &v1alpha1.ApplyOnceStrategy{Path: []string{"*"}}, + }}, + }, + } + h._currentRT = &v1beta1.ResourceTracker{ + Spec: v1beta1.ResourceTrackerSpec{ + ManagedResources: []v1beta1.ManagedResource{{ + ClusterObjectReference: createConfigMapClusterObjectReference("cm-apply-once-gc"), + OAMObjectReference: common.NewOAMObjectReferenceFromObject(cm), + Data: &runtime.RawExtension{Raw: cmRaw}, + }}, + }, + } + + Expect(h.StateKeep(context.Background())).Should(Succeed()) + + // Verify the resource was not re-created + got := &unstructured.Unstructured{} + got.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap")) + err = cli.Get(context.Background(), client.ObjectKeyFromObject(cm), got) + Expect(err).Should(HaveOccurred()) + Expect(kerrors.IsNotFound(err)).Should(BeTrue()) + }) + It("Test StateKeep for shared resources", func() { cli := testClient ctx := context.Background()