Repository: GoogleCloudPlatform/k8s-config-connector
Pull request #4036: feat: add types, mappers and fuzzer for BackupDRBackupPlanAssociation
Author: login: jingyih
Labels: name: lgtm
name: approved
Created: 2025-03-15T01:43:21Z | Merged: 2025-03-16T23:25:51Z



Diff:
diff --git a/apis/backupdr/v1alpha1/backupplanassociation_identity.go b/apis/backupdr/v1alpha1/backupplanassociation_identity.go
new file mode 100644
index 00000000000..99a7b137653
--- /dev/null
+++ b/apis/backupdr/v1alpha1/backupplanassociation_identity.go
@@ -0,0 +1,117 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1alpha1
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common"
+	refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// BackupPlanAssociationIdentity defines the resource reference to BackupDRBackupPlanAssociation, which "External" field
+// holds the GCP identifier for the KRM object.
+type BackupPlanAssociationIdentity struct {
+	parent *BackupPlanAssociationParent
+	id     string
+}
+
+func (i *BackupPlanAssociationIdentity) String() string {
+	return i.parent.String() + "/backupPlanAssociations/" + i.id
+}
+
+func (i *BackupPlanAssociationIdentity) ID() string {
+	return i.id
+}
+
+func (i *BackupPlanAssociationIdentity) Parent() *BackupPlanAssociationParent {
+	return i.parent
+}
+
+type BackupPlanAssociationParent struct {
+	ProjectID string
+	Location  string
+}
+
+func (p *BackupPlanAssociationParent) String() string {
+	return "projects/" + p.ProjectID + "/locations/" + p.Location
+}
+
+// New builds a BackupPlanAssociationIdentity from the Config Connector BackupPlanAssociation object.
+func NewBackupPlanAssociationIdentity(ctx context.Context, reader client.Reader, obj *BackupDRBackupPlanAssociation) (*BackupPlanAssociationIdentity, error) {
+
+	// Get Parent
+	projectRef, err := refsv1beta1.ResolveProject(ctx, reader, obj.GetNamespace(), obj.Spec.ProjectRef)
+	if err != nil {
+		return nil, err
+	}
+	projectID := projectRef.ProjectID
+	if projectID == "" {
+		return nil, fmt.Errorf("cannot resolve project")
+	}
+	location := obj.Spec.Location
+
+	// Get desired ID
+	resourceID := common.ValueOf(obj.Spec.ResourceID)
+	if resourceID == "" {
+		resourceID = obj.GetName()
+	}
+	if resourceID == "" {
+		return nil, fmt.Errorf("cannot resolve resource ID")
+	}
+
+	// Use approved External
+	externalRef := common.ValueOf(obj.Status.ExternalRef)
+	if externalRef != "" {
+		// Validate desired with actual
+		actualParent, actualResourceID, err := ParseBackupPlanAssociationExternal(externalRef)
+		if err != nil {
+			return nil, err
+		}
+		if actualParent.ProjectID != projectID {
+			return nil, fmt.Errorf("spec.projectRef changed, expect %s, got %s", actualParent.ProjectID, projectID)
+		}
+		if actualParent.Location != location {
+			return nil, fmt.Errorf("spec.location changed, expect %s, got %s", actualParent.Location, location)
+		}
+		if actualResourceID != resourceID {
+			return nil, fmt.Errorf("cannot reset `metadata.name` or `spec.resourceID` to %s, since it has already assigned to %s",
+				resourceID, actualResourceID)
+		}
+	}
+	return &BackupPlanAssociationIdentity{
+		parent: &BackupPlanAssociationParent{
+			ProjectID: projectID,
+			Location:  location,
+		},
+		id: resourceID,
+	}, nil
+}
+
+func ParseBackupPlanAssociationExternal(external string) (parent *BackupPlanAssociationParent, resourceID string, err error) {
+	tokens := strings.Split(external, "/")
+	if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "backupPlanAssociations" {
+		return nil, "", fmt.Errorf("format of BackupDRBackupPlanAssociation external=%q was not known (use projects/{{projectID}}/locations/{{location}}/backupPlanAssociations/{{backupplanassociationID}})", external)
+	}
+	parent = &BackupPlanAssociationParent{
+		ProjectID: tokens[1],
+		Location:  tokens[3],
+	}
+	resourceID = tokens[5]
+	return parent, resourceID, nil
+}
diff --git a/apis/backupdr/v1alpha1/backupplanassociation_reference.go b/apis/backupdr/v1alpha1/backupplanassociation_reference.go
new file mode 100644
index 00000000000..e8ba4338e5c
--- /dev/null
+++ b/apis/backupdr/v1alpha1/backupplanassociation_reference.go
@@ -0,0 +1,83 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1alpha1
+
+import (
+	"context"
+	"fmt"
+
+	refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var _ refsv1beta1.ExternalNormalizer = &BackupPlanAssociationRef{}
+
+// BackupPlanAssociationRef defines the resource reference to BackupDRBackupPlanAssociation, which "External" field
+// holds the GCP identifier for the KRM object.
+type BackupPlanAssociationRef struct {
+	// A reference to an externally managed BackupDRBackupPlanAssociation resource.
+	// Should be in the format "projects/{{projectID}}/locations/{{location}}/backupPlanAssociations/{{backupplanassociationID}}".
+	External string `json:"external,omitempty"`
+
+	// The name of a BackupDRBackupPlanAssociation resource.
+	Name string `json:"name,omitempty"`
+
+	// The namespace of a BackupDRBackupPlanAssociation resource.
+	Namespace string `json:"namespace,omitempty"`
+}
+
+// NormalizedExternal provision the "External" value for other resource that depends on BackupDRBackupPlanAssociation.
+// If the "External" is given in the other resource's spec.BackupDRBackupPlanAssociationRef, the given value will be used.
+// Otherwise, the "Name" and "Namespace" will be used to query the actual BackupDRBackupPlanAssociation object from the cluster.
+func (r *BackupPlanAssociationRef) NormalizedExternal(ctx context.Context, reader client.Reader, otherNamespace string) (string, error) {
+	if r.External != "" && r.Name != "" {
+		return "", fmt.Errorf("cannot specify both name and external on %s reference", BackupDRBackupPlanAssociationGVK.Kind)
+	}
+	// From given External
+	if r.External != "" {
+		if _, _, err := ParseBackupPlanAssociationExternal(r.External); err != nil {
+			return "", err
+		}
+		return r.External, nil
+	}
+
+	// From the Config Connector object
+	if r.Namespace == "" {
+		r.Namespace = otherNamespace
+	}
+	key := types.NamespacedName{Name: r.Name, Namespace: r.Namespace}
+	u := &unstructured.Unstructured{}
+	u.SetGroupVersionKind(BackupDRBackupPlanAssociationGVK)
+	if err := reader.Get(ctx, key, u); err != nil {
+		if apierrors.IsNotFound(err) {
+			return "", k8s.NewReferenceNotFoundError(u.GroupVersionKind(), key)
+		}
+		return "", fmt.Errorf("reading referenced %s %s: %w", BackupDRBackupPlanAssociationGVK, key, err)
+	}
+	// Get external from status.externalRef. This is the most trustworthy place.
+	actualExternalRef, _, err := unstructured.NestedString(u.Object, "status", "externalRef")
+	if err != nil {
+		return "", fmt.Errorf("reading status.externalRef: %w", err)
+	}
+	if actualExternalRef == "" {
+		return "", k8s.NewReferenceNotReadyError(u.GroupVersionKind(), key)
+	}
+	r.External = actualExternalRef
+	return r.External, nil
+}
diff --git a/apis/backupdr/v1alpha1/backupplanassociation_types.go b/apis/backupdr/v1alpha1/backupplanassociation_types.go
new file mode 100644
index 00000000000..1abc4367eeb
--- /dev/null
+++ b/apis/backupdr/v1alpha1/backupplanassociation_types.go
@@ -0,0 +1,142 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1alpha1
+
+import (
+	compute "github.com/GoogleCloudPlatform/k8s-config-connector/apis/compute/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var BackupDRBackupPlanAssociationGVK = GroupVersion.WithKind("BackupDRBackupPlanAssociation")
+
+const (
+	ResourceType_ComputeInstance = "compute.googleapis.com/Instance"
+)
+
+type Resource struct {
+	ComputeInstanceRef *compute.InstanceRef `json:"computeInstanceRef,omitempty"`
+	// TODO: add other resource types
+}
+
+// BackupDRBackupPlanAssociationSpec defines the desired state of BackupDRBackupPlanAssociation
+// +kcc:proto=google.cloud.backupdr.v1.BackupPlanAssociation
+type BackupDRBackupPlanAssociationSpec struct {
+	// The BackupDRBackupPlanAssociation name. If not given, the metadata.name will be used.
+	ResourceID *string `json:"resourceID,omitempty"`
+
+	Parent `json:",inline"`
+
+	// Required. Immutable. Resource type of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource_type
+	// +required
+	ResourceType *string `json:"resourceType,omitempty"`
+
+	// Required. Immutable. Resource name of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource
+	// +required
+	Resource *Resource `json:"resource,omitempty"`
+
+	// Required. The backup plan which needs to be applied on
+	//  workload.
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.backup_plan
+	// +required
+	BackupPlanRef *BackupPlanRef `json:"backupPlanRef,omitempty"`
+}
+
+// BackupDRBackupPlanAssociationStatus defines the config connector machine state of BackupDRBackupPlanAssociation
+type BackupDRBackupPlanAssociationStatus struct {
+	/* Conditions represent the latest available observations of the
+	   object's current state. */
+	Conditions []v1alpha1.Condition `json:"conditions,omitempty"`
+
+	// ObservedGeneration is the generation of the resource that was most recently observed by the Config Connector controller. If this is equal to metadata.generation, then that means that the current reported status reflects the most recent desired state of the resource.
+	ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
+
+	// A unique specifier for the BackupDRBackupPlanAssociation resource in GCP.
+	ExternalRef *string `json:"externalRef,omitempty"`
+
+	// ObservedState is the state of the resource as most recently observed in GCP.
+	ObservedState *BackupDRBackupPlanAssociationObservedState `json:"observedState,omitempty"`
+}
+
+// BackupDRBackupPlanAssociationObservedState is the state of the BackupDRBackupPlanAssociation resource as most recently observed in GCP.
+// +kcc:proto=google.cloud.backupdr.v1.BackupPlanAssociation
+type BackupDRBackupPlanAssociationObservedState struct {
+	// Output only. Identifier. The resource name of BackupPlanAssociation in
+	//  below format Format :
+	//  projects/{project}/locations/{location}/backupPlanAssociations/{backupPlanAssociationId}
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.name
+	// NOTYET: this field serves the same purpose as externalRef
+	// Name *string `json:"name,omitempty"`
+
+	// Output only. The time when the instance was created.
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.create_time
+	CreateTime *string `json:"createTime,omitempty"`
+
+	// Output only. The time when the instance was updated.
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.update_time
+	UpdateTime *string `json:"updateTime,omitempty"`
+
+	// Output only. The BackupPlanAssociation resource state.
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.state
+	State *string `json:"state,omitempty"`
+
+	// Output only. The config info related to backup rules.
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.rules_config_info
+	RulesConfigInfo []RuleConfigInfoObservedState `json:"rulesConfigInfo,omitempty"`
+
+	// Output only. Resource name of data source which will be used as storage
+	//  location for backups taken. Format :
+	//  projects/{project}/locations/{location}/backupVaults/{backupvault}/dataSources/{datasource}
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.data_source
+	DataSource *string `json:"dataSource,omitempty"`
+}
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:resource:categories=gcp,shortName=gcpbackupdrbackupplanassociation;gcpbackupdrbackupplanassociations
+// +kubebuilder:subresource:status
+// +kubebuilder:metadata:labels="cnrm.cloud.google.com/managed-by-kcc=true";"cnrm.cloud.google.com/system=true"
+// +kubebuilder:printcolumn:name="Age",JSONPath=".metadata.creationTimestamp",type="date"
+// +kubebuilder:printcolumn:name="Ready",JSONPath=".status.conditions[?(@.type=='Ready')].status",type="string",description="When 'True', the most recent reconcile of the resource succeeded"
+// +kubebuilder:printcolumn:name="Status",JSONPath=".status.conditions[?(@.type=='Ready')].reason",type="string",description="The reason for the value in 'Ready'"
+// +kubebuilder:printcolumn:name="Status Age",JSONPath=".status.conditions[?(@.type=='Ready')].lastTransitionTime",type="date",description="The last transition time for the value in 'Status'"
+
+// BackupDRBackupPlanAssociation is the Schema for the BackupDRBackupPlanAssociation API
+// +k8s:openapi-gen=true
+type BackupDRBackupPlanAssociation struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	// +required
+	Spec   BackupDRBackupPlanAssociationSpec   `json:"spec,omitempty"`
+	Status BackupDRBackupPlanAssociationStatus `json:"status,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// BackupDRBackupPlanAssociationList contains a list of BackupDRBackupPlanAssociation
+type BackupDRBackupPlanAssociationList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []BackupDRBackupPlanAssociation `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&BackupDRBackupPlanAssociation{}, &BackupDRBackupPlanAssociationList{})
+}
diff --git a/apis/backupdr/v1alpha1/generate.sh b/apis/backupdr/v1alpha1/generate.sh
index 0d9ef371a8b..fef76d55763 100755
--- a/apis/backupdr/v1alpha1/generate.sh
+++ b/apis/backupdr/v1alpha1/generate.sh
@@ -26,7 +26,8 @@ go run . generate-types \
     --api-version backupdr.cnrm.cloud.google.com/v1alpha1 \
     --resource BackupDRManagementServer:ManagementServer \
     --resource BackupDRBackupVault:BackupVault \
