Repository: GoogleCloudPlatform/k8s-config-connector
Pull request #4364: Direct Controller for  eventarc googlechannelconfig
Author: login: barney-s
Labels: name: lgtm
name: approved
name: review
Created: 2025-04-15T07:14:46Z | Merged: 2025-04-16T17:54:24Z



Diff:
diff --git a/apis/eventarc/v1alpha1/channel_types.go b/apis/eventarc/v1alpha1/channel_types.go
index 91bf7768f13..09941d10a42 100644
--- a/apis/eventarc/v1alpha1/channel_types.go
+++ b/apis/eventarc/v1alpha1/channel_types.go
@@ -26,7 +26,7 @@ var EventarcChannelGVK = GroupVersion.WithKind("EventarcChannel")
 type Parent struct {
 	// +required
 	Location string `json:"location"`
-	// +optional
+	// +required
 	ProjectRef *refv1beta1.ProjectRef `json:"projectRef,omitempty"`
 }
 
diff --git a/apis/eventarc/v1alpha1/googlechannelconfig_identity.go b/apis/eventarc/v1alpha1/googlechannelconfig_identity.go
index b82fbe0e2c8..c1884fa55fc 100644
--- a/apis/eventarc/v1alpha1/googlechannelconfig_identity.go
+++ b/apis/eventarc/v1alpha1/googlechannelconfig_identity.go
@@ -33,7 +33,8 @@ type GoogleChannelConfigIdentity struct {
 
 func (i *GoogleChannelConfigIdentity) String() string {
 	// Use the singular name 'googleChannelConfig' from the proto definition
-	return i.parent.String() + "/googleChannelConfig/" + i.id
+	// The resource ID *is* expected by the API.
+	return i.parent.String() + "/" + i.id
 }
 
 func (i *GoogleChannelConfigIdentity) ID() string {
@@ -75,6 +76,9 @@ func NewGoogleChannelConfigIdentity(ctx context.Context, reader client.Reader, o
 	if resourceID == "" {
 		return nil, fmt.Errorf("cannot resolve resource ID")
 	}
+	if resourceID != "googleChannelConfig" {
+		return nil, fmt.Errorf("resourceID or .metadata.name can only be 'googleChannelConfig'")
+	}
 
 	// Use approved External
 	externalRef := common.ValueOf(obj.Status.ExternalRef)
@@ -108,13 +112,13 @@ func ParseGoogleChannelConfigExternal(external string) (parent *GoogleChannelCon
 	tokens := strings.Split(external, "/")
 	// Expected format based on proto pattern `projects/{project}/locations/{location}/googleChannelConfig` and KCC convention of adding resource ID:
 	// projects/{projectID}/locations/{location}/googleChannelConfig/{googlechannelconfigID}
-	if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "googleChannelConfig" {
+	if len(tokens) != 5 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "googleChannelConfig" {
 		return nil, "", fmt.Errorf("format of EventarcGoogleChannelConfig external=%q was not known (use projects/{projectID}/locations/{location}/googleChannelConfig/{googlechannelconfigID})", external)
 	}
 	parent = &GoogleChannelConfigParent{
 		ProjectID: tokens[1],
 		Location:  tokens[3],
 	}
-	resourceID = tokens[5]
+	resourceID = tokens[4]
 	return parent, resourceID, nil
 }
diff --git a/apis/eventarc/v1alpha1/googlechannelconfig_types.go b/apis/eventarc/v1alpha1/googlechannelconfig_types.go
index 44b12343b1d..67d6c6043b4 100644
--- a/apis/eventarc/v1alpha1/googlechannelconfig_types.go
+++ b/apis/eventarc/v1alpha1/googlechannelconfig_types.go
@@ -31,7 +31,6 @@ var EventarcGoogleChannelConfigGVK = GroupVersion.WithKind("EventarcGoogleChanne
 // EventarcGoogleChannelConfigSpec defines the desired state of EventarcGoogleChannelConfig
 // +kcc:proto=google.cloud.eventarc.v1.GoogleChannelConfig
 type EventarcGoogleChannelConfigSpec struct {
-	// +required
 	Parent `json:",inline"`
 
 	// Optional. Resource name of a KMS crypto key (managed by the user) used to
diff --git a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcchannels.eventarc.cnrm.cloud.google.com.yaml b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcchannels.eventarc.cnrm.cloud.google.com.yaml
index 167fd18f647..ee74c93c491 100644
--- a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcchannels.eventarc.cnrm.cloud.google.com.yaml
+++ b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcchannels.eventarc.cnrm.cloud.google.com.yaml
@@ -162,6 +162,7 @@ spec:
                 type: string
             required:
             - location
+            - projectRef
             type: object
           status:
             description: EventarcChannelStatus defines the config connector machine
diff --git a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcgooglechannelconfigs.eventarc.cnrm.cloud.google.com.yaml b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcgooglechannelconfigs.eventarc.cnrm.cloud.google.com.yaml
index 47f3503593b..bc43ed530b5 100644
--- a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcgooglechannelconfigs.eventarc.cnrm.cloud.google.com.yaml
+++ b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_eventarcgooglechannelconfigs.eventarc.cnrm.cloud.google.com.yaml
@@ -129,8 +129,8 @@ spec:
                   If not specified, the name of the KRM resource will be used.
                 type: string
             required:
-            - ""
             - location
+            - projectRef
             type: object
           status:
             description: EventarcGoogleChannelConfigStatus defines the config connector
diff --git a/config/tests/samples/create/harness.go b/config/tests/samples/create/harness.go
index eb4c3fc9148..a95649bec96 100644
--- a/config/tests/samples/create/harness.go
+++ b/config/tests/samples/create/harness.go
@@ -874,6 +874,8 @@ func MaybeSkip(t *testing.T, name string, resources []*unstructured.Unstructured
 			case schema.GroupKind{Group: "edgenetwork.cnrm.cloud.google.com", Kind: "EdgeNetworkNetwork"}:
 			case schema.GroupKind{Group: "edgenetwork.cnrm.cloud.google.com", Kind: "EdgeNetworkSubnet"}:
 
+			case schema.GroupKind{Group: "eventarc.cnrm.cloud.google.com", Kind: "EventarcGoogleChannelConfig"}:
+
 			case schema.GroupKind{Group: "firestore.cnrm.cloud.google.com", Kind: "FirestoreDatabase"}:
 
 			case schema.GroupKind{Group: "kms.cnrm.cloud.google.com", Kind: "KMSKeyRing"}:
diff --git a/hack/record-gcp b/hack/record-gcp
index e385068d712..816e7ae9834 100755
--- a/hack/record-gcp
+++ b/hack/record-gcp
@@ -56,6 +56,13 @@ fi
 export KCC_USE_DIRECT_RECONCILERS
 echo "KCC_USE_DIRECT_RECONCILERS=$KCC_USE_DIRECT_RECONCILERS"
 
+# Set timeout from env var, default to 7200s if not set
+if [[ -z "${E2E_TEST_TIMEOUT:-}" ]]; then
+  E2E_TEST_TIMEOUT="7200s"
+fi
+echo "Using test timeout: ${E2E_TEST_TIMEOUT}"
+
+
 # Run e2e tests against real gcp, capturing output
 ARTIFACTS=$(pwd)/artifactz/realgcp \
 E2E_KUBE_TARGET=envtest \
@@ -65,4 +72,4 @@ GOLDEN_OBJECT_CHECKS=1 \
 WRITE_GOLDEN_OUTPUT=1 \
 RUN_E2E=1 \
 KCC_USE_DIRECT_RECONCILERS=$KCC_USE_DIRECT_RECONCILERS \
- go test ./tests/e2e -timeout 7200s -v -run $RUN_TESTS
\ No newline at end of file
+ go test ./tests/e2e -timeout ${E2E_TEST_TIMEOUT} -v -run $RUN_TESTS
\ No newline at end of file
diff --git a/mockgcp/mockeventarc/googlechannelconfig.go b/mockgcp/mockeventarc/googlechannelconfig.go
index 6a18459e68f..08d398b89b9 100755
--- a/mockgcp/mockeventarc/googlechannelconfig.go
+++ b/mockgcp/mockeventarc/googlechannelconfig.go
@@ -36,7 +36,7 @@ import (
 
 func (s *EventarcV1) GetGoogleChannelConfig(ctx context.Context, req *pb.GetGoogleChannelConfigRequest) (*pb.GoogleChannelConfig, error) {
 	reqName := req.GetName()
-	_, err := s.parseGoogleChannelConfigName(reqName)
+	id, err := s.parseGoogleChannelConfigName(reqName)
 	if err != nil {
 		return nil, err
 	}
@@ -49,8 +49,9 @@ func (s *EventarcV1) GetGoogleChannelConfig(ctx context.Context, req *pb.GetGoog
 	}
 
 	minimalConfig := &pb.GoogleChannelConfig{
-		Name:       reqName,
-		UpdateTime: timestamppb.New(time.Now()),
+		Name:          reqName,
+		UpdateTime:    timestamppb.New(time.Now()),
+		CryptoKeyName: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", id.Project.ID, id.Location, "test-key-ring", "test-crypto-key"),
 	}
 
 	return minimalConfig, nil
diff --git a/pkg/controller/direct/eventarc/client.go b/pkg/controller/direct/eventarc/client.go
new file mode 100644
index 00000000000..f1ec2d27f1e
--- /dev/null
+++ b/pkg/controller/direct/eventarc/client.go
@@ -0,0 +1,49 @@
+// 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:controller-client
+// proto.service: google.cloud.eventarc.v1.Eventarc
+
+package eventarc
+
+import (
+	"context"
+	"fmt"
+
+	api "cloud.google.com/go/eventarc/apiv1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
+)
+
+type gcpClient struct {
+	config *config.ControllerConfig
+}
+
+func newGCPClient(ctx context.Context, config *config.ControllerConfig) (*gcpClient, error) {
+	gcpClient := &gcpClient{
+		config: config,
+	}
+	return gcpClient, nil
+}
+
+func (m *gcpClient) newEventarcClient(ctx context.Context) (*api.Client, error) {
+	opts, err := m.config.RESTClientOptions()
+	if err != nil {
+		return nil, err
+	}
+	client, err := api.NewRESTClient(ctx, opts...)
+	if err != nil {
+		return nil, fmt.Errorf("building eventarc client: %w", err)
+	}
+	return client, err
+}
diff --git a/pkg/controller/direct/eventarc/googlechannelconfig_controller.go b/pkg/controller/direct/eventarc/googlechannelconfig_controller.go
new file mode 100644
index 00000000000..adef171d5cf
--- /dev/null
+++ b/pkg/controller/direct/eventarc/googlechannelconfig_controller.go
@@ -0,0 +1,220 @@
+// 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:controller
+// proto.service: google.cloud.eventarc.v1.Eventarc
+// proto.message: google.cloud.eventarc.v1.GoogleChannelConfig
+// crd.type: EventarcGoogleChannelConfig
+// crd.version: v1alpha1
+
+package eventarc
+
+import (
+	"context"
+	"fmt"
+
+	gcp "cloud.google.com/go/eventarc/apiv1"
+	pb "cloud.google.com/go/eventarc/apiv1/eventarcpb"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/eventarc/v1alpha1"
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
+)
+
+func init() {
+	registry.RegisterModel(krm.EventarcGoogleChannelConfigGVK, NewGoogleChannelConfigModel)
+}
+
+func NewGoogleChannelConfigModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &googleChannelConfigModel{config: *config}, nil
+}
+
+var _ directbase.Model = &googleChannelConfigModel{}
+
+type googleChannelConfigModel struct {
+	config config.ControllerConfig
+}
+
+func (m *googleChannelConfigModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.EventarcGoogleChannelConfig{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewGoogleChannelConfigIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get eventarc GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	eventarcClient, err := gcpClient.newEventarcClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &googleChannelConfigAdapter{
+		gcpClient: eventarcClient,
+		id:        id,
+		desired:   obj,
+		reader:    reader,
+	}, nil
+}
+
+func (m *googleChannelConfigModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type googleChannelConfigAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.GoogleChannelConfigIdentity
+	desired   *krm.EventarcGoogleChannelConfig
+	actual    *pb.GoogleChannelConfig
+	reader    client.Reader
+}
+
+var _ directbase.Adapter = &googleChannelConfigAdapter{}
+
+func (a *googleChannelConfigAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting eventarc googlechannelconfig", "name", a.id)
+
+	req := &pb.GetGoogleChannelConfigRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetGoogleChannelConfig(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting eventarc googlechannelconfig %q: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	// GoogleChannelConfig is a singleton resource that cannot be created.
+	// It can only be fetched and updated.
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating eventarc googlechannelconfig is a no-op", "name", a.id)
+	return nil
+}
+
+// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating eventarc googlechannelconfig", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	if err := a.normalizeReferenceFields(ctx); err != nil {
+		return err
+	}
+
+	desired := a.desired.DeepCopy()
+	resource := EventarcGoogleChannelConfigSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	if resource.CryptoKeyName != a.actual.CryptoKeyName {
+		paths = append(paths, "crypto_key_name")
+	}
+
+	var updated *pb.GoogleChannelConfig
+	if len(paths) == 0 {
+		log.V(2).Info("no field needs update", "name", a.id)
+		// even though there is no update, we still want to update KRM status
+		updated = a.actual
+	} else {
+		resource.Name = a.id.String() // we need to set the name so that GCP API can identify the resource
+		req := &pb.UpdateGoogleChannelConfigRequest{
+			GoogleChannelConfig: resource,
+			UpdateMask:          &fieldmaskpb.FieldMask{Paths: paths},
+		}
+		var err error
+		updated, err = a.gcpClient.UpdateGoogleChannelConfig(ctx, req)
+		if err != nil {
+			return fmt.Errorf("updating eventarc googlechannelconfig %s: %w", a.id.String(), err)
+		}
+		log.V(2).Info("successfully updated eventarc googlechannelconfig", "name", a.id)
+	}
+
+	status := &krm.EventarcGoogleChannelConfigStatus{}
+	status.ObservedState = EventarcGoogleChannelConfigObservedState_FromProto(mapCtx, updated)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+	status.ExternalRef = direct.LazyPtr(a.id.String())
+	return updateOp.UpdateStatus(ctx, status, nil)
+}
+
+// Export maps the GCP object to a Config Connector resource `spec`.
+func (a *googleChannelConfigAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) {
+	if a.actual == nil {
+		return nil, fmt.Errorf("Find() not called")
+	}
+	u := &unstructured.Unstructured{}
+
+	obj := &krm.EventarcGoogleChannelConfig{}
+	mapCtx := &direct.MapContext{}
+	obj.Spec = direct.ValueOf(EventarcGoogleChannelConfigSpec_FromProto(mapCtx, a.actual))
+	if mapCtx.Err() != nil {
+		return nil, mapCtx.Err()
+	}
+	obj.Spec.ProjectRef = &refs.ProjectRef{External: a.id.Parent().ProjectID}
+	obj.Spec.Location = a.id.Parent().Location
+	uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// Use the static name for the singleton resource.
+	u.SetName("googlechannelconfig")
+	u.SetGroupVersionKind(krm.EventarcGoogleChannelConfigGVK)
+	u.Object = uObj
+	return u, nil
+}
+
+// Delete the resource from GCP service when the corresponding Config Connector resource is deleted.
+func (a *googleChannelConfigAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("deleting eventarc googlechannelconfig is a no-op", "name", a.id)
+	// GoogleChannelConfig is a singleton resource that cannot be deleted.
+	return true, nil
+}
+
+func (a *googleChannelConfigAdapter) normalizeReferenceFields(ctx context.Context) error {
+	obj := a.desired
+	if obj.Spec.CryptoKeyRef != nil {
+		kmsKeyRef, err := refs.ResolveKMSCryptoKeyRef(ctx, a.reader, obj, obj.Spec.CryptoKeyRef)
+		if err != nil {
+			return err
+		}
+		obj.Spec.CryptoKeyRef = kmsKeyRef
+	}
+	return nil
+}
diff --git a/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_generated_object_eventarcgooglechannelconfig-minimal.golden.yaml b/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_generated_object_eventarcgooglechannelconfig-minimal.golden.yaml
new file mode 100644
index 00000000000..8aae215d7a8
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_generated_object_eventarcgooglechannelconfig-minimal.golden.yaml
@@ -0,0 +1,27 @@
+apiVersion: eventarc.cnrm.cloud.google.com/v1alpha1
+kind: EventarcGoogleChannelConfig
+metadata:
+  finalizers:
+  - cnrm.cloud.google.com/finalizer
+  - cnrm.cloud.google.com/deletion-defender
+  generation: 1
+  labels:
+    cnrm-test: "true"
+  name: eventarcgooglechannelconfig-minimal-${uniqueId}
+  namespace: ${uniqueId}
+spec:
+  location: us-central1
+  projectRef:
+    external: ${projectId}
+  resourceID: googleChannelConfig
+status:
+  conditions:
+  - lastTransitionTime: "1970-01-01T00:00:00Z"
+    message: The resource is up to date
+    reason: UpToDate
+    status: "True"
+    type: Ready
+  externalRef: projects/${projectId}/locations/us-central1/googleChannelConfig
+  observedGeneration: 1
+  observedState:
+    updateTime: "1970-01-01T00:00:00Z"
diff --git a/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_http.log b/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_http.log
new file mode 100644
index 00000000000..c3498617098
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_http.log
@@ -0,0 +1,68 @@
+GET https://eventarc.googleapis.com/v1/projects/${projectId}/locations/us-central1/googleChannelConfig?%24alt=json%3Benum-encoding%3Dint
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: name=projects%2F${projectId}%2Flocations%2Fus-central1%2FgoogleChannelConfig
+
+200 OK
+Content-Type: application/json; charset=UTF-8
+Server: ESF
+Vary: Origin
+Vary: X-Origin
+Vary: Referer
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+X-Xss-Protection: 0
+
+{
+  "cryptoKeyName": "projects/${projectId}/locations/us-central1/keyRings/test-key-ring/cryptoKeys/test-crypto-key",
+  "name": "projects/${projectId}/locations/us-central1/googleChannelConfig",
+  "updateTime": "2024-04-01T12:34:56.123456Z"
+}
+
+---
+
+PATCH https://eventarc.googleapis.com/v1/projects/${projectId}/locations/us-central1/googleChannelConfig?%24alt=json%3Benum-encoding%3Dint&updateMask=cryptoKeyName
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: google_channel_config.name=projects%2F${projectId}%2Flocations%2Fus-central1%2FgoogleChannelConfig
+
+{
+  "name": "projects/${projectId}/locations/us-central1/googleChannelConfig"
+}
+
+200 OK
+Content-Type: application/json; charset=UTF-8
+Server: ESF
+Vary: Origin
+Vary: X-Origin
+Vary: Referer
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+X-Xss-Protection: 0
+
+{
+  "name": "projects/${projectId}/locations/us-central1/googleChannelConfig",
+  "updateTime": "2024-04-01T12:34:56.123456Z"
+}
+
+---
+
+GET https://eventarc.googleapis.com/v1/projects/${projectId}/locations/us-central1/googleChannelConfig?%24alt=json%3Benum-encoding%3Dint
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: name=projects%2F${projectId}%2Flocations%2Fus-central1%2FgoogleChannelConfig
+
+200 OK
+Content-Type: application/json; charset=UTF-8
+Server: ESF
+Vary: Origin
+Vary: X-Origin
+Vary: Referer
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+X-Xss-Protection: 0
+
+{
+  "name": "projects/${projectId}/locations/us-central1/googleChannelConfig",
+  "updateTime": "2024-04-01T12:34:56.123456Z"
+}
\ No newline at end of file
diff --git a/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/create.yaml b/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/create.yaml
new file mode 100644
index 00000000000..e886c94c741
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/create.yaml
@@ -0,0 +1,23 @@
+# 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.
+
+apiVersion: eventarc.cnrm.cloud.google.com/v1alpha1
+kind: EventarcGoogleChannelConfig
+metadata:
+  name: eventarcgooglechannelconfig-minimal-${uniqueId}
+spec:
+  location: us-central1
+  projectRef:
+    external: ${projectId}
+  resourceID: googleChannelConfig


Reviews:
user: login: jingyih
body: /approve
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T19:40:18Z
user: login: barney-s
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T22:02:11Z
user: login: barney-s
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T22:02:43Z
user: login: barney-s
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T22:03:13Z
user: login: barney-s
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T22:38:04Z
user: login: jingyih
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T22:41:00Z
user: login: jingyih
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T22:42:15Z
user: login: jingyih
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T22:43:22Z
user: login: barney-s
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T06:57:46Z
user: login: barney-s
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T16:57:52Z

Review comments:
diff_hunk: @@ -0,0 +1,67 @@
+GET https://eventarc.googleapis.com/v1/projects/${projectId}/locations/us-central1/googleChannelConfig?%24alt=json%3Benum-encoding%3Dint
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: name=projects%2F${projectId}%2Flocations%2Fus-central1%2FgoogleChannelConfig
+
+200 OK
+Content-Type: application/json; charset=UTF-8
+Server: ESF
+Vary: Origin
+Vary: X-Origin
+Vary: Referer
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+X-Xss-Protection: 0
+
+{
+  "name": "projects/${projectId}/locations/us-central1/googleChannelConfig",
path: pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_http.log
user: login: jingyih
body: Is this pre-created before the test?
created_at: 2025-04-15T19:37:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+# 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.
+
+apiVersion: eventarc.cnrm.cloud.google.com/v1alpha1
path: pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/update.yaml
user: login: jingyih
body: I don't see any field being updated in this file. Maybe just remove this file if not used?
created_at: 2025-04-15T19:38:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,218 @@
+// 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:controller
+// proto.service: google.cloud.eventarc.v1.Eventarc
+// proto.message: google.cloud.eventarc.v1.GoogleChannelConfig
+// crd.type: EventarcGoogleChannelConfig
+// crd.version: v1alpha1
+
+package eventarc
+
+import (
+	"context"
+	"fmt"
+
+	gcp "cloud.google.com/go/eventarc/apiv1"
+	pb "cloud.google.com/go/eventarc/apiv1/eventarcpb"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/eventarc/v1alpha1"
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
+)
+
+func init() {
+	registry.RegisterModel(krm.EventarcGoogleChannelConfigGVK, NewGoogleChannelConfigModel)
+}
+
+func NewGoogleChannelConfigModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &googleChannelConfigModel{config: *config}, nil
+}
+
+var _ directbase.Model = &googleChannelConfigModel{}
+
+type googleChannelConfigModel struct {
+	config config.ControllerConfig
+}
+
+func (m *googleChannelConfigModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.EventarcGoogleChannelConfig{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewGoogleChannelConfigIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get eventarc GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	eventarcClient, err := gcpClient.newEventarcClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &googleChannelConfigAdapter{
+		gcpClient: eventarcClient,
+		id:        id,
+		desired:   obj,
+		reader:    reader,
+	}, nil
+}
+
+func (m *googleChannelConfigModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type googleChannelConfigAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.GoogleChannelConfigIdentity
+	desired   *krm.EventarcGoogleChannelConfig
+	actual    *pb.GoogleChannelConfig
+	reader    client.Reader
+}
+
+var _ directbase.Adapter = &googleChannelConfigAdapter{}
+
+func (a *googleChannelConfigAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting eventarc googlechannelconfig", "name", a.id)
+
+	req := &pb.GetGoogleChannelConfigRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetGoogleChannelConfig(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting eventarc googlechannelconfig %q: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	// GoogleChannelConfig is a singleton resource that cannot be created.
+	// It can only be fetched and updated.
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating eventarc googlechannelconfig is a no-op", "name", a.id)
+	return nil
+}
+
+// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating eventarc googlechannelconfig", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	if err := a.normalizeReferenceFields(ctx); err != nil {
+		return err
+	}
+
+	desired := a.desired.DeepCopy()
+	resource := EventarcGoogleChannelConfigSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	paths = append(paths, "crypto_key_name")
path: pkg/controller/direct/eventarc/googlechannelconfig_controller.go
user: login: jingyih
body: I don't think we should hardcode this field to be always updated.
created_at: 2025-04-15T19:39:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+# 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.
+
+apiVersion: eventarc.cnrm.cloud.google.com/v1alpha1
path: pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/update.yaml
user: login: barney-s
body: This resource only has update. Create is the optional one. But the test did not execute without a create. So update is looking like a no-op. Do you know how to deal with update only resource in a test ? 
created_at: 2025-04-15T22:02:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,218 @@
+// 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:controller
+// proto.service: google.cloud.eventarc.v1.Eventarc
+// proto.message: google.cloud.eventarc.v1.GoogleChannelConfig
+// crd.type: EventarcGoogleChannelConfig
+// crd.version: v1alpha1
+
+package eventarc
+
+import (
+	"context"
+	"fmt"
+
+	gcp "cloud.google.com/go/eventarc/apiv1"
+	pb "cloud.google.com/go/eventarc/apiv1/eventarcpb"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/eventarc/v1alpha1"
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
+)
+
+func init() {
+	registry.RegisterModel(krm.EventarcGoogleChannelConfigGVK, NewGoogleChannelConfigModel)
+}
+
+func NewGoogleChannelConfigModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &googleChannelConfigModel{config: *config}, nil
+}
+
+var _ directbase.Model = &googleChannelConfigModel{}
+
+type googleChannelConfigModel struct {
+	config config.ControllerConfig
+}
+
+func (m *googleChannelConfigModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.EventarcGoogleChannelConfig{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewGoogleChannelConfigIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get eventarc GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	eventarcClient, err := gcpClient.newEventarcClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &googleChannelConfigAdapter{
+		gcpClient: eventarcClient,
+		id:        id,
+		desired:   obj,
+		reader:    reader,
+	}, nil
+}
+
+func (m *googleChannelConfigModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type googleChannelConfigAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.GoogleChannelConfigIdentity
+	desired   *krm.EventarcGoogleChannelConfig
+	actual    *pb.GoogleChannelConfig
+	reader    client.Reader
+}
+
+var _ directbase.Adapter = &googleChannelConfigAdapter{}
+
+func (a *googleChannelConfigAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting eventarc googlechannelconfig", "name", a.id)
+
+	req := &pb.GetGoogleChannelConfigRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetGoogleChannelConfig(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting eventarc googlechannelconfig %q: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	// GoogleChannelConfig is a singleton resource that cannot be created.
+	// It can only be fetched and updated.
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating eventarc googlechannelconfig is a no-op", "name", a.id)
+	return nil
+}
+
+// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating eventarc googlechannelconfig", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	if err := a.normalizeReferenceFields(ctx); err != nil {
+		return err
+	}
+
+	desired := a.desired.DeepCopy()
+	resource := EventarcGoogleChannelConfigSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	paths = append(paths, "crypto_key_name")
path: pkg/controller/direct/eventarc/googlechannelconfig_controller.go
user: login: barney-s
body: Update has only one field. How do we deal with this ? 
created_at: 2025-04-15T22:02:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,218 @@
+// 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:controller
+// proto.service: google.cloud.eventarc.v1.Eventarc
+// proto.message: google.cloud.eventarc.v1.GoogleChannelConfig
+// crd.type: EventarcGoogleChannelConfig
+// crd.version: v1alpha1
+
+package eventarc
+
+import (
+	"context"
+	"fmt"
+
+	gcp "cloud.google.com/go/eventarc/apiv1"
+	pb "cloud.google.com/go/eventarc/apiv1/eventarcpb"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/eventarc/v1alpha1"
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
+)
+
+func init() {
+	registry.RegisterModel(krm.EventarcGoogleChannelConfigGVK, NewGoogleChannelConfigModel)
+}
+
+func NewGoogleChannelConfigModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &googleChannelConfigModel{config: *config}, nil
+}
+
+var _ directbase.Model = &googleChannelConfigModel{}
+
+type googleChannelConfigModel struct {
+	config config.ControllerConfig
+}
+
+func (m *googleChannelConfigModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.EventarcGoogleChannelConfig{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewGoogleChannelConfigIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get eventarc GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	eventarcClient, err := gcpClient.newEventarcClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &googleChannelConfigAdapter{
+		gcpClient: eventarcClient,
+		id:        id,
+		desired:   obj,
+		reader:    reader,
+	}, nil
+}
+
+func (m *googleChannelConfigModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type googleChannelConfigAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.GoogleChannelConfigIdentity
+	desired   *krm.EventarcGoogleChannelConfig
+	actual    *pb.GoogleChannelConfig
+	reader    client.Reader
+}
+
+var _ directbase.Adapter = &googleChannelConfigAdapter{}
+
+func (a *googleChannelConfigAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting eventarc googlechannelconfig", "name", a.id)
+
+	req := &pb.GetGoogleChannelConfigRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetGoogleChannelConfig(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting eventarc googlechannelconfig %q: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	// GoogleChannelConfig is a singleton resource that cannot be created.
+	// It can only be fetched and updated.
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating eventarc googlechannelconfig is a no-op", "name", a.id)
+	return nil
+}
+
+// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating eventarc googlechannelconfig", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	if err := a.normalizeReferenceFields(ctx); err != nil {
+		return err
+	}
+
+	desired := a.desired.DeepCopy()
+	resource := EventarcGoogleChannelConfigSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	paths = append(paths, "crypto_key_name")
path: pkg/controller/direct/eventarc/googlechannelconfig_controller.go
user: login: barney-s
body: More so with removing the reference is an update. removing key ref is the only update.
created_at: 2025-04-15T22:03:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,67 @@
+GET https://eventarc.googleapis.com/v1/projects/${projectId}/locations/us-central1/googleChannelConfig?%24alt=json%3Benum-encoding%3Dint
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: name=projects%2F${projectId}%2Flocations%2Fus-central1%2FgoogleChannelConfig
+
+200 OK
+Content-Type: application/json; charset=UTF-8
+Server: ESF
+Vary: Origin
+Vary: X-Origin
+Vary: Referer
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+X-Xss-Protection: 0
+
+{
+  "name": "projects/${projectId}/locations/us-central1/googleChannelConfig",
path: pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_http.log
user: login: barney-s
body: its a singleton. always exists in the cloud.,
created_at: 2025-04-15T22:38:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,23 @@
+# 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.
+
+apiVersion: eventarc.cnrm.cloud.google.com/v1alpha1
path: pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/update.yaml
user: login: jingyih
body: Could you try remove the update.yaml and see what happens? I think the test should still work. 
created_at: 2025-04-15T22:41:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,218 @@
+// 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:controller
+// proto.service: google.cloud.eventarc.v1.Eventarc
+// proto.message: google.cloud.eventarc.v1.GoogleChannelConfig
+// crd.type: EventarcGoogleChannelConfig
+// crd.version: v1alpha1
+
+package eventarc
+
+import (
+	"context"
+	"fmt"
+
+	gcp "cloud.google.com/go/eventarc/apiv1"
+	pb "cloud.google.com/go/eventarc/apiv1/eventarcpb"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/eventarc/v1alpha1"
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
+)
+
+func init() {
+	registry.RegisterModel(krm.EventarcGoogleChannelConfigGVK, NewGoogleChannelConfigModel)
+}
+
+func NewGoogleChannelConfigModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &googleChannelConfigModel{config: *config}, nil
+}
+
+var _ directbase.Model = &googleChannelConfigModel{}
+
+type googleChannelConfigModel struct {
+	config config.ControllerConfig
+}
+
+func (m *googleChannelConfigModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.EventarcGoogleChannelConfig{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewGoogleChannelConfigIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get eventarc GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	eventarcClient, err := gcpClient.newEventarcClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &googleChannelConfigAdapter{
+		gcpClient: eventarcClient,
+		id:        id,
+		desired:   obj,
+		reader:    reader,
+	}, nil
+}
+
+func (m *googleChannelConfigModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type googleChannelConfigAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.GoogleChannelConfigIdentity
+	desired   *krm.EventarcGoogleChannelConfig
+	actual    *pb.GoogleChannelConfig
+	reader    client.Reader
+}
+
+var _ directbase.Adapter = &googleChannelConfigAdapter{}
+
+func (a *googleChannelConfigAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting eventarc googlechannelconfig", "name", a.id)
+
+	req := &pb.GetGoogleChannelConfigRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetGoogleChannelConfig(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting eventarc googlechannelconfig %q: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	// GoogleChannelConfig is a singleton resource that cannot be created.
+	// It can only be fetched and updated.
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating eventarc googlechannelconfig is a no-op", "name", a.id)
+	return nil
+}
+
+// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating eventarc googlechannelconfig", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	if err := a.normalizeReferenceFields(ctx); err != nil {
+		return err
+	}
+
+	desired := a.desired.DeepCopy()
+	resource := EventarcGoogleChannelConfigSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	paths = append(paths, "crypto_key_name")
path: pkg/controller/direct/eventarc/googlechannelconfig_controller.go
user: login: jingyih
body: I think we need to compare desired vs. actual and skip the update if there is no diff in this field? (assuming this field is the only field that is updatable)
created_at: 2025-04-15T22:42:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+GET https://eventarc.googleapis.com/v1/projects/${projectId}/locations/us-central1/googleChannelConfig?%24alt=json%3Benum-encoding%3Dint
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: name=projects%2F${projectId}%2Flocations%2Fus-central1%2FgoogleChannelConfig
+
+200 OK
+Content-Type: application/json; charset=UTF-8
+Server: ESF
+Vary: Origin
+Vary: X-Origin
+Vary: Referer
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+X-Xss-Protection: 0
+
+{
+  "name": "projects/${projectId}/locations/us-central1/googleChannelConfig",
path: pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/_http.log
user: login: jingyih
body: I see. Thanks!
created_at: 2025-04-15T22:43:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+# 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.
+
+apiVersion: eventarc.cnrm.cloud.google.com/v1alpha1
path: pkg/test/resourcefixture/testdata/basic/eventarc/v1alpha1/eventarcgooglechannelconfig/eventarcgooglechannelconfig-minimal/update.yaml
user: login: barney-s
body: done. works. 

hack/compare-mock fixtures/eventarcgooglechannelconfig-minimal    shows no diff. 
created_at: 2025-04-16T06:57:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,218 @@
+// 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:controller
+// proto.service: google.cloud.eventarc.v1.Eventarc
+// proto.message: google.cloud.eventarc.v1.GoogleChannelConfig
+// crd.type: EventarcGoogleChannelConfig
+// crd.version: v1alpha1
+
+package eventarc
+
+import (
+	"context"
+	"fmt"
+
+	gcp "cloud.google.com/go/eventarc/apiv1"
+	pb "cloud.google.com/go/eventarc/apiv1/eventarcpb"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/eventarc/v1alpha1"
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
+	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
+)
+
+func init() {
+	registry.RegisterModel(krm.EventarcGoogleChannelConfigGVK, NewGoogleChannelConfigModel)
+}
+
+func NewGoogleChannelConfigModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &googleChannelConfigModel{config: *config}, nil
+}
+
+var _ directbase.Model = &googleChannelConfigModel{}
+
+type googleChannelConfigModel struct {
+	config config.ControllerConfig
+}
+
+func (m *googleChannelConfigModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.EventarcGoogleChannelConfig{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewGoogleChannelConfigIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get eventarc GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	eventarcClient, err := gcpClient.newEventarcClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &googleChannelConfigAdapter{
+		gcpClient: eventarcClient,
+		id:        id,
+		desired:   obj,
+		reader:    reader,
+	}, nil
+}
+
+func (m *googleChannelConfigModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type googleChannelConfigAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.GoogleChannelConfigIdentity
+	desired   *krm.EventarcGoogleChannelConfig
+	actual    *pb.GoogleChannelConfig
+	reader    client.Reader
+}
+
+var _ directbase.Adapter = &googleChannelConfigAdapter{}
+
+func (a *googleChannelConfigAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting eventarc googlechannelconfig", "name", a.id)
+
+	req := &pb.GetGoogleChannelConfigRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetGoogleChannelConfig(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting eventarc googlechannelconfig %q: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	// GoogleChannelConfig is a singleton resource that cannot be created.
+	// It can only be fetched and updated.
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating eventarc googlechannelconfig is a no-op", "name", a.id)
+	return nil
+}
+
+// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
+func (a *googleChannelConfigAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating eventarc googlechannelconfig", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	if err := a.normalizeReferenceFields(ctx); err != nil {
+		return err
+	}
+
+	desired := a.desired.DeepCopy()
+	resource := EventarcGoogleChannelConfigSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	paths = append(paths, "crypto_key_name")
path: pkg/controller/direct/eventarc/googlechannelconfig_controller.go
user: login: barney-s
body: done.
created_at: 2025-04-16T16:57:52Z
author_association: COLLABORATOR

PR comments:
user: login: google-oss-prow[bot]
created_at: 2025-04-15T19:40:30Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/GoogleCloudPlatform/k8s-config-connector/pull/4364#pullrequestreview-2769577355" title="Approved">jingyih</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)~~ [jingyih]

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: yuwenma
created_at: 2025-04-16T16:58:11Z
body: /lgtm
author_association: CONTRIBUTOR