Repository: GoogleCloudPlatform/k8s-config-connector
Pull request #4163: feat: add controller, test and mock support for VMwareEnginePrivateCloud
Author: login: jingyih
Labels: name: lgtm
name: approved
Created: 2025-03-23T17:36:52Z | Merged: 2025-03-24T22:11:32Z

The resource takes a few hours to be created and takes a week to be deleted (scheduled deletion).  

I had to modify some hardcoded timeouts in the test to get the resource creation to finish.

 - Wait for ready timeout
https://github.com/GoogleCloudPlatform/k8s-config-connector/blob/b932c0a671f9380ce64704a6113b7fb692e40c76/config/tests/samples/create/samples.go#L48

 - Wait for ready poll interval (avoid log spam)
https://github.com/GoogleCloudPlatform/k8s-config-connector/blob/b932c0a671f9380ce64704a6113b7fb692e40c76/config/tests/samples/create/samples.go#L182

 - Reconcile timeout
 https://github.com/GoogleCloudPlatform/k8s-config-connector/blob/b932c0a671f9380ce64704a6113b7fb692e40c76/pkg/controller/direct/directbase/directbase_controller.go#L159

 - Per test timeout
https://github.com/GoogleCloudPlatform/k8s-config-connector/blob/b932c0a671f9380ce64704a6113b7fb692e40c76/tests/e2e/unified_test.go#L145