-    --resource BackupDRBackupPlan:BackupPlan
+    --resource BackupDRBackupPlan:BackupPlan \
+    --resource BackupDRBackupPlanAssociation:BackupPlanAssociation
 
 go run . generate-mapper \
     --service google.cloud.backupdr.v1 \
@@ -36,5 +37,5 @@ go run . generate-mapper \
 cd ${REPO_ROOT}
 dev/tasks/generate-crds
 
-${REPO_ROOT}/dev/tasks/fix-gofmt
+go run -mod=readonly golang.org/x/tools/cmd/goimports@latest -w  pkg/controller/direct/backupdr/
 
diff --git a/apis/backupdr/v1alpha1/types.generated.go b/apis/backupdr/v1alpha1/types.generated.go
index f01e2795ab4..2852a57636e 100644
--- a/apis/backupdr/v1alpha1/types.generated.go
+++ b/apis/backupdr/v1alpha1/types.generated.go
@@ -60,6 +60,10 @@ type BackupWindow struct {
 	EndHourOfDay *int32 `json:"endHourOfDay,omitempty"`
 }
 
+// +kcc:proto=google.cloud.backupdr.v1.RuleConfigInfo
+type RuleConfigInfo struct {
+}
+
 // +kcc:proto=google.cloud.backupdr.v1.StandardSchedule
 type StandardSchedule struct {
 	// Required. Specifies the `RecurrenceType` for the schedule.
@@ -148,6 +152,83 @@ type WeekDayOfMonth struct {
 type WorkforceIdentityBasedManagementURI struct {
 }
 
+// +kcc:proto=google.protobuf.Any
+type Any struct {
+	// A URL/resource name that uniquely identifies the type of the serialized
+	//  protocol buffer message. This string must contain at least
+	//  one "/" character. The last segment of the URL's path must represent
+	//  the fully qualified name of the type (as in
+	//  `path/google.protobuf.Duration`). The name should be in a canonical form
+	//  (e.g., leading "." is not accepted).
+	//
+	//  In practice, teams usually precompile into the binary all types that they
+	//  expect it to use in the context of Any. However, for URLs which use the
+	//  scheme `http`, `https`, or no scheme, one can optionally set up a type
+	//  server that maps type URLs to message definitions as follows:
+	//
+	//  * If no scheme is provided, `https` is assumed.
+	//  * An HTTP GET on the URL must yield a [google.protobuf.Type][]
+	//    value in binary format, or produce an error.
+	//  * Applications are allowed to cache lookup results based on the
+	//    URL, or have them precompiled into a binary to avoid any
+	//    lookup. Therefore, binary compatibility needs to be preserved
+	//    on changes to types. (Use versioned type names to manage
+	//    breaking changes.)
+	//
+	//  Note: this functionality is not currently available in the official
+	//  protobuf release, and it is not used for type URLs beginning with
+	//  type.googleapis.com.
+	//
+	//  Schemes other than `http`, `https` (or the empty scheme) might be
+	//  used with implementation specific semantics.
+	// +kcc:proto:field=google.protobuf.Any.type_url
+	TypeURL *string `json:"typeURL,omitempty"`
+
+	// Must be a valid serialized protocol buffer of the above specified type.
+	// +kcc:proto:field=google.protobuf.Any.value
+	Value []byte `json:"value,omitempty"`
+}
+
+// +kcc:proto=google.rpc.Status
+type Status struct {
+	// The status code, which should be an enum value of
+	//  [google.rpc.Code][google.rpc.Code].
+	// +kcc:proto:field=google.rpc.Status.code
+	Code *int32 `json:"code,omitempty"`
+
+	// A developer-facing error message, which should be in English. Any
+	//  user-facing error message should be localized and sent in the
+	//  [google.rpc.Status.details][google.rpc.Status.details] field, or localized
+	//  by the client.
+	// +kcc:proto:field=google.rpc.Status.message
+	Message *string `json:"message,omitempty"`
+
+	// A list of messages that carry the error details.  There is a common set of
+	//  message types for APIs to use.
+	// +kcc:proto:field=google.rpc.Status.details
+	Details []Any `json:"details,omitempty"`
+}
+
+// +kcc:proto=google.cloud.backupdr.v1.RuleConfigInfo
+type RuleConfigInfoObservedState struct {
+	// Output only. Backup Rule id fetched from backup plan.
+	// +kcc:proto:field=google.cloud.backupdr.v1.RuleConfigInfo.rule_id
+	RuleID *string `json:"ruleID,omitempty"`
+
+	// Output only. The last backup state for rule.
+	// +kcc:proto:field=google.cloud.backupdr.v1.RuleConfigInfo.last_backup_state
+	LastBackupState *string `json:"lastBackupState,omitempty"`
+
+	// Output only. google.rpc.Status object to store the last backup error.
+	// +kcc:proto:field=google.cloud.backupdr.v1.RuleConfigInfo.last_backup_error
+	LastBackupError *Status `json:"lastBackupError,omitempty"`
+
+	// Output only. The point in time when the last successful backup was captured
+	//  from the source.
+	// +kcc:proto:field=google.cloud.backupdr.v1.RuleConfigInfo.last_successful_backup_consistency_time
+	LastSuccessfulBackupConsistencyTime *string `json:"lastSuccessfulBackupConsistencyTime,omitempty"`
+}
+
 // +kcc:proto=google.cloud.backupdr.v1.WorkforceIdentityBasedManagementURI
 type WorkforceIdentityBasedManagementURIObservedState struct {
 	// Output only. First party Management URI for Google Identities.
diff --git a/apis/backupdr/v1alpha1/zz_generated.deepcopy.go b/apis/backupdr/v1alpha1/zz_generated.deepcopy.go
index ee813475de9..69d80d5a7af 100644
--- a/apis/backupdr/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/backupdr/v1alpha1/zz_generated.deepcopy.go
@@ -19,11 +19,37 @@
 package v1alpha1
 
 import (
-	"github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/apis/compute/v1beta1"
+	refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
 	k8sv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Any) DeepCopyInto(out *Any) {
+	*out = *in
+	if in.TypeURL != nil {
+		in, out := &in.TypeURL, &out.TypeURL
+		*out = new(string)
+		**out = **in
+	}
+	if in.Value != nil {
+		in, out := &in.Value, &out.Value
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Any.
+func (in *Any) DeepCopy() *Any {
+	if in == nil {
+		return nil
+	}
+	out := new(Any)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *BackupDRBackupPlan) DeepCopyInto(out *BackupDRBackupPlan) {
 	*out = *in
@@ -51,6 +77,178 @@ func (in *BackupDRBackupPlan) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupDRBackupPlanAssociation) DeepCopyInto(out *BackupDRBackupPlanAssociation) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupDRBackupPlanAssociation.
+func (in *BackupDRBackupPlanAssociation) DeepCopy() *BackupDRBackupPlanAssociation {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupDRBackupPlanAssociation)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *BackupDRBackupPlanAssociation) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupDRBackupPlanAssociationList) DeepCopyInto(out *BackupDRBackupPlanAssociationList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]BackupDRBackupPlanAssociation, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupDRBackupPlanAssociationList.
+func (in *BackupDRBackupPlanAssociationList) DeepCopy() *BackupDRBackupPlanAssociationList {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupDRBackupPlanAssociationList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *BackupDRBackupPlanAssociationList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupDRBackupPlanAssociationObservedState) DeepCopyInto(out *BackupDRBackupPlanAssociationObservedState) {
+	*out = *in
+	if in.CreateTime != nil {
+		in, out := &in.CreateTime, &out.CreateTime
+		*out = new(string)
+		**out = **in
+	}
+	if in.UpdateTime != nil {
+		in, out := &in.UpdateTime, &out.UpdateTime
+		*out = new(string)
+		**out = **in
+	}
+	if in.State != nil {
+		in, out := &in.State, &out.State
+		*out = new(string)
+		**out = **in
+	}
+	if in.RulesConfigInfo != nil {
+		in, out := &in.RulesConfigInfo, &out.RulesConfigInfo
+		*out = make([]RuleConfigInfoObservedState, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.DataSource != nil {
+		in, out := &in.DataSource, &out.DataSource
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupDRBackupPlanAssociationObservedState.
+func (in *BackupDRBackupPlanAssociationObservedState) DeepCopy() *BackupDRBackupPlanAssociationObservedState {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupDRBackupPlanAssociationObservedState)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupDRBackupPlanAssociationSpec) DeepCopyInto(out *BackupDRBackupPlanAssociationSpec) {
+	*out = *in
+	if in.ResourceID != nil {
+		in, out := &in.ResourceID, &out.ResourceID
+		*out = new(string)
+		**out = **in
+	}
+	in.Parent.DeepCopyInto(&out.Parent)
+	if in.ResourceType != nil {
+		in, out := &in.ResourceType, &out.ResourceType
+		*out = new(string)
+		**out = **in
+	}
+	if in.Resource != nil {
+		in, out := &in.Resource, &out.Resource
+		*out = new(Resource)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.BackupPlanRef != nil {
+		in, out := &in.BackupPlanRef, &out.BackupPlanRef
+		*out = new(BackupPlanRef)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupDRBackupPlanAssociationSpec.
+func (in *BackupDRBackupPlanAssociationSpec) DeepCopy() *BackupDRBackupPlanAssociationSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupDRBackupPlanAssociationSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupDRBackupPlanAssociationStatus) DeepCopyInto(out *BackupDRBackupPlanAssociationStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]k8sv1alpha1.Condition, len(*in))
+		copy(*out, *in)
+	}
+	if in.ObservedGeneration != nil {
+		in, out := &in.ObservedGeneration, &out.ObservedGeneration
+		*out = new(int64)
+		**out = **in
+	}
+	if in.ExternalRef != nil {
+		in, out := &in.ExternalRef, &out.ExternalRef
+		*out = new(string)
+		**out = **in
+	}
+	if in.ObservedState != nil {
+		in, out := &in.ObservedState, &out.ObservedState
+		*out = new(BackupDRBackupPlanAssociationObservedState)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupDRBackupPlanAssociationStatus.
+func (in *BackupDRBackupPlanAssociationStatus) DeepCopy() *BackupDRBackupPlanAssociationStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupDRBackupPlanAssociationStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *BackupDRBackupPlanList) DeepCopyInto(out *BackupDRBackupPlanList) {
 	*out = *in
@@ -616,6 +814,56 @@ func (in *BackupDRManagementServerStatus) DeepCopy() *BackupDRManagementServerSt
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupPlanAssociationIdentity) DeepCopyInto(out *BackupPlanAssociationIdentity) {
+	*out = *in
+	if in.parent != nil {
+		in, out := &in.parent, &out.parent
+		*out = new(BackupPlanAssociationParent)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupPlanAssociationIdentity.
+func (in *BackupPlanAssociationIdentity) DeepCopy() *BackupPlanAssociationIdentity {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupPlanAssociationIdentity)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupPlanAssociationParent) DeepCopyInto(out *BackupPlanAssociationParent) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupPlanAssociationParent.
+func (in *BackupPlanAssociationParent) DeepCopy() *BackupPlanAssociationParent {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupPlanAssociationParent)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackupPlanAssociationRef) DeepCopyInto(out *BackupPlanAssociationRef) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupPlanAssociationRef.
+func (in *BackupPlanAssociationRef) DeepCopy() *BackupPlanAssociationRef {
+	if in == nil {
+		return nil
+	}
+	out := new(BackupPlanAssociationRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *BackupPlanIdentity) DeepCopyInto(out *BackupPlanIdentity) {
 	*out = *in
@@ -851,7 +1099,7 @@ func (in *NetworkConfig) DeepCopyInto(out *NetworkConfig) {
 	*out = *in
 	if in.NetworkRef != nil {
 		in, out := &in.NetworkRef, &out.NetworkRef
-		*out = new(v1beta1.ComputeNetworkRef)
+		*out = new(refsv1beta1.ComputeNetworkRef)
 		**out = **in
 	}
 	if in.PeeringMode != nil {
@@ -876,7 +1124,7 @@ func (in *Parent) DeepCopyInto(out *Parent) {
 	*out = *in
 	if in.ProjectRef != nil {
 		in, out := &in.ProjectRef, &out.ProjectRef
-		*out = new(v1beta1.ProjectRef)
+		*out = new(refsv1beta1.ProjectRef)
 		**out = **in
 	}
 }
@@ -891,6 +1139,76 @@ func (in *Parent) DeepCopy() *Parent {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Resource) DeepCopyInto(out *Resource) {
+	*out = *in
+	if in.ComputeInstanceRef != nil {
+		in, out := &in.ComputeInstanceRef, &out.ComputeInstanceRef
+		*out = new(v1beta1.InstanceRef)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource.
+func (in *Resource) DeepCopy() *Resource {
+	if in == nil {
+		return nil
+	}
+	out := new(Resource)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RuleConfigInfo) DeepCopyInto(out *RuleConfigInfo) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleConfigInfo.
+func (in *RuleConfigInfo) DeepCopy() *RuleConfigInfo {
+	if in == nil {
+		return nil
+	}
+	out := new(RuleConfigInfo)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RuleConfigInfoObservedState) DeepCopyInto(out *RuleConfigInfoObservedState) {
+	*out = *in
+	if in.RuleID != nil {
+		in, out := &in.RuleID, &out.RuleID
+		*out = new(string)
+		**out = **in
+	}
+	if in.LastBackupState != nil {
+		in, out := &in.LastBackupState, &out.LastBackupState
+		*out = new(string)
+		**out = **in
+	}
+	if in.LastBackupError != nil {
+		in, out := &in.LastBackupError, &out.LastBackupError
+		*out = new(Status)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.LastSuccessfulBackupConsistencyTime != nil {
+		in, out := &in.LastSuccessfulBackupConsistencyTime, &out.LastSuccessfulBackupConsistencyTime
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleConfigInfoObservedState.
+func (in *RuleConfigInfoObservedState) DeepCopy() *RuleConfigInfoObservedState {
+	if in == nil {
+		return nil
+	}
+	out := new(RuleConfigInfoObservedState)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StandardSchedule) DeepCopyInto(out *StandardSchedule) {
 	*out = *in
@@ -946,6 +1264,38 @@ func (in *StandardSchedule) DeepCopy() *StandardSchedule {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Status) DeepCopyInto(out *Status) {
+	*out = *in
+	if in.Code != nil {
+		in, out := &in.Code, &out.Code
+		*out = new(int32)
+		**out = **in
+	}
+	if in.Message != nil {
+		in, out := &in.Message, &out.Message
+		*out = new(string)
+		**out = **in
+	}
+	if in.Details != nil {
+		in, out := &in.Details, &out.Details
+		*out = make([]Any, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status.
+func (in *Status) DeepCopy() *Status {
+	if in == nil {
+		return nil
+	}
+	out := new(Status)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *WeekDayOfMonth) DeepCopyInto(out *WeekDayOfMonth) {
 	*out = *in
diff --git a/apis/compute/v1beta1/instance_identity.go b/apis/compute/v1beta1/instance_identity.go
new file mode 100644
index 00000000000..68603fa4de9
--- /dev/null
+++ b/apis/compute/v1beta1/instance_identity.go
@@ -0,0 +1,138 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1beta1
+
+import (
+	"fmt"
+	"strings"
+)
+
+// InstanceIdentity defines the resource reference to ComputeInstance, which "External" field
+// holds the GCP identifier for the KRM object.
+type InstanceIdentity struct {
+	parent *InstanceParent
+	id     string
+}
+
+func (i *InstanceIdentity) String() string {
+	return i.parent.String() + "/instances/" + i.id
+}
+
+func (i *InstanceIdentity) ID() string {
+	return i.id
+}
+
+func (i *InstanceIdentity) Parent() *InstanceParent {
+	return i.parent
+}
+
+type InstanceParent struct {
+	ProjectID string
+	Location  string
+}
+
+func (p *InstanceParent) String() string {
+	return "projects/" + p.ProjectID + "/zones/" + p.Location
+}
+
+/* NOTYET
+// New builds a InstanceIdentity from the Config Connector Instance object.
+func NewInstanceIdentity(ctx context.Context, reader client.Reader, obj *ComputeInstance) (*InstanceIdentity, error) {
+
+	// Get Parent
+	projectRef, err := refsv1beta1.ResolveProject(ctx, reader, obj.GetNamespace(), obj.Spec.ProjectRef)
+	if err != nil {
+		return nil, err
+	}
+	projectID := projectRef.ProjectID
+	if projectID == "" {
+		return nil, fmt.Errorf("cannot resolve project")
+	}
+	location := obj.Spec.Location
+
+	// Get desired ID
+	resourceID := common.ValueOf(obj.Spec.ResourceID)
+	if resourceID == "" {
+		resourceID = obj.GetName()
+	}
+	if resourceID == "" {
+		return nil, fmt.Errorf("cannot resolve resource ID")
+	}
+
+	// Use approved External
+	externalRef := common.ValueOf(obj.Status.ExternalRef)
+	if externalRef != "" {
+		// Validate desired with actual
+		actualParent, actualResourceID, err := ParseInstanceExternal(externalRef)
+		if err != nil {
+			return nil, err
+		}
+		if actualParent.ProjectID != projectID {
+			return nil, fmt.Errorf("spec.projectRef changed, expect %s, got %s", actualParent.ProjectID, projectID)
+		}
+		if actualParent.Location != location {
+			return nil, fmt.Errorf("spec.location changed, expect %s, got %s", actualParent.Location, location)
+		}
+		if actualResourceID != resourceID {
+			return nil, fmt.Errorf("cannot reset `metadata.name` or `spec.resourceID` to %s, since it has already assigned to %s",
+				resourceID, actualResourceID)
+		}
+	}
+	return &InstanceIdentity{
+		parent: &InstanceParent{
+			ProjectID: projectID,
+			Location:  location,
+		},
+		id: resourceID,
+	}, nil
+}
+*/
+
+func ParseInstanceExternal(external string) (parent *InstanceParent, resourceID string, err error) {
+	// e.g. projects/my-project/zones/us-central1-a/instances/my-instance
+	tokens := strings.Split(external, "/")
+	if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "zones" || tokens[4] != "instances" {
+		return nil, "", fmt.Errorf("format of ComputeInstance external=%q was not known (use projects/{{projectID}}/zones/{{zone}}/instances/{{instanceID}})", external)
+	}
+	parent = &InstanceParent{
+		ProjectID: tokens[1],
+		Location:  tokens[3],
+	}
+	resourceID = tokens[5]
+	return parent, resourceID, nil
+}
+
+func ParseSelfLink(selfLink string) (InstanceIdentity, error) {
+	// e.g. selfLink: https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-a/instances/my-instance
+
+	// Remove the service prefix if present
+	path := selfLink
+	if strings.HasPrefix(selfLink, "https://") {
+		parts := strings.SplitN(selfLink, "/projects/", 2)
+		if len(parts) != 2 {
+			return InstanceIdentity{}, fmt.Errorf("invalid selfLink format: %s", selfLink)
+		}
+		path = "projects/" + parts[1]
+	}
+
+	parent, resourceID, err := ParseInstanceExternal(path)
+	if err != nil {
+		return InstanceIdentity{}, fmt.Errorf("failed to parse selfLink: %w", err)
+	}
+	return InstanceIdentity{
+		parent: parent,
+		id:     resourceID,
+	}, nil
+}
diff --git a/apis/compute/v1beta1/instance_reference.go b/apis/compute/v1beta1/instance_reference.go
new file mode 100644
index 00000000000..2a11aec3925
--- /dev/null
+++ b/apis/compute/v1beta1/instance_reference.go
@@ -0,0 +1,85 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1beta1
+
+import (
+	"context"
+	"fmt"
+
+	refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var _ refsv1beta1.ExternalNormalizer = &InstanceRef{}
+var ComputeInstanceGVK = GroupVersion.WithKind("ComputeInstance")
+
+// InstanceRef defines the resource reference to ComputeInstance, which "External" field
+// holds the GCP identifier for the KRM object.
+type InstanceRef struct {
+	// A reference to an externally managed ComputeInstance resource.
+	// Should be in the format "projects/{{projectID}}/locations/{{location}}/instances/{{instanceID}}".
+	External string `json:"external,omitempty"`
+
+	// The name of a ComputeInstance resource.
+	Name string `json:"name,omitempty"`
+
+	// The namespace of a ComputeInstance resource.
+	Namespace string `json:"namespace,omitempty"`
+}
+
+// NormalizedExternal provision the "External" value for other resource that depends on ComputeInstance.
+// If the "External" is given in the other resource's spec.ComputeInstanceRef, the given value will be used.
+// Otherwise, the "Name" and "Namespace" will be used to query the actual ComputeInstance object from the cluster.
+func (r *InstanceRef) NormalizedExternal(ctx context.Context, reader client.Reader, otherNamespace string) (string, error) {
+	if r.External != "" && r.Name != "" {
+		return "", fmt.Errorf("cannot specify both name and external on %s reference", ComputeInstanceGVK.Kind)
+	}
+	// From given External
+	if r.External != "" {
+		if _, _, err := ParseInstanceExternal(r.External); err != nil {
+			return "", err
+		}
+		return r.External, nil
+	}
+
+	// From the Config Connector object
+	if r.Namespace == "" {
+		r.Namespace = otherNamespace
+	}
+	key := types.NamespacedName{Name: r.Name, Namespace: r.Namespace}
+	u := &unstructured.Unstructured{}
+	u.SetGroupVersionKind(ComputeInstanceGVK)
+	if err := reader.Get(ctx, key, u); err != nil {
+		if apierrors.IsNotFound(err) {
+			return "", k8s.NewReferenceNotFoundError(u.GroupVersionKind(), key)
+		}
+		return "", fmt.Errorf("reading referenced %s %s: %w", ComputeInstanceGVK, key, err)
+	}
+
+	selfLink, _, err := unstructured.NestedString(u.Object, "status", "selfLink")
+	if err != nil || selfLink == "" {
+		return "", fmt.Errorf("cannot get selfLink for referenced %s %v (status.selfLink is empty)", u.GetKind(), u.GetNamespace())
+	}
+	identity, err := ParseSelfLink(selfLink)
+	if err != nil {
+		return "", fmt.Errorf("failed to parse selfLink: %w", err)
+	}
+	r.External = identity.String()
+	return r.External, nil
+}
diff --git a/apis/compute/v1beta1/zz_generated.deepcopy.go b/apis/compute/v1beta1/zz_generated.deepcopy.go
index 9058fa7d6c8..0b677e8f29f 100644
--- a/apis/compute/v1beta1/zz_generated.deepcopy.go
+++ b/apis/compute/v1beta1/zz_generated.deepcopy.go
@@ -907,6 +907,56 @@ func (in *ForwardingruleTarget) DeepCopy() *ForwardingruleTarget {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InstanceIdentity) DeepCopyInto(out *InstanceIdentity) {
+	*out = *in
+	if in.parent != nil {
+		in, out := &in.parent, &out.parent
+		*out = new(InstanceParent)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceIdentity.
+func (in *InstanceIdentity) DeepCopy() *InstanceIdentity {
+	if in == nil {
+		return nil
+	}
+	out := new(InstanceIdentity)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InstanceParent) DeepCopyInto(out *InstanceParent) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceParent.
+func (in *InstanceParent) DeepCopy() *InstanceParent {
+	if in == nil {
+		return nil
+	}
+	out := new(InstanceParent)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InstanceRef) DeepCopyInto(out *InstanceRef) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceRef.
+func (in *InstanceRef) DeepCopy() *InstanceRef {
+	if in == nil {
+		return nil
+	}
+	out := new(InstanceRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *TargetTCPProxyIdentity) DeepCopyInto(out *TargetTCPProxyIdentity) {
 	*out = *in
diff --git a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_backupdrbackupplanassociations.backupdr.cnrm.cloud.google.com.yaml b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_backupdrbackupplanassociations.backupdr.cnrm.cloud.google.com.yaml
new file mode 100644
index 00000000000..f2fa3d38c4a
--- /dev/null
+++ b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_backupdrbackupplanassociations.backupdr.cnrm.cloud.google.com.yaml
@@ -0,0 +1,325 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cnrm.cloud.google.com/version: 0.0.0-dev
+  creationTimestamp: null
+  labels:
+    cnrm.cloud.google.com/managed-by-kcc: "true"
+    cnrm.cloud.google.com/system: "true"
+  name: backupdrbackupplanassociations.backupdr.cnrm.cloud.google.com
+spec:
+  group: backupdr.cnrm.cloud.google.com
+  names:
+    categories:
+    - gcp
+    kind: BackupDRBackupPlanAssociation
+    listKind: BackupDRBackupPlanAssociationList
+    plural: backupdrbackupplanassociations
+    shortNames:
+    - gcpbackupdrbackupplanassociation
+    - gcpbackupdrbackupplanassociations
+    singular: backupdrbackupplanassociation
+  preserveUnknownFields: false
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: When 'True', the most recent reconcile of the resource succeeded
+      jsonPath: .status.conditions[?(@.type=='Ready')].status
+      name: Ready
+      type: string
+    - description: The reason for the value in 'Ready'
+      jsonPath: .status.conditions[?(@.type=='Ready')].reason
+      name: Status
+      type: string
+    - description: The last transition time for the value in 'Status'
+      jsonPath: .status.conditions[?(@.type=='Ready')].lastTransitionTime
+      name: Status Age
+      type: date
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: BackupDRBackupPlanAssociation is the Schema for the BackupDRBackupPlanAssociation
+          API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BackupDRBackupPlanAssociationSpec defines the desired state
+              of BackupDRBackupPlanAssociation
+            properties:
+              backupPlanRef:
+                description: Required. The backup plan which needs to be applied on
+                  workload.
+                oneOf:
+                - not:
+                    required:
+                    - external
+                  required:
+                  - name
+                - not:
+                    anyOf:
+                    - required:
+                      - name
+                    - required:
+                      - namespace
+                  required:
+                  - external
+                properties:
+                  external:
+                    description: A reference to an externally managed BackupDRBackupPlan
+                      resource. Should be in the format "projects/{{projectID}}/locations/{{location}}/backupplans/{{backupplanID}}".
+                    type: string
+                  name:
+                    description: The name of a BackupDRBackupPlan resource.
+                    type: string
+                  namespace:
+                    description: The namespace of a BackupDRBackupPlan resource.
+                    type: string
+                type: object
+              location:
+                description: Immutable.
+                type: string
+                x-kubernetes-validations:
+                - message: Location field is immutable
+                  rule: self == oldSelf
+              projectRef:
+                description: The Project that this resource belongs to.
+                oneOf:
+                - not:
+                    required:
+                    - external
+                  required:
+                  - name
+                - not:
+                    anyOf:
+                    - required:
+                      - name
+                    - required:
+                      - namespace
+                  required:
+                  - external
+                properties:
+                  external:
+                    description: The `projectID` field of a project, when not managed
+                      by Config Connector.
+                    type: string
+                  kind:
+                    description: The kind of the Project resource; optional but must
+                      be `Project` if provided.
+                    type: string
+                  name:
+                    description: The `name` field of a `Project` resource.
+                    type: string
+                  namespace:
+                    description: The `namespace` field of a `Project` resource.
+                    type: string
+                type: object
+              resource:
+                description: Required. Immutable. Resource name of workload on which
+                  backupplan is applied
+                properties:
+                  computeInstanceRef:
+                    description: InstanceRef defines the resource reference to ComputeInstance,
+                      which "External" field holds the GCP identifier for the KRM
+                      object.
+                    oneOf:
+                    - not:
+                        required:
+                        - external
+                      required:
+                      - name
+                    - not:
+                        anyOf:
+                        - required:
+                          - name
+                        - required:
+                          - namespace
+                      required:
+                      - external
+                    properties:
+                      external:
+                        description: A reference to an externally managed ComputeInstance
+                          resource. Should be in the format "projects/{{projectID}}/locations/{{location}}/instances/{{instanceID}}".
+                        type: string
+                      name:
+                        description: The name of a ComputeInstance resource.
+                        type: string
+                      namespace:
+                        description: The namespace of a ComputeInstance resource.
+                        type: string
+                    type: object
+                type: object
+              resourceID:
+                description: The BackupDRBackupPlanAssociation name. If not given,
+                  the metadata.name will be used.
+                type: string
+              resourceType:
+                description: Required. Immutable. Resource type of workload on which
+                  backupplan is applied
+                type: string
+            required:
+            - backupPlanRef
+            - location
+            - projectRef
+            - resource
+            - resourceType
+            type: object
+          status:
+            description: BackupDRBackupPlanAssociationStatus defines the config connector
+              machine state of BackupDRBackupPlanAssociation
+            properties:
+              conditions:
+                description: Conditions represent the latest available observations
+                  of the object's current state.
+                items:
+                  properties:
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status
+                        to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about
+                        last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's
+                        last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True,
+                        False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                type: array
+              externalRef:
+                description: A unique specifier for the BackupDRBackupPlanAssociation
+                  resource in GCP.
+                type: string
+              observedGeneration:
+                description: ObservedGeneration is the generation of the resource
+                  that was most recently observed by the Config Connector controller.
+                  If this is equal to metadata.generation, then that means that the
+                  current reported status reflects the most recent desired state of
+                  the resource.
+                format: int64
+                type: integer
+              observedState:
+                description: ObservedState is the state of the resource as most recently
+                  observed in GCP.
+                properties:
+                  createTime:
+                    description: Output only. The time when the instance was created.
+                    type: string
+                  dataSource:
+                    description: 'Output only. Resource name of data source which
+                      will be used as storage location for backups taken. Format :
+                      projects/{project}/locations/{location}/backupVaults/{backupvault}/dataSources/{datasource}'
+                    type: string
+                  rulesConfigInfo:
+                    description: Output only. The config info related to backup rules.
+                    items:
+                      properties:
+                        lastBackupError:
+                          description: Output only. google.rpc.Status object to store
+                            the last backup error.
+                          properties:
+                            code:
+                              description: The status code, which should be an enum
+                                value of [google.rpc.Code][google.rpc.Code].
+                              format: int32
+                              type: integer
+                            details:
+                              description: A list of messages that carry the error
+                                details.  There is a common set of message types for
+                                APIs to use.
+                              items:
+                                properties:
+                                  typeURL:
+                                    description: |-
+                                      A URL/resource name that uniquely identifies the type of the serialized
+                                       protocol buffer message. This string must contain at least
+                                       one "/" character. The last segment of the URL's path must represent
+                                       the fully qualified name of the type (as in
+                                       `path/google.protobuf.Duration`). The name should be in a canonical form
+                                       (e.g., leading "." is not accepted).
+
+                                       In practice, teams usually precompile into the binary all types that they
+                                       expect it to use in the context of Any. However, for URLs which use the
+                                       scheme `http`, `https`, or no scheme, one can optionally set up a type
+                                       server that maps type URLs to message definitions as follows:
+
+                                       * If no scheme is provided, `https` is assumed.
+                                       * An HTTP GET on the URL must yield a [google.protobuf.Type][]
+                                         value in binary format, or produce an error.
+                                       * Applications are allowed to cache lookup results based on the
+                                         URL, or have them precompiled into a binary to avoid any
+                                         lookup. Therefore, binary compatibility needs to be preserved
+                                         on changes to types. (Use versioned type names to manage
+                                         breaking changes.)
+
+                                       Note: this functionality is not currently available in the official
+                                       protobuf release, and it is not used for type URLs beginning with
+                                       type.googleapis.com.
+
+                                       Schemes other than `http`, `https` (or the empty scheme) might be
+                                       used with implementation specific semantics.
+                                    type: string
+                                  value:
+                                    description: Must be a valid serialized protocol
+                                      buffer of the above specified type.
+                                    format: byte
+                                    type: string
+                                type: object
+                              type: array
+                            message:
+                              description: A developer-facing error message, which
+                                should be in English. Any user-facing error message
+                                should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details]
+                                field, or localized by the client.
+                              type: string
+                          type: object
+                        lastBackupState:
+                          description: Output only. The last backup state for rule.
+                          type: string
+                        lastSuccessfulBackupConsistencyTime:
+                          description: Output only. The point in time when the last
+                            successful backup was captured from the source.
+                          type: string
+                        ruleID:
+                          description: Output only. Backup Rule id fetched from backup
+                            plan.
+                          type: string
+                      type: object
+                    type: array
+                  state:
+                    description: Output only. The BackupPlanAssociation resource state.
+                    type: string
+                  updateTime:
+                    description: Output only. The time when the instance was updated.
+                    type: string
+                type: object
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/pkg/controller/direct/backupdr/backupplanassociation_fuzzer.go b/pkg/controller/direct/backupdr/backupplanassociation_fuzzer.go
new file mode 100644
index 00000000000..833b68d98ff
--- /dev/null
+++ b/pkg/controller/direct/backupdr/backupplanassociation_fuzzer.go
@@ -0,0 +1,52 @@
+// Copyright 2024 Google LLC
+//
+// 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.
+
+// +tool:fuzz-gen
+// proto.message: google.cloud.backupdr.v1.BackupPlanAssociation
+// api.group: backupdr.cnrm.cloud.google.com
+
+package backupdr
+
+import (
+	pb "cloud.google.com/go/backupdr/apiv1/backupdrpb"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/fuzztesting"
+)
+
+func init() {
+	fuzztesting.RegisterKRMFuzzer(backupDRBackupPlanAssociationFuzzer())
+}
+
+func backupDRBackupPlanAssociationFuzzer() fuzztesting.KRMFuzzer {
+	f := fuzztesting.NewKRMTypedFuzzer(&pb.BackupPlanAssociation{},
+		BackupDRBackupPlanAssociationSpec_FromProto, BackupDRBackupPlanAssociationSpec_ToProto,
+		BackupDRBackupPlanAssociationObservedState_FromProto, BackupDRBackupPlanAssociationObservedState_ToProto,
+	)
+
+	f.SpecFields.Insert(".resource_type")
+	f.SpecFields.Insert(".resource")
+	f.SpecFields.Insert(".backup_plan")
+
+	f.StatusFields.Insert(".create_time")
+	f.StatusFields.Insert(".update_time")
+	f.StatusFields.Insert(".state")
+	f.StatusFields.Insert(".rules_config_info")
+	f.StatusFields.Insert(".data_source")
+
+	f.UnimplementedFields.Insert(".name")
+
+	// the conversion of `spec.resource` relies on correctly setting `.spec.resourceType` to a magic string "compute.googleapis.com/Instance"
+	f.UnimplementedFields.Insert(".resource")
+
+	return f
+}
diff --git a/pkg/controller/direct/backupdr/backupplanassociation_mappers.go b/pkg/controller/direct/backupdr/backupplanassociation_mappers.go
new file mode 100644
index 00000000000..a5d91f14d77
--- /dev/null
+++ b/pkg/controller/direct/backupdr/backupplanassociation_mappers.go
@@ -0,0 +1,109 @@
+// Copyright 2025 Google LLC
+//
+// 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 backupdr
+
+import (
+	pb "cloud.google.com/go/backupdr/apiv1/backupdrpb"
+	statuspb "google.golang.org/genproto/googleapis/rpc/status"
+	"google.golang.org/protobuf/types/known/anypb"
+
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/backupdr/v1alpha1"
+	compute "github.com/GoogleCloudPlatform/k8s-config-connector/apis/compute/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
+)
+
+func BackupDRBackupPlanAssociationSpec_Resource_FromProto(mapCtx *direct.MapContext, resource string, resourceType string) *krm.Resource {
+	if resource == "" {
+		return nil
+	}
+	out := &krm.Resource{}
+	if resourceType == krm.ResourceType_ComputeInstance {
+		out.ComputeInstanceRef = &compute.InstanceRef{External: resource}
+	}
+	return out
+}
+func BackupDRBackupPlanAssociationSpec_Resource_ToProto(mapCtx *direct.MapContext, in *krm.Resource) string {
+	if in == nil {
+		return ""
+	}
+	if in.ComputeInstanceRef != nil {
+		return in.ComputeInstanceRef.External
+	}
+	return ""
+}
+func BackupDRBackupPlanAssociationSpec_FromProto(mapCtx *direct.MapContext, in *pb.BackupPlanAssociation) *krm.BackupDRBackupPlanAssociationSpec {
+	if in == nil {
+		return nil
+	}
+	out := &krm.BackupDRBackupPlanAssociationSpec{}
+	// MISSING: Name
+	out.ResourceType = direct.LazyPtr(in.GetResourceType())
+	out.Resource = BackupDRBackupPlanAssociationSpec_Resource_FromProto(mapCtx, in.GetResource(), in.GetResourceType())
+	if in.GetBackupPlan() != "" {
+		out.BackupPlanRef = &krm.BackupPlanRef{External: in.GetBackupPlan()}
+	}
+	return out
+}
+func BackupDRBackupPlanAssociationSpec_ToProto(mapCtx *direct.MapContext, in *krm.BackupDRBackupPlanAssociationSpec) *pb.BackupPlanAssociation {
+	if in == nil {
+		return nil
+	}
+	out := &pb.BackupPlanAssociation{}
+	// MISSING: Name
+	out.ResourceType = direct.ValueOf(in.ResourceType)
+	out.Resource = BackupDRBackupPlanAssociationSpec_Resource_ToProto(mapCtx, in.Resource)
+	if in.BackupPlanRef != nil {
+		out.BackupPlan = in.BackupPlanRef.External
+	}
+	return out
+}
+func Status_ToProto(mapCtx *direct.MapContext, in *krm.Status) *statuspb.Status {
+	if in == nil {
+		return nil
+	}
+	out := &statuspb.Status{}
+	out.Code = direct.ValueOf(in.Code)
+	out.Message = direct.ValueOf(in.Message)
+	out.Details = direct.Slice_ToProto(mapCtx, in.Details, Detail_ToProto)
+	return out
+}
+func Status_FromProto(mapCtx *direct.MapContext, in *statuspb.Status) *krm.Status {
+	if in == nil {
+		return nil
+	}
+	out := &krm.Status{}
+	out.Code = direct.LazyPtr(in.GetCode())
+	out.Message = direct.LazyPtr(in.GetMessage())
+	out.Details = direct.Slice_FromProto(mapCtx, in.GetDetails(), Detail_FromProto)
+	return out
+}
+func Detail_ToProto(mapCtx *direct.MapContext, in *krm.Any) *anypb.Any {
+	if in == nil {
+		return nil
+	}
+	out := &anypb.Any{}
+	out.TypeUrl = direct.ValueOf(in.TypeURL)
+	out.Value = in.Value
+	return out
+}
+func Detail_FromProto(mapCtx *direct.MapContext, in *anypb.Any) *krm.Any {
+	if in == nil {
+		return nil
+	}
+	out := &krm.Any{}
+	out.TypeURL = direct.LazyPtr(in.GetTypeUrl())
+	out.Value = in.GetValue()
+	return out
+}
diff --git a/pkg/controller/direct/backupdr/mapper.generated.go b/pkg/controller/direct/backupdr/mapper.generated.go
index 894e10b9949..55a59529236 100644
--- a/pkg/controller/direct/backupdr/mapper.generated.go
+++ b/pkg/controller/direct/backupdr/mapper.generated.go
@@ -20,6 +20,32 @@ import (
 	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
 )
 
+func BackupDRBackupPlanAssociationObservedState_FromProto(mapCtx *direct.MapContext, in *pb.BackupPlanAssociation) *krm.BackupDRBackupPlanAssociationObservedState {
+	if in == nil {
+		return nil
+	}
+	out := &krm.BackupDRBackupPlanAssociationObservedState{}
+	// MISSING: Name
+	out.CreateTime = direct.StringTimestamp_FromProto(mapCtx, in.GetCreateTime())
+	out.UpdateTime = direct.StringTimestamp_FromProto(mapCtx, in.GetUpdateTime())
+	out.State = direct.Enum_FromProto(mapCtx, in.GetState())
+	out.RulesConfigInfo = direct.Slice_FromProto(mapCtx, in.RulesConfigInfo, RuleConfigInfoObservedState_FromProto)
+	out.DataSource = direct.LazyPtr(in.GetDataSource())
+	return out
+}
+func BackupDRBackupPlanAssociationObservedState_ToProto(mapCtx *direct.MapContext, in *krm.BackupDRBackupPlanAssociationObservedState) *pb.BackupPlanAssociation {
+	if in == nil {
+		return nil
+	}
+	out := &pb.BackupPlanAssociation{}
+	// MISSING: Name
+	out.CreateTime = direct.StringTimestamp_ToProto(mapCtx, in.CreateTime)
+	out.UpdateTime = direct.StringTimestamp_ToProto(mapCtx, in.UpdateTime)
+	out.State = direct.Enum_ToProto[pb.BackupPlanAssociation_State](mapCtx, in.State)
+	out.RulesConfigInfo = direct.Slice_ToProto(mapCtx, in.RulesConfigInfo, RuleConfigInfoObservedState_ToProto)
+	out.DataSource = direct.ValueOf(in.DataSource)
+	return out
+}
 func BackupDRBackupPlanObservedState_FromProto(mapCtx *direct.MapContext, in *pb.BackupPlan) *krm.BackupDRBackupPlanObservedState {
 	if in == nil {
 		return nil
@@ -147,6 +173,50 @@ func NetworkConfig_ToProto(mapCtx *direct.MapContext, in *krm.NetworkConfig) *pb
 	out.PeeringMode = direct.Enum_ToProto[pb.NetworkConfig_PeeringMode](mapCtx, in.PeeringMode)
 	return out
 }
+func RuleConfigInfo_FromProto(mapCtx *direct.MapContext, in *pb.RuleConfigInfo) *krm.RuleConfigInfo {
+	if in == nil {
+		return nil
+	}
+	out := &krm.RuleConfigInfo{}
+	// MISSING: RuleID
+	// MISSING: LastBackupState
+	// MISSING: LastBackupError
+	// MISSING: LastSuccessfulBackupConsistencyTime
+	return out
+}
+func RuleConfigInfo_ToProto(mapCtx *direct.MapContext, in *krm.RuleConfigInfo) *pb.RuleConfigInfo {
+	if in == nil {
+		return nil
+	}
+	out := &pb.RuleConfigInfo{}
+	// MISSING: RuleID
+	// MISSING: LastBackupState
+	// MISSING: LastBackupError
+	// MISSING: LastSuccessfulBackupConsistencyTime
+	return out
+}
+func RuleConfigInfoObservedState_FromProto(mapCtx *direct.MapContext, in *pb.RuleConfigInfo) *krm.RuleConfigInfoObservedState {
+	if in == nil {
+		return nil
+	}
+	out := &krm.RuleConfigInfoObservedState{}
+	out.RuleID = direct.LazyPtr(in.GetRuleId())
+	out.LastBackupState = direct.Enum_FromProto(mapCtx, in.GetLastBackupState())
+	out.LastBackupError = Status_FromProto(mapCtx, in.GetLastBackupError())
+	out.LastSuccessfulBackupConsistencyTime = direct.StringTimestamp_FromProto(mapCtx, in.GetLastSuccessfulBackupConsistencyTime())
+	return out
+}
+func RuleConfigInfoObservedState_ToProto(mapCtx *direct.MapContext, in *krm.RuleConfigInfoObservedState) *pb.RuleConfigInfo {
+	if in == nil {
+		return nil
+	}
+	out := &pb.RuleConfigInfo{}
+	out.RuleId = direct.ValueOf(in.RuleID)
+	out.LastBackupState = direct.Enum_ToProto[pb.RuleConfigInfo_LastBackupState](mapCtx, in.LastBackupState)
+	out.LastBackupError = Status_ToProto(mapCtx, in.LastBackupError)
+	out.LastSuccessfulBackupConsistencyTime = direct.StringTimestamp_ToProto(mapCtx, in.LastSuccessfulBackupConsistencyTime)
+	return out
+}
 func StandardSchedule_FromProto(mapCtx *direct.MapContext, in *pb.StandardSchedule) *krm.StandardSchedule {
 	if in == nil {
 		return nil
diff --git a/pkg/gvks/supportedgvks/gvks_generated.go b/pkg/gvks/supportedgvks/gvks_generated.go
index 71e0c7af901..386b120d287 100644
--- a/pkg/gvks/supportedgvks/gvks_generated.go
+++ b/pkg/gvks/supportedgvks/gvks_generated.go
@@ -487,6 +487,16 @@ var SupportedGVKs = map[schema.GroupVersionKind]GVKMetadata{
 			"cnrm.cloud.google.com/tf2crd":          "true",
 		},
 	},
+	{
+		Group:   "backupdr.cnrm.cloud.google.com",
+		Version: "v1alpha1",
+		Kind:    "BackupDRBackupPlanAssociation",
+	}: {
+		Labels: map[string]string{
+			"cnrm.cloud.google.com/managed-by-kcc": "true",
+			"cnrm.cloud.google.com/system":         "true",
+		},
+	},
 	{
 		Group:   "backupdr.cnrm.cloud.google.com",
 		Version: "v1alpha1",


Reviews:
user: login: yuwenma
body: /approve

One question to confirm, otherwise looks good!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T19:41:17Z
user: login: jingyih
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T23:04:38Z
user: login: jingyih
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T22:31:07Z

Review comments:
diff_hunk: @@ -0,0 +1,131 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1alpha1
+
+import (
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var BackupDRBackupPlanAssociationGVK = GroupVersion.WithKind("BackupDRBackupPlanAssociation")
+
+// BackupDRBackupPlanAssociationSpec defines the desired state of BackupDRBackupPlanAssociation
+// +kcc:proto=google.cloud.backupdr.v1.BackupPlanAssociation
+type BackupDRBackupPlanAssociationSpec struct {
+	// The BackupDRBackupPlanAssociation name. If not given, the metadata.name will be used.
+	ResourceID *string `json:"resourceID,omitempty"`
+
+	Parent `json:",inline"`
+
+	// Required. Immutable. Resource type of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource_type
+	// +required
+	ResourceType *string `json:"resourceType,omitempty"`
+
+	// Required. Immutable. Resource name of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource
+	// +required
+	Resource *string `json:"resource,omitempty"`
path: apis/backupdr/v1alpha1/backupplanassociation_types.go
user: login: yuwenma
body: This looks like a reference, could you confirm?
created_at: 2025-03-15T19:40:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1alpha1
+
+import (
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var BackupDRBackupPlanAssociationGVK = GroupVersion.WithKind("BackupDRBackupPlanAssociation")
+
+// BackupDRBackupPlanAssociationSpec defines the desired state of BackupDRBackupPlanAssociation
+// +kcc:proto=google.cloud.backupdr.v1.BackupPlanAssociation
+type BackupDRBackupPlanAssociationSpec struct {
+	// The BackupDRBackupPlanAssociation name. If not given, the metadata.name will be used.
+	ResourceID *string `json:"resourceID,omitempty"`
+
+	Parent `json:",inline"`
+
+	// Required. Immutable. Resource type of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource_type
+	// +required
+	ResourceType *string `json:"resourceType,omitempty"`
+
+	// Required. Immutable. Resource name of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource
+	// +required
+	Resource *string `json:"resource,omitempty"`
path: apis/backupdr/v1alpha1/backupplanassociation_types.go
user: login: jingyih
body: Good catch! There isn't much description in the REST API documentation [1]. After checking Google's internal code, I found that their tests use a something like
```
Resource = "projects/" + ConsumerProjectID + "/zones/" + ResourceLocation + "/instances/" + ResourceID
```
This confirms that it is indeed a reference field. Further investigation indicates that this field can reference multiple resource types [2]. Maybe we start by supporting the most commonly used Compute Engine instances and consider other resource types as feature requests.

[1] https://cloud.google.com/backup-disaster-recovery/docs/reference/rest/v1/projects.locations.backupPlanAssociations

[2] https://cloud.google.com/backup-disaster-recovery/docs/create-plan/apply-backup-temp-overview#application-types_specific_protection_procedures
created_at: 2025-03-15T23:04:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+// Copyright 2025 Google LLC
+//
+// 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 v1alpha1
+
+import (
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var BackupDRBackupPlanAssociationGVK = GroupVersion.WithKind("BackupDRBackupPlanAssociation")
+
+// BackupDRBackupPlanAssociationSpec defines the desired state of BackupDRBackupPlanAssociation
+// +kcc:proto=google.cloud.backupdr.v1.BackupPlanAssociation
+type BackupDRBackupPlanAssociationSpec struct {
+	// The BackupDRBackupPlanAssociation name. If not given, the metadata.name will be used.
+	ResourceID *string `json:"resourceID,omitempty"`
+
+	Parent `json:",inline"`
+
+	// Required. Immutable. Resource type of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource_type
+	// +required
+	ResourceType *string `json:"resourceType,omitempty"`
+
+	// Required. Immutable. Resource name of workload on which backupplan is
+	//  applied
+	// +kcc:proto:field=google.cloud.backupdr.v1.BackupPlanAssociation.resource
+	// +required
+	Resource *string `json:"resource,omitempty"`
path: apis/backupdr/v1alpha1/backupplanassociation_types.go
user: login: jingyih
body: Fixed.

 - https://github.com/GoogleCloudPlatform/k8s-config-connector/pull/4036/commits/930a0b40efecb6d81a89200c8068afa640be6d25: add `computeInstanceRef`

 - https://github.com/GoogleCloudPlatform/k8s-config-connector/pull/4036/commits/5d4d14928ec830e7447d1e2cb5e845b3b3de545d: make `spec.resource` reference `computeInstanceRef`
created_at: 2025-03-16T22:31:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: google-oss-prow[bot]
created_at: 2025-03-15T19:41:34Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/GoogleCloudPlatform/k8s-config-connector/pull/4036#pullrequestreview-2688263231" title="Approved">yuwenma</a>*

The full list of commands accepted by this bot can be found [here](https://oss.gprow.dev/command-help?repo=GoogleCloudPlatform%2Fk8s-config-connector).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/GoogleCloudPlatform/k8s-config-connector/blob/master/OWNERS)~~ [yuwenma]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: jingyih
created_at: 2025-03-16T22:31:18Z
body: Rebase to pick up the fix for broken HEAD.
author_association: CONTRIBUTOR
user: login: yuwenma
created_at: 2025-03-16T22:57:07Z
body: /lgtm
author_association: CONTRIBUTOR