Repository: kubernetes-sigs/kro

Issue:
Regression: RGD instance is marked as ACTIVE before the managed resource reaches a ready state
### Description

**Observed Behavior**:
The RGD instance is set to `ACTIVE` before the underlying resource meets its `readyWhen` condition.

**Expected Behavior**:
The RGD instance should stay in the `IN_PROGRESS` state until the underlying resource satisfies its `readyWhen` condition.

**Reproduction Steps**:

RGD:
```yaml
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: test-jobs
spec:
  schema:
    apiVersion: v1
    kind: TestJobs
  resources:
    - id: job1
      readyWhen:
        - ${job1.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-1
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: ["sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #1'"]
              restartPolicy: Never
```

RGD instance:
```yaml
apiVersion: kro.run/v1
kind: TestJobs
metadata:
  name: test-jobs
  namespace: kro-tests
```

**Versions**:
- kro version: 0.5.0
- Kubernetes Version (`kubectl version`): 
```
Client Version: v1.34.1
Kustomize Version: v5.7.1
Server Version: v1.34.0
```

**Logs**:
```
2025-10-22T16:45:30.580Z    DEBUG    dynamic-controller    Enqueueing object    {"objectIdentifiers": {"NamespacedKey":"kro-tests/test-jobs","GVR":{"Group":"kro.run","Version":"v1","Resource":"testjobs"}}, "eventType": "add"}
2025-10-22T16:45:30.580Z    DEBUG    dynamic-controller    Syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs"}
2025-10-22T16:45:30.581Z    DEBUG    testjobs-controller    Setting managed state    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "name": "test-jobs", "namespace": "kro-tests"}
2025-10-22T16:45:30.583Z    LEVEL(-2)    dynamic-controller    Skipping update due to unchanged generation    {"name": "test-jobs", "namespace": "kro-tests", "generation": 1}
2025-10-22T16:45:30.584Z    LEVEL(-2)    testjobs-controller    adding object to applyset    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:/job-1]", "cluster-revision": ""}
2025-10-22T16:45:30.588Z    LEVEL(-2)    testjobs-controller    applied object    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:/job-1]", "applied-revision": "2651", "error": null}
2025-10-22T16:45:30.588Z    LEVEL(-2)    testjobs-controller    listing objects for pruning    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "namespace": "kro-tests", "resource": "batch/v1, Resource=jobs"}
2025-10-22T16:45:30.589Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "resource job1 is not resolved", "error": null}
2025-10-22T16:45:30.593Z    LEVEL(-2)    dynamic-controller    Skipping update due to unchanged generation    {"name": "test-jobs", "namespace": "kro-tests", "generation": 1}
2025-10-22T16:45:30.593Z    DEBUG    dynamic-controller    Finished syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs", "duration": "13.516625ms"}
2025-10-22T16:45:30.593Z    DEBUG    dynamic-controller    Requeue needed after delay    {"item": {"NamespacedKey":"kro-tests/test-jobs","GVR":{"Group":"kro.run","Version":"v1","Resource":"testjobs"}}, "error": "changes applied to cluster", "delay": "3s"}
2025-10-22T16:45:33.596Z    DEBUG    dynamic-controller    Syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs"}
2025-10-22T16:45:33.603Z    LEVEL(-2)    testjobs-controller    adding object to applyset    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:/job-1]", "cluster-revision": "2654"}
2025-10-22T16:45:33.603Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "", "error": "failed evaluating expressison job1.status.completionTime != null: failed evaluating expression job1.status.completionTime != null: no such key: completionTime"}
2025-10-22T16:45:33.607Z    LEVEL(-2)    testjobs-controller    applied object    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:/job-1]", "applied-revision": "2654", "error": null}
2025-10-22T16:45:33.607Z    LEVEL(-2)    testjobs-controller    listing objects for pruning    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "namespace": "kro-tests", "resource": "batch/v1, Resource=jobs"}
2025-10-22T16:45:33.608Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "", "error": "failed evaluating expressison job1.status.completionTime != null: failed evaluating expression job1.status.completionTime != null: no such key: completionTime"}
2025-10-22T16:45:33.613Z    DEBUG    dynamic-controller    Finished syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs", "duration": "16.459458ms"}
2025-10-22T16:45:33.613Z    LEVEL(-2)    dynamic-controller    Skipping update due to unchanged generation    {"name": "test-jobs", "namespace": "kro-tests", "generation": 1}

```

* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request
* Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
* If you are interested in working on this issue or have submitted a pull request, please leave a comment


### Which option describes the most your issue?

_No response_

Hints from issue comments:
@devdanylo: Related issue: https://github.com/kubernetes-sigs/kro/issues/744@a-hilaly: @devdanylo was this working fine in 0.4.1? are we looking at a regression?@devdanylo: > [@devdanylo](https://github.com/devdanylo) was this working fine in 0.4.1? are we looking at a regression?

Correct, this is a regression.@a-hilaly: @devdanylo we released v0.5.1, this should be fixed now.

Solution patch:
diff --git a/pkg/applyset/applyset.go b/pkg/applyset/applyset.go
index 602bdd9..e1c95f1 100644
--- a/pkg/applyset/applyset.go
+++ b/pkg/applyset/applyset.go
@@ -26,13 +26,13 @@ import (
 	"crypto/sha256"
 	"encoding/base64"
 	"fmt"
-	"reflect"
 	"sort"
 	"strings"
 	"sync"
 
 	"github.com/go-logr/logr"
 	"golang.org/x/sync/errgroup"
+	"k8s.io/apimachinery/pkg/api/equality"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -374,13 +374,21 @@ func (a *applySet) Add(ctx context.Context, obj ApplyableObject) (*unstructured.
 }
 
 // ID is the label value that we are using to identify this applyset.
-// Format: base64(sha256(<name>.<namespace>.<kind>.<apiVersion>)), using the URL safe encoding of RFC4648.
 func (a *applySet) ID() string {
+	return ID(a.parent)
+}
+
+// ID computes an apply set identifier for a given parent object.
+// Format: base64(sha256(<name>.<namespace>.<kind>.<apiVersion>)), using the URL safe encoding of RFC4648.
+func ID(parent interface {
+	metav1.Object
+	schema.ObjectKind
+}) string {
 	unencoded := strings.Join([]string{
-		a.parent.GetName(),
-		a.parent.GetNamespace(),
-		a.parent.GroupVersionKind().Kind,
-		a.parent.GroupVersionKind().Group,
+		parent.GetName(),
+		parent.GetNamespace(),
+		parent.GroupVersionKind().Kind,
+		parent.GroupVersionKind().Group,
 	}, ApplySetIDPartDelimiter)
 	hashed := sha256.Sum256([]byte(unencoded))
 	b64 := base64.RawURLEncoding.EncodeToString(hashed[:])
@@ -425,55 +433,29 @@ func (a *applySet) updateParentLabelsAndAnnotations(
 
 	// Generate and append the desired labels to the parent labels
 	desiredLabels := a.desiredParentLabels()
-	labels := a.parent.GetLabels()
-	if labels == nil {
-		labels = make(map[string]string)
-	}
-	for k, v := range desiredLabels {
-		labels[k] = v
-	}
-
 	// Get the desired annotations and append them to the parent
 	desiredAnnotations, returnNamespaces, returnGKs := a.desiredParentAnnotations(mode == updateToSuperset)
-	annotations := a.parent.GetAnnotations()
-	if annotations == nil {
-		annotations = make(map[string]string)
-	}
-	for k, v := range desiredAnnotations {
-		annotations[k] = v
-	}
 
 	options := metav1.ApplyOptions{
 		FieldManager: a.fieldManager + "-parent-labeller",
 		Force:        false,
 	}
 
-	// Convert labels to map[string]interface{} for the unstructured object
-	labelsMap := make(map[string]interface{})
-	for k, v := range labels {
-		labelsMap[k] = v
-	}
-
-	// Convert annotations to map[string]interface{} for the unstructured object
-	annotationsMap := make(map[string]interface{})
-	for k, v := range annotations {
-		annotationsMap[k] = v
-	}
-
 	parentPatch := &unstructured.Unstructured{}
 	parentPatch.SetUnstructuredContent(map[string]interface{}{
 		"apiVersion": a.parent.APIVersion,
 		"kind":       a.parent.Kind,
 		"metadata": map[string]interface{}{
-			"name":        a.parent.GetName(),
-			"namespace":   a.parent.GetNamespace(),
-			"labels":      labelsMap,
-			"annotations": annotationsMap,
+			"name":      a.parent.GetName(),
+			"namespace": a.parent.GetNamespace(),
 		},
 	})
+	parentPatch.SetAnnotations(desiredAnnotations)
+	parentPatch.SetLabels(desiredLabels)
 	// update parent in the cluster.
-	if !reflect.DeepEqual(original.GetLabels(), parentPatch.GetLabels()) ||
-		!reflect.DeepEqual(original.GetAnnotations(), parentPatch.GetAnnotations()) {
+
+	if !equality.Semantic.DeepEqual(original.GetLabels(), parentPatch.GetLabels()) ||
+		!equality.Semantic.DeepEqual(original.GetAnnotations(), parentPatch.GetAnnotations()) {
 		if _, err := a.parentClient.Apply(ctx, a.parent.GetName(), parentPatch, options); err != nil {
 			return nil, nil, fmt.Errorf("error updating parent %w", err)
 		}
@@ -577,7 +559,11 @@ func (a *applySet) apply(ctx context.Context, dryRun bool) (*ApplyResult, error)
 			mu.Lock()
 			defer mu.Unlock()
 			results.recordApplied(obj, lastApplied, err)
-			a.log.V(2).Info("applied object", "object", obj.String(), "applied-revision", lastApplied.GetResourceVersion(),
+			var appliedRevision string
+			if lastApplied != nil {
+				appliedRevision = lastApplied.GetResourceVersion()
+			}
+			a.log.V(2).Info("applied object", "object", obj.String(), "applied-revision", appliedRevision,
 				"error", err)
 			return nil
 		})
diff --git a/pkg/controller/instance/instance_state.go b/pkg/controller/instance/instance_state.go
index 5c2e58a..bd4b4d6 100644
--- a/pkg/controller/instance/instance_state.go
+++ b/pkg/controller/instance/instance_state.go
@@ -52,9 +52,9 @@ type InstanceState struct {
 }
 
 func (s *InstanceState) ResourceErrors() error {
-	errorsSeen := []error{}
+	var errorsSeen []error
 	for _, resourceState := range s.ResourceStates {
-		if resourceState.State == ResourceStateError {
+		if resourceState.Err != nil {
 			errorsSeen = append(errorsSeen, resourceState.Err)
 		}
 	}
diff --git a/pkg/controller/instance/metrics.go b/pkg/controller/instance/metrics.go
deleted file mode 100644
index a62569b..0000000
--- a/pkg/controller/instance/metrics.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2025 The Kubernetes Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package instance
-
-import (
-	"github.com/prometheus/client_golang/prometheus"
-	"sigs.k8s.io/controller-runtime/pkg/metrics"
-)
-
-const (
-	// MetricImpersonationTotal is the total number of impersonation requests
-	// made by the controller
-	MetricImpersonationTotal = "controller_impersonation_total"
-	// MetricImpersonationErrors is the total number of errors encountered
-	// while making impersonation requests
-	MetricImpersonationErrors = "controller_impersonation_errors_total"
-	// MetricImpersonationDuration tracks the duration of impersonation operations
-	MetricImpersonationDuration = "controller_impersonation_duration_seconds"
-)
-
-var (
-	impersonationTotal = prometheus.NewCounterVec(
-		prometheus.CounterOpts{
-			Name: MetricImpersonationTotal,
-			Help: "Total number of service account impersonation attempts by namespace and result",
-		},
-		[]string{"namespace", "service_account", "result"},
-	)
-
-	impersonationErrors = prometheus.NewCounterVec(
-		prometheus.CounterOpts{
-			Name: MetricImpersonationErrors,
-			Help: "Total number of service account impersonation errors by category",
-		},
-		[]string{"namespace", "service_account", "error_type"},
-	)
-
-	impersonationDuration = prometheus.NewHistogramVec(
-		prometheus.HistogramOpts{
-			Name:    MetricImpersonationDuration,
-			Help:    "Duration of service account impersonation operations",
-			Buckets: []float64{0.01, 0.1, 0.5, 1, 2, 5},
-		},
-		[]string{"namespace", "service_account"},
-	)
-)
-
-func init() {
-	metrics.Registry.MustRegister(
-		impersonationTotal,
-		impersonationErrors,
-		impersonationDuration,
-	)
-}
diff --git a/website/docs/docs/getting-started/01-Installation.md b/website/docs/docs/getting-started/01-Installation.md
index 89a8651..c86ae21 100644
--- a/website/docs/docs/getting-started/01-Installation.md
+++ b/website/docs/docs/getting-started/01-Installation.md
@@ -46,13 +46,6 @@ helm install kro oci://registry.k8s.io/kro/charts/kro \
   --version=${KRO_VERSION}
 ```
 
-:::info[**Troubleshooting Helm Install**]
-Note that authentication is not required for pulling charts from public GHCR (GitHub Container Registry) repositories.
-
-Helm install download failures occur due to expired local credentials. To resolve this issue, clear your local credentials cache by running `helm registry logout ghcr.io` in your terminal, then retry the installation.
-
-:::
-
 ## Verifying the Installation
 
 After running the installation command, verify that Kro has been installed



Test patch:
diff --git a/pkg/applyset/applyset_test.go b/pkg/applyset/applyset_test.go
index 96f4f45..1c92cdb 100644
--- a/pkg/applyset/applyset_test.go
+++ b/pkg/applyset/applyset_test.go
@@ -17,6 +17,7 @@ package applyset
 import (
 	"context"
 	"encoding/json"
+	"strings"
 	"testing"
 
 	"github.com/go-logr/logr"
@@ -200,6 +201,8 @@ func TestApplySet_Apply(t *testing.T) {
 	_, err := aset.Add(context.Background(), configMap("test-cm", "default"))
 	assert.NoError(t, err)
 
+	expected := []string{"test-cm"}
+
 	dynamicClient.PrependReactor("patch", "configmaps", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
 		var appliedCM unstructured.Unstructured
 		patchAction := action.(k8stesting.PatchAction)
@@ -207,7 +210,7 @@ func TestApplySet_Apply(t *testing.T) {
 		err = json.Unmarshal(patchAction.GetPatch(), &appliedCM)
 		assert.NoError(t, err)
 		assert.NotNil(t, appliedCM)
-		assert.Equal(t, "test-cm", appliedCM.GetName())
+		assert.Contains(t, expected, appliedCM.GetName())
 		assert.Contains(t, appliedCM.GetLabels(), ApplysetPartOfLabel)
 		// The fake client needs to return the object that was applied.
 		return true, &appliedCM, nil
@@ -236,6 +239,70 @@ func TestApplySet_Apply(t *testing.T) {
 	assert.NoError(t, result.Errors())
 	assert.Equal(t, 1, result.DesiredCount)
 	assert.Len(t, result.AppliedObjects, 1)
+
+	t.Run("reapply with new object", func(t *testing.T) {
+		_, err := aset.Add(context.Background(), configMap("test-cm-2", "default"))
+		assert.NoError(t, err)
+		expected = append(expected, "test-cm-2")
+
+		result, err := aset.Apply(context.Background(), false)
+		assert.NoError(t, err)
+		assert.NoError(t, result.Errors())
+		assert.Equal(t, 2, result.DesiredCount)
+		assert.Len(t, result.AppliedObjects, 2)
+	})
+
+}
+
+func TestApplySet_UpdatesParentAnnotationsWithMultipleGroupKinds(t *testing.T) {
+	parent := parentObj(secretGVK, "parent-secret")
+
+	aset, dynamicClient := newTestApplySet(t, parent)
+
+	// Add two different GKs
+	_, err := aset.Add(context.Background(), configMap("cm-1", "default"))
+	assert.NoError(t, err)
+	_, err = aset.Add(context.Background(), foo("foo-1", "default"))
+	assert.NoError(t, err)
+
+	// Reactor for both GKs
+	dynamicClient.PrependReactor("patch", "configmaps", func(action k8stesting.Action) (bool, runtime.Object, error) {
+		var obj unstructured.Unstructured
+		_ = json.Unmarshal(action.(k8stesting.PatchAction).GetPatch(), &obj)
+		return true, &obj, nil
+	})
+	dynamicClient.PrependReactor("patch", "foos", func(action k8stesting.Action) (bool, runtime.Object, error) {
+		var obj unstructured.Unstructured
+		_ = json.Unmarshal(action.(k8stesting.PatchAction).GetPatch(), &obj)
+		return true, &obj, nil
+	})
+
+	// Capture and validate the parent update
+	dynamicClient.PrependReactor("patch", "secrets", func(action k8stesting.Action) (bool, runtime.Object, error) {
+		patchAction := action.(k8stesting.PatchAction)
+		assert.Equal(t, types.ApplyPatchType, patchAction.GetPatchType())
+
+		var parentPatch unstructured.Unstructured
+		err := json.Unmarshal(patchAction.GetPatch(), &parentPatch)
+		assert.NoError(t, err)
+
+		ann := parentPatch.GetAnnotations()
+		assert.NotNil(t, ann)
+		assert.Contains(t, ann, ApplySetGKsAnnotation)
+
+		// Expect both GKs (order not guaranteed)
+		gks := ann[ApplySetGKsAnnotation]
+		assert.True(t, strings.Contains(gks, "ConfigMap"))
+		assert.True(t, strings.Contains(gks, "Foo"))
+
+		return true, &parentPatch, nil
+	})
+
+	result, err := aset.Apply(context.Background(), false)
+	assert.NoError(t, err)
+	assert.NoError(t, result.Errors())
+	assert.Equal(t, 2, result.DesiredCount)
+	assert.Len(t, result.AppliedObjects, 2)
 }
 
 func TestApplySet_Prune(t *testing.T) {
@@ -516,7 +583,7 @@ func TestApplySet_PruneOldNamespace(t *testing.T) {
 		assert.Contains(t, parentPatch.GetLabels(), ApplySetParentIDLabel)
 		assert.Contains(t, parentPatch.GetAnnotations(), ApplySetAdditionalNamespacesAnnotation)
 		assert.Contains(t, parentPatch.GetAnnotations(), ApplySetGKsAnnotation)
-		//assert.Equal(t, parentPatch.GetAnnotations()[ApplySetAdditionalNamespacesAnnotation], "ns1,ns2,ns3,oldns1")
+		// assert.Equal(t, parentPatch.GetAnnotations()[ApplySetAdditionalNamespacesAnnotation], "ns1,ns2,ns3,oldns1")
 		assert.Equal(t, parentPatch.GetAnnotations()[ApplySetGKsAnnotation], "ConfigMap,Foo.foo.bar")
 
 		return true, &parentPatch, nil
@@ -587,7 +654,7 @@ func TestApplySet_PruneOldGVKs(t *testing.T) {
 		assert.Contains(t, parentPatch.GetAnnotations(), ApplySetAdditionalNamespacesAnnotation)
 		assert.Contains(t, parentPatch.GetAnnotations(), ApplySetGKsAnnotation)
 		assert.Equal(t, parentPatch.GetAnnotations()[ApplySetAdditionalNamespacesAnnotation], "ns1,ns2")
-		//assert.Equal(t, parentPatch.GetAnnotations()[ApplySetGKsAnnotation], "ConfigMap,Foo.foo.bar")
+		// assert.Equal(t, parentPatch.GetAnnotations()[ApplySetGKsAnnotation], "ConfigMap,Foo.foo.bar")
 
 		return true, &parentPatch, nil
 	})
diff --git a/test/integration/suites/core/annotation_label_test.go b/test/integration/suites/core/annotation_label_test.go
new file mode 100644
index 0000000..e56cea9
--- /dev/null
+++ b/test/integration/suites/core/annotation_label_test.go
@@ -0,0 +1,161 @@
+// Copyright 2025 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package core_test
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/kubernetes-sigs/kro/pkg/applyset"
+	"github.com/kubernetes-sigs/kro/pkg/metadata"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	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"
+	"k8s.io/apimachinery/pkg/util/rand"
+
+	krov1alpha1 "github.com/kubernetes-sigs/kro/api/v1alpha1"
+	"github.com/kubernetes-sigs/kro/pkg/testutil/generator"
+)
+
+var _ = Describe("Labels and Annotations", func() {
+	var (
+		namespace string
+	)
+
+	BeforeEach(func(ctx SpecContext) {
+		namespace = fmt.Sprintf("test-%s", rand.String(5))
+		// Create namespace
+		Expect(env.Client.Create(ctx, &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: namespace,
+			},
+		})).To(Succeed())
+	})
+
+	AfterEach(func(ctx SpecContext) {
+		Expect(env.Client.Delete(ctx, &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: namespace,
+			},
+		})).To(Succeed())
+	})
+
+	It("should have correct conditions when ResourceGraphDefinition is created", func(ctx SpecContext) {
+		rgd := generator.NewResourceGraphDefinition("test-apply",
+			generator.WithSchema(
+				"TestApply", "v1alpha1",
+				map[string]interface{}{
+					"field1": "string",
+				},
+				nil,
+			),
+			generator.WithResource("res1", map[string]interface{}{
+				"apiVersion": "v1",
+				"kind":       "ConfigMap",
+				"metadata": map[string]interface{}{
+					"name": "${schema.spec.field1}",
+				},
+			}, nil, nil),
+		)
+
+		Expect(env.Client.Create(ctx, rgd)).To(Succeed())
+		DeferCleanup(func(ctx SpecContext) {
+			Expect(env.Client.Delete(ctx, rgd)).To(Succeed())
+		})
+
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name: rgd.Name,
+			}, rgd)
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(rgd.Status.State).To(Equal(krov1alpha1.ResourceGraphDefinitionStateActive))
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		apiVersion, kind := schema.GroupVersionKind{
+			Group:   rgd.Spec.Schema.Group,
+			Version: rgd.Spec.Schema.APIVersion,
+			Kind:    rgd.Spec.Schema.Kind,
+		}.ToAPIVersionAndKind()
+
+		// Create instance
+		instance := &unstructured.Unstructured{
+			Object: map[string]interface{}{
+				"apiVersion": apiVersion,
+				"kind":       kind,
+				"metadata": map[string]interface{}{
+					"name":      "instance",
+					"namespace": namespace,
+				},
+				"spec": map[string]interface{}{
+					"field1": "foobar",
+				},
+			},
+		}
+		Expect(env.Client.Create(ctx, instance)).To(Succeed())
+		DeferCleanup(func(ctx SpecContext) {
+			Expect(env.Client.Delete(ctx, instance)).To(Succeed())
+		})
+
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instance.GetName(),
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			g.Expect(instance.GetAnnotations()).To(SatisfyAll(
+				Not(BeNil()),
+				HaveKeyWithValue(applyset.ApplySetGKsAnnotation, "ConfigMap"),
+				HaveKeyWithValue(applyset.ApplySetToolingAnnotation, "kro/devel"),
+				HaveKeyWithValue(applyset.ApplySetAdditionalNamespacesAnnotation, namespace),
+			), "instance should have group kind for owned resource")
+
+			g.Expect(instance.GetLabels()).To(SatisfyAll(
+				Not(BeNil()),
+				HaveKeyWithValue(applyset.ApplySetParentIDLabel, applyset.ID(instance)),
+			), "instance should be assigned as apply set parent")
+
+			g.Expect(instance.GetLabels()).To(SatisfyAll(
+				HaveKeyWithValue(metadata.KROVersionLabel, "devel"),
+				HaveKeyWithValue(metadata.OwnedLabel, "true"),
+				HaveKeyWithValue(metadata.ResourceGraphDefinitionIDLabel, string(rgd.GetUID())),
+				HaveKeyWithValue(metadata.ResourceGraphDefinitionNameLabel, rgd.GetName()),
+			), "default kro labels should also be present")
+
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		cfgMap := &corev1.ConfigMap{}
+		err := env.Client.Get(ctx, types.NamespacedName{
+			Name:      "foobar",
+			Namespace: namespace,
+		}, cfgMap)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(cfgMap.GetLabels()).To(SatisfyAll(
+			HaveKeyWithValue(applyset.ApplysetPartOfLabel, applyset.ID(instance)),
+			HaveKeyWithValue(metadata.InstanceNamespaceLabel, instance.GetNamespace()),
+			HaveKeyWithValue(metadata.InstanceLabel, instance.GetName()),
+			HaveKeyWithValue(metadata.InstanceIDLabel, string(instance.GetUID())),
+			HaveKeyWithValue(metadata.KROVersionLabel, "devel"),
+			HaveKeyWithValue(metadata.OwnedLabel, "true"),
+			HaveKeyWithValue(metadata.ResourceGraphDefinitionIDLabel, string(rgd.GetUID())),
+			HaveKeyWithValue(metadata.ResourceGraphDefinitionNameLabel, rgd.GetName()),
+		), "config map should be created as part of apply set managed by instance created through rgd")
+	})
+
+})
diff --git a/test/integration/suites/core/readiness_test.go b/test/integration/suites/core/readiness_test.go
index ea14143..51ff2fc 100644
--- a/test/integration/suites/core/readiness_test.go
+++ b/test/integration/suites/core/readiness_test.go
@@ -21,6 +21,7 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	appsv1 "k8s.io/api/apps/v1"
+	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -234,12 +235,24 @@ var _ = Describe("Readiness", func() {
 		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
 
 		// Verify Service is not created yet
+		err := env.Client.Get(ctx, types.NamespacedName{
+			Name:      name,
+			Namespace: namespace,
+		}, &corev1.Service{})
+		Expect(err).To(MatchError(errors.IsNotFound, "service should not be created yet"))
+
+		// Expect instance state to be IN_PROGRESS
 		Eventually(func(g Gomega, ctx SpecContext) {
 			err := env.Client.Get(ctx, types.NamespacedName{
 				Name:      name,
 				Namespace: namespace,
-			}, &corev1.Service{})
-			g.Expect(err).To(MatchError(errors.IsNotFound, "service should not be created yet"))
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			status, found, err := unstructured.NestedString(instance.Object, "status", "state")
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(found).To(BeTrue())
+			g.Expect(status).To(Equal("IN_PROGRESS"))
 		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
 
 		// Patch the deployment to have available replicas in status
@@ -294,4 +307,131 @@ var _ = Describe("Readiness", func() {
 		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
 	})
 
+	It(`should should wait for the last node in the DAG to be ready`, func(ctx SpecContext) {
+		rgd := generator.NewResourceGraphDefinition("test-readiness-tail",
+			generator.WithSchema(
+				"TestReadinessTail", "v1alpha1",
+				map[string]interface{}{
+					"name": "string",
+				},
+				nil,
+			),
+			// Job resource
+			generator.WithResource("job", map[string]interface{}{
+				"apiVersion": "batch/v1",
+				"kind":       "Job",
+				"metadata": map[string]interface{}{
+					"name": "${schema.spec.name}",
+				},
+				"spec": map[string]interface{}{
+					"template": map[string]interface{}{
+						"spec": map[string]interface{}{
+							"containers": []interface{}{
+								map[string]interface{}{
+									"name":  "sleep",
+									"image": "busybox",
+									"args":  []interface{}{"sleep", "3"},
+								},
+							},
+							"restartPolicy": "Never",
+						},
+					},
+				},
+			}, []string{"${job.status.completionTime != null}"}, nil),
+		)
+
+		Expect(env.Client.Create(ctx, rgd)).To(Succeed())
+		// Verify ResourceGraphDefinition is created and becomes ready
+		createdRGD := &krov1alpha1.ResourceGraphDefinition{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name: rgd.Name,
+			}, createdRGD)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			g.Expect(createdRGD.Status.State).To(Equal(krov1alpha1.ResourceGraphDefinitionStateActive))
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		instanceName := "test-readiness-tail"
+
+		// Create instance
+		instance := &unstructured.Unstructured{
+			Object: map[string]interface{}{
+				"apiVersion": fmt.Sprintf("%s/%s", krov1alpha1.KRODomainName, "v1alpha1"),
+				"kind":       "TestReadinessTail",
+				"metadata": map[string]interface{}{
+					"name":      instanceName,
+					"namespace": namespace,
+				},
+				"spec": map[string]interface{}{
+					"name": instanceName,
+				},
+			},
+		}
+		Expect(env.Client.Create(ctx, instance)).To(Succeed())
+
+		// Check if instance is created
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Expect instance state to be IN_PROGRESS
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Verify Job is created now
+		job := &batchv1.Job{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, job)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Verify instance state is IN_PROGRESS consistently for 30seconds
+		Consistently(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+			status, found, err := unstructured.NestedString(instance.Object, "status", "state")
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(found).To(BeTrue())
+			g.Expect(status).To(Equal("IN_PROGRESS"))
+		}, 30*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Patch job completionTime to simulate job completion
+		now := metav1.Now()
+		job.Status.CompletionTime = &now
+		Expect(env.Client.Status().Update(ctx, job)).To(Succeed())
+
+		// Verify instance is now marked as ACTIVE
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			status, found, err := unstructured.NestedString(instance.Object, "status", "state")
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(found).To(BeTrue())
+			g.Expect(status).To(Equal("ACTIVE"))
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Cleanup
+		Expect(env.Client.Delete(ctx, instance)).To(Succeed())
+		Expect(env.Client.Delete(ctx, rgd)).To(Succeed())
+	})
 })