Diff:
diff --git a/config/tests/samples/create/harness.go b/config/tests/samples/create/harness.go
index b181ba1c18c..2daf470a0d6 100644
--- a/config/tests/samples/create/harness.go
+++ b/config/tests/samples/create/harness.go
@@ -943,6 +943,7 @@ func MaybeSkip(t *testing.T, name string, resources []*unstructured.Unstructured
 
 			case schema.GroupKind{Group: "vmwareengine.cnrm.cloud.google.com", Kind: "VMwareEngineNetwork"}:
 			case schema.GroupKind{Group: "vmwareengine.cnrm.cloud.google.com", Kind: "VMwareEngineNetworkPolicy"}:
+			case schema.GroupKind{Group: "vmwareengine.cnrm.cloud.google.com", Kind: "VMwareEnginePrivateCloud"}:
 
 			case schema.GroupKind{Group: "vpcaccess.cnrm.cloud.google.com", Kind: "VPCAccessConnector"}:
 
diff --git a/mockgcp/mockvmwareengine/privatecloud.go b/mockgcp/mockvmwareengine/privatecloud.go
new file mode 100755
index 00000000000..4dbf693a87c
--- /dev/null
+++ b/mockgcp/mockvmwareengine/privatecloud.go
@@ -0,0 +1,239 @@
+// 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:mockgcp-support
+// proto.service: google.cloud.vmwareengine.v1.VmwareEngine
+// proto.message: google.cloud.vmwareengine.v1.PrivateCloud
+
+package mockvmwareengine
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+	"google.golang.org/protobuf/proto"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects"
+	pb "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/mockgcp/cloud/vmwareengine/v1"
+	longrunningpb "google.golang.org/genproto/googleapis/longrunning"
+)
+
+func (s *VMwareEngineV1) GetPrivateCloud(ctx context.Context, req *pb.GetPrivateCloudRequest) (*pb.PrivateCloud, error) {
+	name, err := s.parsePrivateCloudName(req.Name)
+	if err != nil {
+		return nil, err
+	}
+
+	fqn := name.String()
+
+	obj := &pb.PrivateCloud{}
+	if err := s.storage.Get(ctx, fqn, obj); err != nil {
+		if status.Code(err) == codes.NotFound {
+			return nil, status.Errorf(codes.NotFound, "Resource '%s' was not found", fqn)
+		}
+		return nil, err
+	}
+
+	return obj, nil
+}
+
+func (s *VMwareEngineV1) CreatePrivateCloud(ctx context.Context, req *pb.CreatePrivateCloudRequest) (*longrunningpb.Operation, error) {
+	reqName := req.Parent + "/privateClouds/" + req.PrivateCloudId
+	name, err := s.parsePrivateCloudName(reqName)
+	if err != nil {
+		return nil, err
+	}
+
+	fqn := name.String()
+	now := time.Now()
+
+	obj := proto.Clone(req.GetPrivateCloud()).(*pb.PrivateCloud)
+	obj.Name = fqn
+	obj.CreateTime = timestamppb.New(now)
+	obj.State = pb.PrivateCloud_CREATING
+	obj.Uid = "111111111111111111111"
+	obj.ManagementCluster = nil // this field is not readable from GCP
+
+	setGeneratedFields(obj, name)
+
+	if err := s.storage.Create(ctx, fqn, obj); err != nil {
+		return nil, err
+	}
+
+	prefix := fmt.Sprintf("projects/%s/locations/%s", name.Project.ID, name.Location)
+	metadata := &pb.OperationMetadata{
+		ApiVersion: "v1",
+		CreateTime: timestamppb.New(now),
+		Target:     name.String(),
+		Verb:       "create",
+	}
+	return s.operations.StartLRO(ctx, prefix, metadata, func() (proto.Message, error) {
+		metadata.EndTime = timestamppb.New(now)
+		obj.UpdateTime = timestamppb.New(now)
+		obj.State = pb.PrivateCloud_ACTIVE
+		if err := s.storage.Update(ctx, fqn, obj); err != nil {
+			return nil, err
+		}
+		return obj, nil
+	})
+}
+
+func (s *VMwareEngineV1) UpdatePrivateCloud(ctx context.Context, req *pb.UpdatePrivateCloudRequest) (*longrunningpb.Operation, error) {
+	name, err := s.parsePrivateCloudName(req.GetPrivateCloud().GetName())
+	if err != nil {
+		return nil, err
+	}
+
+	fqn := name.String()
+	now := time.Now()
+
+	obj := &pb.PrivateCloud{}
+	if err := s.storage.Get(ctx, fqn, obj); err != nil {
+		return nil, err
+	}
+	paths := req.GetUpdateMask().GetPaths()
+	if len(paths) == 0 {
+		return nil, status.Errorf(codes.InvalidArgument, "update_mask must be provided")
+	}
+
+	for _, path := range paths {
+		switch path {
+		case "description":
+			obj.Description = req.GetPrivateCloud().Description
+		default:
+			return nil, status.Errorf(codes.InvalidArgument, "update_mask path %q not valid", path)
+		}
+	}
+
+	obj.UpdateTime = timestamppb.New(now)
+
+	if err := s.storage.Update(ctx, fqn, obj); err != nil {
+		return nil, err
+	}
+
+	prefix := fmt.Sprintf("projects/%s/locations/%s", name.Project.ID, name.Location)
+	metadata := &pb.OperationMetadata{
+		ApiVersion: "v1",
+		CreateTime: timestamppb.New(now),
+		Target:     name.String(),
+		Verb:       "update",
+	}
+	return s.operations.StartLRO(ctx, prefix, metadata, func() (proto.Message, error) {
+		metadata.EndTime = timestamppb.New(now)
+		return obj, nil
+	})
+}
+
+func (s *VMwareEngineV1) DeletePrivateCloud(ctx context.Context, req *pb.DeletePrivateCloudRequest) (*longrunningpb.Operation, error) {
+	name, err := s.parsePrivateCloudName(req.Name)
+	if err != nil {
+		return nil, err
+	}
+
+	fqn := name.String()
+
+	obj := &pb.PrivateCloud{}
+	if err := s.storage.Get(ctx, fqn, obj); err != nil {
+		return nil, err
+	}
+
+	now := time.Now()
+	obj.State = pb.PrivateCloud_DELETED
+	obj.DeleteTime = timestamppb.New(now)
+	obj.ExpireTime = timestamppb.New(now)
+	obj.UpdateTime = timestamppb.New(now)
+
+	if err := s.storage.Update(ctx, fqn, obj); err != nil {
+		return nil, err
+	}
+
+	prefix := fmt.Sprintf("projects/%s/locations/%s", name.Project.ID, name.Location)
+	metadata := &pb.OperationMetadata{
+		CreateTime: timestamppb.New(now),
+		Target:     fqn,
+		ApiVersion: "v1",
+		Verb:       "update", // update to be scheduled for deletion
+	}
+	return s.operations.StartLRO(ctx, prefix, metadata, func() (proto.Message, error) {
+		metadata.EndTime = timestamppb.New(now)
+		return obj, nil
+	})
+}
+
+type privateCloudName struct {
+	Project        *projects.ProjectData
+	Location       string
+	PrivateCloudID string
+}
+
+func (n *privateCloudName) String() string {
+	return fmt.Sprintf("projects/%s/locations/%s/privateClouds/%s", n.Project.ID, n.Location, n.PrivateCloudID)
+}
+
+// parsePrivateCloudName parses a string into a privateCloudName.
+// The expected form is `projects/*/locations/*/privateClouds/*`.
+func (s *MockService) parsePrivateCloudName(name string) (*privateCloudName, error) {
+	tokens := strings.Split(name, "/")
+
+	if len(tokens) == 6 && tokens[0] == "projects" && tokens[2] == "locations" && tokens[4] == "privateClouds" {
+		project, err := s.Projects.GetProjectByID(tokens[1])
+		if err != nil {
+			return nil, err
+		}
+
+		name := &privateCloudName{
+			Project:        project,
+			Location:       tokens[3],
+			PrivateCloudID: tokens[5],
+		}
+
+		return name, nil
+	}
+
+	return nil, status.Errorf(codes.InvalidArgument, "name %q is not valid", name)
+}
+
+func setGeneratedFields(obj *pb.PrivateCloud, name *privateCloudName) {
+	obj.Hcx = &pb.Hcx{
+		Fqdn:       "hcx-414861.c8819727.us-west2.gve.goog",
+		InternalIp: "192.168.30.3",
+		State:      pb.Hcx_ACTIVE,
+		Version:    "4.10.3.24447633",
+	}
+	obj.Nsx = &pb.Nsx{
+		Fqdn:       "nsx-414860.c8819727.us-west2.gve.goog",
+		InternalIp: "192.168.30.18",
+		State:      pb.Nsx_ACTIVE,
+		Version:    "3.2.3.1",
+	}
+	obj.Vcenter = &pb.Vcenter{
+		Fqdn:       "vcsa-359395.c8819727.us-west2.gve.goog",
+		InternalIp: "192.168.30.2",
+		State:      pb.Vcenter_ACTIVE,
+		Version:    "7.0.3.23085514",
+	}
+	if obj.NetworkConfig != nil {
+		obj.NetworkConfig.DnsServerIp = "192.168.30.234"
+		obj.NetworkConfig.ManagementIpAddressLayoutVersion = 2
+		obj.NetworkConfig.VmwareEngineNetworkCanonical = obj.NetworkConfig.VmwareEngineNetwork
+		// change from project ID to project number
+		obj.NetworkConfig.VmwareEngineNetworkCanonical = strings.Replace(obj.NetworkConfig.VmwareEngineNetworkCanonical, "projects/"+name.Project.ID+"/locations/", "projects/"+strconv.FormatInt(name.Project.Number, 10)+"/locations/", 1)
+	}
+}
diff --git a/pkg/controller/direct/vmwareengine/privatecloud_controller.go b/pkg/controller/direct/vmwareengine/privatecloud_controller.go
new file mode 100644
index 00000000000..f70db0f9788
--- /dev/null
+++ b/pkg/controller/direct/vmwareengine/privatecloud_controller.go
@@ -0,0 +1,250 @@
+// 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.vmwareengine.v1.VmwareEngine
+// proto.message: google.cloud.vmwareengine.v1.PrivateCloud
+// crd.type: VMwareEnginePrivateCloud
+// crd.version: v1alpha1
+
+package vmwareengine
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	gcp "cloud.google.com/go/vmwareengine/apiv1"
+	pb "cloud.google.com/go/vmwareengine/apiv1/vmwareenginepb"
+	"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"
+
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/vmwareengine/v1alpha1"
+	"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.VMwareEnginePrivateCloudGVK, NewPrivateCloudModel)
+}
+
+func NewPrivateCloudModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &privateCloudModel{config: *config}, nil
+}
+
+var _ directbase.Model = &privateCloudModel{}
+
+type privateCloudModel struct {
+	config config.ControllerConfig
+}
+
+func (m *privateCloudModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.VMwareEnginePrivateCloud{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewPrivateCloudIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// normalize reference fields
+	if obj.Spec.NetworkConfig != nil && obj.Spec.NetworkConfig.VMwareEngineNetworkRef != nil {
+		if _, err := obj.Spec.NetworkConfig.VMwareEngineNetworkRef.NormalizedExternal(ctx, reader, obj.GetNamespace()); err != nil {
+			return nil, err
+		}
+	}
+
+	// Get VMwareEngine GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	client, err := gcpClient.newClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &privateCloudAdapter{
+		gcpClient: client,
+		id:        id,
+		desired:   obj,
+	}, nil
+}
+
+func (m *privateCloudModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type privateCloudAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.PrivateCloudIdentity
+	desired   *krm.VMwareEnginePrivateCloud
+	actual    *pb.PrivateCloud
+}
+
+var _ directbase.Adapter = &privateCloudAdapter{}
+
+func (a *privateCloudAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting vmwareengine private cloud", "name", a.id)
+
+	req := &pb.GetPrivateCloudRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetPrivateCloud(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting vmwareengine private cloud %q from gcp: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+func (a *privateCloudAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating vmwareengine private cloud", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	desired := a.desired.DeepCopy()
+	resource := VMwareEnginePrivateCloudSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	req := &pb.CreatePrivateCloudRequest{
+		Parent:         a.id.Parent().String(),
+		PrivateCloudId: a.id.ID(),
+		PrivateCloud:   resource,
+	}
+	op, err := a.gcpClient.CreatePrivateCloud(ctx, req)
+	if err != nil {
+		return fmt.Errorf("creating vmwareengine private cloud %s: %w", a.id.String(), err)
+	}
+	created, err := op.Wait(ctx)
+	if err != nil {
+		return fmt.Errorf("vmwareengine private cloud %s waiting creation: %w", a.id, err)
+	}
+	log.V(2).Info("successfully created vmwareengine private cloud in gcp", "name", a.id)
+
+	status := &krm.VMwareEnginePrivateCloudStatus{}
+	status.ObservedState = VMwareEnginePrivateCloudObservedState_FromProto(mapCtx, created)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+	status.ExternalRef = direct.LazyPtr(a.id.String())
+	return createOp.UpdateStatus(ctx, status, nil)
+}
+
+func (a *privateCloudAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating vmwareengine private cloud", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	desired := a.desired.DeepCopy()
+	resource := VMwareEnginePrivateCloudSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	if desired.Spec.Description != nil && !reflect.DeepEqual(resource.Description, a.actual.Description) {
+		paths = append(paths, "description")
+	}
+	// TODO: spec.managementCluster is not readable from GCP
+	// TODO: spec.networkConfig is has generated fields from GCP
+
+	if len(paths) == 0 {
+		log.V(2).Info("no field needs update", "name", a.id)
+		return nil
+	}
+
+	resource.Name = a.id.String() // we need to set the name so that GCP API can identify the resource
+	req := &pb.UpdatePrivateCloudRequest{
+		PrivateCloud: resource,
+		UpdateMask:   &fieldmaskpb.FieldMask{Paths: paths},
+	}
+	op, err := a.gcpClient.UpdatePrivateCloud(ctx, req)
+	if err != nil {
+		return fmt.Errorf("updating vmwareengine private cloud %s: %w", a.id.String(), err)
+	}
+	updated, err := op.Wait(ctx)
+	if err != nil {
+		return fmt.Errorf("vmwareengine private cloud %s waiting for update: %w", a.id, err)
+	}
+	log.V(2).Info("successfully updated vmwareengine private cloud", "name", a.id)
+
+	status := &krm.VMwareEnginePrivateCloudStatus{}
+	status.ObservedState = VMwareEnginePrivateCloudObservedState_FromProto(mapCtx, updated)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+	return updateOp.UpdateStatus(ctx, status, nil)
+}
+
+func (a *privateCloudAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) {
+	if a.actual == nil {
+		return nil, fmt.Errorf("Find() not called")
+	}
+	u := &unstructured.Unstructured{}
+
+	obj := &krm.VMwareEnginePrivateCloud{}
+	mapCtx := &direct.MapContext{}
+	obj.Spec = direct.ValueOf(VMwareEnginePrivateCloudSpec_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
+	}
+
+	u.SetName(a.actual.Name)
+	u.SetGroupVersionKind(krm.VMwareEnginePrivateCloudGVK)
+	u.Object = uObj
+	return u, nil
+}
+
+// Delete implements the Adapter interface.
+func (a *privateCloudAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("deleting vmwareengine private cloud", "name", a.id)
+
+	req := &pb.DeletePrivateCloudRequest{Name: a.id.String()}
+	op, err := a.gcpClient.DeletePrivateCloud(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			log.V(2).Info("skipping delete for non-existent BackupVault, assuming it was already deleted", "name", a.id)
+			return true, nil
+		}
+		return false, fmt.Errorf("deleting vmwareengine private cloud %s: %w", a.id.String(), err)
+	}
+	log.V(2).Info("successfully deleted vmwareengine private cloud", "name", a.id)
+
+	_, err = op.Wait(ctx)
+	if err != nil {
+		return false, fmt.Errorf("waiting delete BackupVault %s: %w", a.id, err)
+	}
+	return true, nil
+}
diff --git a/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/_generated_object_vmwareengineprivatecloud-minimal.golden.yaml b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/_generated_object_vmwareengineprivatecloud-minimal.golden.yaml
new file mode 100644
index 00000000000..fe38e2364d2
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/_generated_object_vmwareengineprivatecloud-minimal.golden.yaml
@@ -0,0 +1,58 @@
+apiVersion: vmwareengine.cnrm.cloud.google.com/v1alpha1
+kind: VMwareEnginePrivateCloud
+metadata:
+  finalizers:
+  - cnrm.cloud.google.com/finalizer
+  - cnrm.cloud.google.com/deletion-defender
+  generation: 2
+  labels:
+    cnrm-test: "true"
+  name: vmwareengineprivatecloud-minimal-${uniqueId}
+  namespace: ${uniqueId}
+spec:
+  description: Updated description
+  location: us-west2-a
+  managementCluster:
+    clusterID: cluster-1
+    nodeTypeConfigs:
+    - nodeCount: 3
+      nodeTypeID: standard-72
+  networkConfig:
+    managementCIDR: 192.168.30.0/24
+    vmwareEngineNetworkRef:
+      name: vmwareenginenetwork-minimal-${uniqueId}
+  projectRef:
+    external: ${projectId}
+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-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}
+  observedGeneration: 2
+  observedState:
+    createTime: "1970-01-01T00:00:00Z"
+    hcx:
+      fqdn: hcx-414861.c8819727.us-west2.gve.goog
+      internalIP: 192.168.30.3
+      state: ACTIVE
+      version: 4.10.3.24447633
+    networkConfig:
+      dnsServerIP: 192.168.30.234
+      managementIPAddressLayoutVersion: 2
+      vmwareEngineNetworkCanonical: projects/${projectNumber}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}
+    nsx:
+      fqdn: nsx-414860.c8819727.us-west2.gve.goog
+      internalIP: 192.168.30.18
+      state: ACTIVE
+      version: 3.2.3.1
+    state: ACTIVE
+    uid: 0123456789abcdef
+    updateTime: "1970-01-01T00:00:00Z"
+    vcenter:
+      fqdn: vcsa-359395.c8819727.us-west2.gve.goog
+      internalIP: 192.168.30.2
+      state: ACTIVE
+      version: 7.0.3.23085514
diff --git a/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/_http.log b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/_http.log
new file mode 100644
index 00000000000..2ff3ec5a614
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/_http.log
@@ -0,0 +1,651 @@
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}?%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%2Fglobal%2FvmwareEngineNetworks%2Fvmwareenginenetwork-minimal-${uniqueId}
+
+404 Not Found
+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
+
+{
+  "error": {
+    "code": 404,
+    "message": "Resource 'projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}' was not found",
+    "status": "NOT_FOUND"
+  }
+}
+
+---
+
+POST https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/global/vmwareEngineNetworks?%24alt=json%3Benum-encoding%3Dint&vmwareEngineNetworkId=vmwareenginenetwork-minimal-${uniqueId}
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: parent=projects%2F${projectId}%2Flocations%2Fglobal
+
+{
+  "description": "test dependency vmwareengine network",
+  "type": 2
+}
+
+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
+
+{
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+    "verb": "create"
+  },
+  "name": "projects/${projectId}/locations/global/operations/${operationID}"
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/global/operations/${operationID}
+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%2Fglobal%2Foperations%2F${operationID}
+
+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
+
+{
+  "done": true,
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "endTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+    "verb": "create"
+  },
+  "name": "projects/${projectId}/locations/global/operations/${operationID}",
+  "response": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.VmwareEngineNetwork",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "description": "test dependency vmwareengine network",
+    "etag": "abcdef0123A=",
+    "name": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+    "state": "ACTIVE",
+    "type": "STANDARD",
+    "uid": "111111111111111111111",
+    "updateTime": "2024-04-01T12:34:56.123456Z",
+    "vpcNetworks": [
+      {
+        "network": "projects/b3e854f0b4bedfea6-tp/global/networks/${networkId}",
+        "type": "INTERNET"
+      },
+      {
+        "network": "projects/b3e854f0b4bedfea6-tp/global/networks/${networkId}",
+        "type": "INTRANET"
+      },
+      {
+        "network": "projects/b3e854f0b4bedfea6-tp/global/networks/${networkId}",
+        "type": "GOOGLE_CLOUD"
+      }
+    ]
+  }
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}?%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-west2-a%2FprivateClouds%2Fvmwareengineprivatecloud-minimal-${uniqueId}
+
+404 Not Found
+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
+
+{
+  "error": {
+    "code": 404,
+    "message": "Resource 'projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}' was not found",
+    "status": "NOT_FOUND"
+  }
+}
+
+---
+
+POST https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/privateClouds?%24alt=json%3Benum-encoding%3Dint&privateCloudId=vmwareengineprivatecloud-minimal-${uniqueId}
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: parent=projects%2F${projectId}%2Flocations%2Fus-west2-a
+
+{
+  "description": "Initial description",
+  "managementCluster": {
+    "clusterId": "cluster-1",
+    "nodeTypeConfigs": {
+      "standard-72": {
+        "nodeCount": 3
+      }
+    }
+  },
+  "networkConfig": {
+    "managementCidr": "192.168.30.0/24",
+    "vmwareEngineNetwork": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}"
+  }
+}
+
+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
+
+{
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "verb": "create"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/operations/${operationID}"
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/operations/${operationID}
+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-west2-a%2Foperations%2F${operationID}
+
+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
+
+{
+  "done": true,
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "endTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "verb": "create"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/operations/${operationID}",
+  "response": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.PrivateCloud",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "description": "Initial description",
+    "hcx": {
+      "fqdn": "hcx-414861.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.3",
+      "state": "ACTIVE",
+      "version": "4.10.3.24447633"
+    },
+    "name": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "networkConfig": {
+      "dnsServerIp": "192.168.30.234",
+      "managementCidr": "192.168.30.0/24",
+      "managementIpAddressLayoutVersion": 2,
+      "vmwareEngineNetwork": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+      "vmwareEngineNetworkCanonical": "projects/${projectNumber}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}"
+    },
+    "nsx": {
+      "fqdn": "nsx-414860.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.18",
+      "state": "ACTIVE",
+      "version": "3.2.3.1"
+    },
+    "state": "ACTIVE",
+    "uid": "111111111111111111111",
+    "updateTime": "2024-04-01T12:34:56.123456Z",
+    "vcenter": {
+      "fqdn": "vcsa-359395.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.2",
+      "state": "ACTIVE",
+      "version": "7.0.3.23085514"
+    }
+  }
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}?%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-west2-a%2FprivateClouds%2Fvmwareengineprivatecloud-minimal-${uniqueId}
+
+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
+
+{
+  "createTime": "2024-04-01T12:34:56.123456Z",
+  "description": "Initial description",
+  "hcx": {
+    "fqdn": "hcx-414861.c8819727.us-west2.gve.goog",
+    "internalIp": "192.168.30.3",
+    "state": 1,
+    "version": "4.10.3.24447633"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+  "networkConfig": {
+    "dnsServerIp": "192.168.30.234",
+    "managementCidr": "192.168.30.0/24",
+    "managementIpAddressLayoutVersion": 2,
+    "vmwareEngineNetwork": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+    "vmwareEngineNetworkCanonical": "projects/${projectNumber}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}"
+  },
+  "nsx": {
+    "fqdn": "nsx-414860.c8819727.us-west2.gve.goog",
+    "internalIp": "192.168.30.18",
+    "state": 1,
+    "version": "3.2.3.1"
+  },
+  "state": 1,
+  "uid": "111111111111111111111",
+  "updateTime": "2024-04-01T12:34:56.123456Z",
+  "vcenter": {
+    "fqdn": "vcsa-359395.c8819727.us-west2.gve.goog",
+    "internalIp": "192.168.30.2",
+    "state": 1,
+    "version": "7.0.3.23085514"
+  }
+}
+
+---
+
+PATCH https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}?%24alt=json%3Benum-encoding%3Dint&updateMask=description
+Content-Type: application/json
+User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
+X-Goog-Request-Params: private_cloud.name=projects%2F${projectId}%2Flocations%2Fus-west2-a%2FprivateClouds%2Fvmwareengineprivatecloud-minimal-${uniqueId}
+
+{
+  "description": "Updated description",
+  "managementCluster": {
+    "clusterId": "cluster-1",
+    "nodeTypeConfigs": {
+      "standard-72": {
+        "nodeCount": 3
+      }
+    }
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+  "networkConfig": {
+    "managementCidr": "192.168.30.0/24",
+    "vmwareEngineNetwork": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}"
+  }
+}
+
+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
+
+{
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "verb": "update"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/operations/${operationID}"
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/operations/${operationID}
+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-west2-a%2Foperations%2F${operationID}
+
+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
+
+{
+  "done": true,
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "endTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "verb": "update"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/operations/${operationID}",
+  "response": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.PrivateCloud",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "description": "Updated description",
+    "hcx": {
+      "fqdn": "hcx-414861.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.3",
+      "state": "ACTIVE",
+      "version": "4.10.3.24447633"
+    },
+    "name": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "networkConfig": {
+      "dnsServerIp": "192.168.30.234",
+      "managementCidr": "192.168.30.0/24",
+      "managementIpAddressLayoutVersion": 2,
+      "vmwareEngineNetwork": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+      "vmwareEngineNetworkCanonical": "projects/${projectNumber}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}"
+    },
+    "nsx": {
+      "fqdn": "nsx-414860.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.18",
+      "state": "ACTIVE",
+      "version": "3.2.3.1"
+    },
+    "state": "ACTIVE",
+    "uid": "111111111111111111111",
+    "updateTime": "2024-04-01T12:34:56.123456Z",
+    "vcenter": {
+      "fqdn": "vcsa-359395.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.2",
+      "state": "ACTIVE",
+      "version": "7.0.3.23085514"
+    }
+  }
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}?%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-west2-a%2FprivateClouds%2Fvmwareengineprivatecloud-minimal-${uniqueId}
+
+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
+
+{
+  "createTime": "2024-04-01T12:34:56.123456Z",
+  "description": "Updated description",
+  "hcx": {
+    "fqdn": "hcx-414861.c8819727.us-west2.gve.goog",
+    "internalIp": "192.168.30.3",
+    "state": 1,
+    "version": "4.10.3.24447633"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+  "networkConfig": {
+    "dnsServerIp": "192.168.30.234",
+    "managementCidr": "192.168.30.0/24",
+    "managementIpAddressLayoutVersion": 2,
+    "vmwareEngineNetwork": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+    "vmwareEngineNetworkCanonical": "projects/${projectNumber}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}"
+  },
+  "nsx": {
+    "fqdn": "nsx-414860.c8819727.us-west2.gve.goog",
+    "internalIp": "192.168.30.18",
+    "state": 1,
+    "version": "3.2.3.1"
+  },
+  "state": 1,
+  "uid": "111111111111111111111",
+  "updateTime": "2024-04-01T12:34:56.123456Z",
+  "vcenter": {
+    "fqdn": "vcsa-359395.c8819727.us-west2.gve.goog",
+    "internalIp": "192.168.30.2",
+    "state": 1,
+    "version": "7.0.3.23085514"
+  }
+}
+
+---
+
+DELETE https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}?%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-west2-a%2FprivateClouds%2Fvmwareengineprivatecloud-minimal-${uniqueId}
+
+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
+
+{
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "verb": "update"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/operations/${operationID}"
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/us-west2-a/operations/${operationID}
+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-west2-a%2Foperations%2F${operationID}
+
+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
+
+{
+  "done": true,
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "endTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "verb": "update"
+  },
+  "name": "projects/${projectId}/locations/us-west2-a/operations/${operationID}",
+  "response": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.PrivateCloud",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "deleteTime": "2024-04-01T12:34:56.123456Z",
+    "description": "Updated description",
+    "expireTime": "2024-04-01T12:34:56.123456Z",
+    "hcx": {
+      "fqdn": "hcx-414861.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.3",
+      "state": "ACTIVE",
+      "version": "4.10.3.24447633"
+    },
+    "name": "projects/${projectId}/locations/us-west2-a/privateClouds/vmwareengineprivatecloud-minimal-${uniqueId}",
+    "networkConfig": {
+      "dnsServerIp": "192.168.30.234",
+      "managementCidr": "192.168.30.0/24",
+      "managementIpAddressLayoutVersion": 2,
+      "vmwareEngineNetwork": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+      "vmwareEngineNetworkCanonical": "projects/${projectNumber}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}"
+    },
+    "nsx": {
+      "fqdn": "nsx-414860.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.18",
+      "state": "ACTIVE",
+      "version": "3.2.3.1"
+    },
+    "state": "DELETED",
+    "uid": "111111111111111111111",
+    "updateTime": "2024-04-01T12:34:56.123456Z",
+    "vcenter": {
+      "fqdn": "vcsa-359395.c8819727.us-west2.gve.goog",
+      "internalIp": "192.168.30.2",
+      "state": "ACTIVE",
+      "version": "7.0.3.23085514"
+    }
+  }
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}?%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%2Fglobal%2FvmwareEngineNetworks%2Fvmwareenginenetwork-minimal-${uniqueId}
+
+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
+
+{
+  "createTime": "2024-04-01T12:34:56.123456Z",
+  "description": "test dependency vmwareengine network",
+  "etag": "abcdef0123A=",
+  "name": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+  "state": 2,
+  "type": 2,
+  "uid": "111111111111111111111",
+  "updateTime": "2024-04-01T12:34:56.123456Z",
+  "vpcNetworks": [
+    {
+      "network": "projects/b3e854f0b4bedfea6-tp/global/networks/${networkId}",
+      "type": 2
+    },
+    {
+      "network": "projects/b3e854f0b4bedfea6-tp/global/networks/${networkId}",
+      "type": 1
+    },
+    {
+      "network": "projects/b3e854f0b4bedfea6-tp/global/networks/${networkId}",
+      "type": 3
+    }
+  ]
+}
+
+---
+
+DELETE https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}?%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%2Fglobal%2FvmwareEngineNetworks%2Fvmwareenginenetwork-minimal-${uniqueId}
+
+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
+
+{
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+    "verb": "delete"
+  },
+  "name": "projects/${projectId}/locations/global/operations/${operationID}"
+}
+
+---
+
+GET https://vmwareengine.googleapis.com/v1/projects/${projectId}/locations/global/operations/${operationID}
+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%2Fglobal%2Foperations%2F${operationID}
+
+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
+
+{
+  "done": true,
+  "metadata": {
+    "@type": "type.googleapis.com/google.cloud.vmwareengine.v1.OperationMetadata",
+    "apiVersion": "v1",
+    "createTime": "2024-04-01T12:34:56.123456Z",
+    "endTime": "2024-04-01T12:34:56.123456Z",
+    "target": "projects/${projectId}/locations/global/vmwareEngineNetworks/vmwareenginenetwork-minimal-${uniqueId}",
+    "verb": "delete"
+  },
+  "name": "projects/${projectId}/locations/global/operations/${operationID}",
+  "response": {
+    "@type": "type.googleapis.com/google.protobuf.Empty"
+  }
+}
\ No newline at end of file
diff --git a/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/create.yaml b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/create.yaml
new file mode 100644
index 00000000000..485d387ec73
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/create.yaml
@@ -0,0 +1,32 @@
+# 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: vmwareengine.cnrm.cloud.google.com/v1alpha1
+kind: VMwareEnginePrivateCloud
+metadata:
+  name: vmwareengineprivatecloud-minimal-${uniqueId}
+spec:
+  projectRef:
+    external: ${projectId}
+  location: us-west2-a # Private clouds of type STANDARD and TIME_LIMITED are zonal resources, STRETCHED private clouds are regional. 
+  description: "Initial description"
+  networkConfig:
+    vmwareEngineNetworkRef:
+      name: vmwareenginenetwork-minimal-${uniqueId}
+    managementCIDR: "192.168.30.0/24"
+  managementCluster:
+    clusterID: "cluster-1"
+    nodeTypeConfigs:
+      - nodeTypeID: "standard-72"
+        nodeCount: 3
diff --git a/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/dependencies.yaml b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/dependencies.yaml
new file mode 100644
index 00000000000..671a564ab19
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/dependencies.yaml
@@ -0,0 +1,24 @@
+# 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: vmwareengine.cnrm.cloud.google.com/v1alpha1
+kind: VMwareEngineNetwork
+metadata:
+  name: vmwareenginenetwork-minimal-${uniqueId}
+spec:
+  projectRef:
+    external: ${projectId}
+  location: global
+  description: "test dependency vmwareengine network"
+  type: STANDARD
diff --git a/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/update.yaml b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/update.yaml
new file mode 100644
index 00000000000..ccd35fb25c2
--- /dev/null
+++ b/pkg/test/resourcefixture/testdata/basic/vmwareengine/v1alpha1/vmwareengineprivatecloud/vmwareengineprivatecloud-minimal/update.yaml
@@ -0,0 +1,32 @@
+# 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: vmwareengine.cnrm.cloud.google.com/v1alpha1
+kind: VMwareEnginePrivateCloud
+metadata:
+  name: vmwareengineprivatecloud-minimal-${uniqueId}
+spec:
+  projectRef:
+    external: ${projectId}
+  location: us-west2-a
+  description: "Updated description"
+  networkConfig:
+    vmwareEngineNetworkRef:
+      name: vmwareenginenetwork-minimal-${uniqueId}
+    managementCIDR: "192.168.30.0/24"
+  managementCluster:
+    clusterID: "cluster-1"
+    nodeTypeConfigs:
+      - nodeTypeID: "standard-72"
+        nodeCount: 3
diff --git a/tests/e2e/unified_test.go b/tests/e2e/unified_test.go
index 5b8ad46c895..e60764bc64f 100644
--- a/tests/e2e/unified_test.go
+++ b/tests/e2e/unified_test.go
@@ -529,6 +529,7 @@ func runScenario(ctx context.Context, t *testing.T, testPause bool, fixture reso
 					addReplacement("createTime", "2024-04-01T12:34:56.123456Z")
 					addReplacement("expireTime", "2024-04-01T12:34:56.123456Z")
 					addReplacement("response.createTime", "2024-04-01T12:34:56.123456Z")
+					addReplacement("response.expireTime", "2024-04-01T12:34:56.123456Z")
 					addReplacement("response.deleteTime", "2024-04-01T12:34:56.123456Z")
 					addReplacement("creationTimestamp", "2024-04-01T12:34:56.123456Z")
 					addReplacement("metadata.createTime", "2024-04-01T12:34:56.123456Z")


Reviews:
user: login: yuwenma
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T16:47:16Z
user: login: jingyih
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:37:50Z
user: login: jingyih
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:37:52Z

Review comments:
diff_hunk: @@ -0,0 +1,250 @@
+// 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.vmwareengine.v1.VmwareEngine
+// proto.message: google.cloud.vmwareengine.v1.PrivateCloud
+// crd.type: VMwareEnginePrivateCloud
+// crd.version: v1alpha1
+
+package vmwareengine
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	gcp "cloud.google.com/go/vmwareengine/apiv1"
+	pb "cloud.google.com/go/vmwareengine/apiv1/vmwareenginepb"
+	"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"
+
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/vmwareengine/v1alpha1"
+	"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.VMwareEnginePrivateCloudGVK, NewPrivateCloudModel)
+}
+
+func NewPrivateCloudModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &privateCloudModel{config: *config}, nil
+}
+
+var _ directbase.Model = &privateCloudModel{}
+
+type privateCloudModel struct {
+	config config.ControllerConfig
+}
+
+func (m *privateCloudModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.VMwareEnginePrivateCloud{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewPrivateCloudIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// normalize reference fields
+	if obj.Spec.NetworkConfig != nil && obj.Spec.NetworkConfig.VMwareEngineNetworkRef != nil {
path: pkg/controller/direct/vmwareengine/privatecloud_controller.go
user: login: yuwenma
body: suggest moving to create and update only.
created_at: 2025-03-24T16:46:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+// 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.vmwareengine.v1.VmwareEngine
+// proto.message: google.cloud.vmwareengine.v1.PrivateCloud
+// crd.type: VMwareEnginePrivateCloud
+// crd.version: v1alpha1
+
+package vmwareengine
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	gcp "cloud.google.com/go/vmwareengine/apiv1"
+	pb "cloud.google.com/go/vmwareengine/apiv1/vmwareenginepb"
+	"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"
+
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/vmwareengine/v1alpha1"
+	"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.VMwareEnginePrivateCloudGVK, NewPrivateCloudModel)
+}
+
+func NewPrivateCloudModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &privateCloudModel{config: *config}, nil
+}
+
+var _ directbase.Model = &privateCloudModel{}
+
+type privateCloudModel struct {
+	config config.ControllerConfig
+}
+
+func (m *privateCloudModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.VMwareEnginePrivateCloud{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewPrivateCloudIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// normalize reference fields
+	if obj.Spec.NetworkConfig != nil && obj.Spec.NetworkConfig.VMwareEngineNetworkRef != nil {
+		if _, err := obj.Spec.NetworkConfig.VMwareEngineNetworkRef.NormalizedExternal(ctx, reader, obj.GetNamespace()); err != nil {
+			return nil, err
+		}
+	}
+
+	// Get VMwareEngine GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	client, err := gcpClient.newClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &privateCloudAdapter{
+		gcpClient: client,
+		id:        id,
+		desired:   obj,
+	}, nil
+}
+
+func (m *privateCloudModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type privateCloudAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.PrivateCloudIdentity
+	desired   *krm.VMwareEnginePrivateCloud
+	actual    *pb.PrivateCloud
+}
+
+var _ directbase.Adapter = &privateCloudAdapter{}
+
+func (a *privateCloudAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting vmwareengine private cloud", "name", a.id)
+
+	req := &pb.GetPrivateCloudRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetPrivateCloud(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting vmwareengine private cloud %q from gcp: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+func (a *privateCloudAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating vmwareengine private cloud", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	desired := a.desired.DeepCopy()
+	resource := VMwareEnginePrivateCloudSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	req := &pb.CreatePrivateCloudRequest{
+		Parent:         a.id.Parent().String(),
+		PrivateCloudId: a.id.ID(),
+		PrivateCloud:   resource,
+	}
+	op, err := a.gcpClient.CreatePrivateCloud(ctx, req)
+	if err != nil {
+		return fmt.Errorf("creating vmwareengine private cloud %s: %w", a.id.String(), err)
+	}
+	created, err := op.Wait(ctx)
+	if err != nil {
+		return fmt.Errorf("vmwareengine private cloud %s waiting creation: %w", a.id, err)
+	}
+	log.V(2).Info("successfully created vmwareengine private cloud in gcp", "name", a.id)
+
+	status := &krm.VMwareEnginePrivateCloudStatus{}
+	status.ObservedState = VMwareEnginePrivateCloudObservedState_FromProto(mapCtx, created)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+	status.ExternalRef = direct.LazyPtr(a.id.String())
+	return createOp.UpdateStatus(ctx, status, nil)
+}
+
+func (a *privateCloudAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating vmwareengine private cloud", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	desired := a.desired.DeepCopy()
+	resource := VMwareEnginePrivateCloudSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	if desired.Spec.Description != nil && !reflect.DeepEqual(resource.Description, a.actual.Description) {
+		paths = append(paths, "description")
+	}
+	// TODO: spec.managementCluster is not readable from GCP
+	// TODO: spec.networkConfig is has generated fields from GCP
+
+	if len(paths) == 0 {
+		log.V(2).Info("no field needs update", "name", a.id)
path: pkg/controller/direct/vmwareengine/privatecloud_controller.go
user: login: yuwenma
body: (This should have been fixed in scifi). If the paths is empty, we still want to update the KCC object `.status` for first time acquire. 
created_at: 2025-03-24T16:47:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+// 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.vmwareengine.v1.VmwareEngine
+// proto.message: google.cloud.vmwareengine.v1.PrivateCloud
+// crd.type: VMwareEnginePrivateCloud
+// crd.version: v1alpha1
+
+package vmwareengine
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	gcp "cloud.google.com/go/vmwareengine/apiv1"
+	pb "cloud.google.com/go/vmwareengine/apiv1/vmwareenginepb"
+	"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"
+
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/vmwareengine/v1alpha1"
+	"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.VMwareEnginePrivateCloudGVK, NewPrivateCloudModel)
+}
+
+func NewPrivateCloudModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &privateCloudModel{config: *config}, nil
+}
+
+var _ directbase.Model = &privateCloudModel{}
+
+type privateCloudModel struct {
+	config config.ControllerConfig
+}
+
+func (m *privateCloudModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.VMwareEnginePrivateCloud{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewPrivateCloudIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// normalize reference fields
+	if obj.Spec.NetworkConfig != nil && obj.Spec.NetworkConfig.VMwareEngineNetworkRef != nil {
path: pkg/controller/direct/vmwareengine/privatecloud_controller.go
user: login: jingyih
body: OK. This seems to be a common pattern in the controllers I've implemented. I'll review all of them and fix them.
created_at: 2025-03-24T17:37:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+// 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.vmwareengine.v1.VmwareEngine
+// proto.message: google.cloud.vmwareengine.v1.PrivateCloud
+// crd.type: VMwareEnginePrivateCloud
+// crd.version: v1alpha1
+
+package vmwareengine
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	gcp "cloud.google.com/go/vmwareengine/apiv1"
+	pb "cloud.google.com/go/vmwareengine/apiv1/vmwareenginepb"
+	"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"
+
+	refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
+	krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/vmwareengine/v1alpha1"
+	"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.VMwareEnginePrivateCloudGVK, NewPrivateCloudModel)
+}
+
+func NewPrivateCloudModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
+	return &privateCloudModel{config: *config}, nil
+}
+
+var _ directbase.Model = &privateCloudModel{}
+
+type privateCloudModel struct {
+	config config.ControllerConfig
+}
+
+func (m *privateCloudModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
+	obj := &krm.VMwareEnginePrivateCloud{}
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
+		return nil, fmt.Errorf("error converting to %T: %w", obj, err)
+	}
+
+	id, err := krm.NewPrivateCloudIdentity(ctx, reader, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	// normalize reference fields
+	if obj.Spec.NetworkConfig != nil && obj.Spec.NetworkConfig.VMwareEngineNetworkRef != nil {
+		if _, err := obj.Spec.NetworkConfig.VMwareEngineNetworkRef.NormalizedExternal(ctx, reader, obj.GetNamespace()); err != nil {
+			return nil, err
+		}
+	}
+
+	// Get VMwareEngine GCP client
+	gcpClient, err := newGCPClient(ctx, &m.config)
+	if err != nil {
+		return nil, err
+	}
+	client, err := gcpClient.newClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &privateCloudAdapter{
+		gcpClient: client,
+		id:        id,
+		desired:   obj,
+	}, nil
+}
+
+func (m *privateCloudModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
+	// TODO: Support URLs
+	return nil, nil
+}
+
+type privateCloudAdapter struct {
+	gcpClient *gcp.Client
+	id        *krm.PrivateCloudIdentity
+	desired   *krm.VMwareEnginePrivateCloud
+	actual    *pb.PrivateCloud
+}
+
+var _ directbase.Adapter = &privateCloudAdapter{}
+
+func (a *privateCloudAdapter) Find(ctx context.Context) (bool, error) {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("getting vmwareengine private cloud", "name", a.id)
+
+	req := &pb.GetPrivateCloudRequest{Name: a.id.String()}
+	actual, err := a.gcpClient.GetPrivateCloud(ctx, req)
+	if err != nil {
+		if direct.IsNotFound(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("getting vmwareengine private cloud %q from gcp: %w", a.id.String(), err)
+	}
+
+	a.actual = actual
+	return true, nil
+}
+
+func (a *privateCloudAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("creating vmwareengine private cloud", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	desired := a.desired.DeepCopy()
+	resource := VMwareEnginePrivateCloudSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	req := &pb.CreatePrivateCloudRequest{
+		Parent:         a.id.Parent().String(),
+		PrivateCloudId: a.id.ID(),
+		PrivateCloud:   resource,
+	}
+	op, err := a.gcpClient.CreatePrivateCloud(ctx, req)
+	if err != nil {
+		return fmt.Errorf("creating vmwareengine private cloud %s: %w", a.id.String(), err)
+	}
+	created, err := op.Wait(ctx)
+	if err != nil {
+		return fmt.Errorf("vmwareengine private cloud %s waiting creation: %w", a.id, err)
+	}
+	log.V(2).Info("successfully created vmwareengine private cloud in gcp", "name", a.id)
+
+	status := &krm.VMwareEnginePrivateCloudStatus{}
+	status.ObservedState = VMwareEnginePrivateCloudObservedState_FromProto(mapCtx, created)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+	status.ExternalRef = direct.LazyPtr(a.id.String())
+	return createOp.UpdateStatus(ctx, status, nil)
+}
+
+func (a *privateCloudAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
+	log := klog.FromContext(ctx)
+	log.V(2).Info("updating vmwareengine private cloud", "name", a.id)
+	mapCtx := &direct.MapContext{}
+
+	desired := a.desired.DeepCopy()
+	resource := VMwareEnginePrivateCloudSpec_ToProto(mapCtx, &desired.Spec)
+	if mapCtx.Err() != nil {
+		return mapCtx.Err()
+	}
+
+	paths := []string{}
+	if desired.Spec.Description != nil && !reflect.DeepEqual(resource.Description, a.actual.Description) {
+		paths = append(paths, "description")
+	}
+	// TODO: spec.managementCluster is not readable from GCP
+	// TODO: spec.networkConfig is has generated fields from GCP
+
+	if len(paths) == 0 {
+		log.V(2).Info("no field needs update", "name", a.id)
path: pkg/controller/direct/vmwareengine/privatecloud_controller.go
user: login: jingyih
body: Same here. I’ll apply a batch fix to all the controllers I’ve implemented.
created_at: 2025-03-24T17:37:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: jingyih
created_at: 2025-03-23T18:33:24Z
body: /hold
Based on the CRD PR #4149.
author_association: CONTRIBUTOR
user: login: jingyih
created_at: 2025-03-24T17:55:28Z
body: Rebased to resolve merge conflict.
author_association: CONTRIBUTOR
user: login: jingyih
created_at: 2025-03-24T18:00:04Z
body: /hold cancel
author_association: CONTRIBUTOR
user: login: yuwenma
created_at: 2025-03-24T22:09:19Z
body: /lgtm
/approve

Sg!
author_association: CONTRIBUTOR
user: login: google-oss-prow[bot]
created_at: 2025-03-24T22:09:30Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/GoogleCloudPlatform/k8s-config-connector/pull/4163#issuecomment-2749512613" 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