diff --git a/apis/cloudbuild/v1alpha1/conversion.go b/apis/cloudbuild/v1alpha1/conversion.go index 768b96cbb3..11bc325119 100644 --- a/apis/cloudbuild/v1alpha1/conversion.go +++ b/apis/cloudbuild/v1alpha1/conversion.go @@ -25,6 +25,8 @@ func Convert_WorkerPool_API_v1_To_KRM_status(in *cloudbuildpb.WorkerPool, out *C return nil } out.ObservedState = &CloudBuildWorkerPoolObservedState{} + + out.ObservedState.ETag = &in.Etag if err := Convert_PrivatePoolV1Config_API_v1_To_KRM(in.GetPrivatePoolV1Config(), out.ObservedState); err != nil { return err } diff --git a/apis/cloudbuild/v1alpha1/workerpool_types.go b/apis/cloudbuild/v1alpha1/workerpool_types.go index 6397bfdf3a..b2fabdce91 100644 --- a/apis/cloudbuild/v1alpha1/workerpool_types.go +++ b/apis/cloudbuild/v1alpha1/workerpool_types.go @@ -86,6 +86,10 @@ type CloudBuildWorkerPoolObservedState struct { // +optional WorkerConfig *WorkerConfig `json:"workerConfig,omitempty"` NetworkConfig *NetworkConfigState `json:"networkConfig,omitempty"` + + /* The Checksum computed by the server, using weak indicator.*/ + // +optional + ETag *string `json:"etag,omitempty"` } type NetworkConfigState struct { diff --git a/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go b/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go index 2e9aed9c88..e4bbc1fbe6 100644 --- a/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go +++ b/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go @@ -92,6 +92,11 @@ func (in *CloudBuildWorkerPoolObservedState) DeepCopyInto(out *CloudBuildWorkerP *out = new(NetworkConfigState) **out = **in } + if in.ETag != nil { + in, out := &in.ETag, &out.ETag + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudBuildWorkerPoolObservedState. diff --git a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_cloudbuildworkerpools.cloudbuild.cnrm.cloud.google.com.yaml b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_cloudbuildworkerpools.cloudbuild.cnrm.cloud.google.com.yaml index f072f3b0bb..6581240c36 100644 --- a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_cloudbuildworkerpools.cloudbuild.cnrm.cloud.google.com.yaml +++ b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_cloudbuildworkerpools.cloudbuild.cnrm.cloud.google.com.yaml @@ -180,6 +180,9 @@ spec: description: The creation timestamp of the workerpool. format: date-time type: string + etag: + description: The Checksum computed by the server, using weak indicator. + type: string networkConfig: properties: egressOption: diff --git a/mockgcp/common/fields/etag.go b/mockgcp/common/fields/etag.go new file mode 100644 index 0000000000..8f90639c1e --- /dev/null +++ b/mockgcp/common/fields/etag.go @@ -0,0 +1,71 @@ +// 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. + +package fields + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" +) + +var mustFields = []string{ + "displayName", + "state", +} + +// ComputeEtag computes the etag of the proto object with weak indicator. +func ComputeEtag(obj proto.Message) string { + pb := proto.Clone(obj) + + // ignore dynamic fields like timestampe or uniqueId. + descriptor := pb.ProtoReflect().Descriptor() + fieldDescs := descriptor.Fields() + for i := 0; i < fieldDescs.Len(); i++ { + fieldDesc := fieldDescs.Get(i) + must := false + for _, mustField := range mustFields { + if fieldDesc.JSONName() == mustField { + must = true + } + } + if !must { + pb.ProtoReflect().Clear(fieldDesc) + } + } + + m, err := prototext.Marshal(pb) + if err != nil { + panic(fmt.Sprintf("converting to prototext: %v", err)) + } + h := sha256.Sum256([]byte(m)) + str := base64.StdEncoding.EncodeToString(h[:]) + strong := fmt.Sprintf(`"%s"`, str) // ETag must be quoted. + return "W/" + strong +} + +func ComputeEtagBytes(obj proto.Message) []byte { + return []byte(ComputeEtag(obj)) +} + +func ComputeEtagPtr(obj proto.Message) *string { + return ptrTo(ComputeEtag(obj)) +} + +func ptrTo[T any](t T) *T { + return &t +} diff --git a/mockgcp/mockcloudbuild/workerpool.go b/mockgcp/mockcloudbuild/workerpool.go index 393a1c9fda..674e2950da 100644 --- a/mockgcp/mockcloudbuild/workerpool.go +++ b/mockgcp/mockcloudbuild/workerpool.go @@ -20,6 +20,7 @@ import ( "strings" "cloud.google.com/go/longrunning/autogen/longrunningpb" + "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/fields" "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects" pb "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/mockgcp/devtools/cloudbuild/v1" "google.golang.org/grpc/codes" @@ -77,6 +78,7 @@ func (s *CloudBuildV1) CreateWorkerPool(ctx context.Context, req *pb.CreateWorke result.CreateTime = now result.UpdateTime = now result.State = pb.WorkerPool_RUNNING + result.Etag = fields.ComputeEtag(result) return result, nil }) } @@ -99,7 +101,7 @@ func (s *CloudBuildV1) UpdateWorkerPool(ctx context.Context, req *pb.UpdateWorke f := target.FieldByName(path) if f.IsValid() && f.CanSet() { switch f.Kind() { - case reflect.Int: + case reflect.Int, reflect.Int64: intVal := source.FieldByName(path).Int() f.SetInt(intVal) case reflect.String: @@ -122,6 +124,7 @@ func (s *CloudBuildV1) UpdateWorkerPool(ctx context.Context, req *pb.UpdateWorke result := proto.Clone(obj).(*pb.WorkerPool) result.UpdateTime = now result.State = pb.WorkerPool_RUNNING + result.Etag = fields.ComputeEtag(result) return result, nil }) } diff --git a/pkg/clients/generated/apis/cloudbuild/v1alpha1/cloudbuildworkerpool_types.go b/pkg/clients/generated/apis/cloudbuild/v1alpha1/cloudbuildworkerpool_types.go index 73d62f9f33..38bfd72fb3 100644 --- a/pkg/clients/generated/apis/cloudbuild/v1alpha1/cloudbuildworkerpool_types.go +++ b/pkg/clients/generated/apis/cloudbuild/v1alpha1/cloudbuildworkerpool_types.go @@ -94,6 +94,10 @@ type WorkerpoolObservedStateStatus struct { // +optional CreateTime *string `json:"createTime,omitempty"` + /* The Checksum computed by the server, using weak indicator. */ + // +optional + Etag *string `json:"etag,omitempty"` + // +optional NetworkConfig *WorkerpoolNetworkConfigStatus `json:"networkConfig,omitempty"` diff --git a/pkg/clients/generated/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go b/pkg/clients/generated/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go index 12492f78c1..8eaf16910c 100644 --- a/pkg/clients/generated/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/clients/generated/apis/cloudbuild/v1alpha1/zz_generated.deepcopy.go @@ -220,6 +220,11 @@ func (in *WorkerpoolObservedStateStatus) DeepCopyInto(out *WorkerpoolObservedSta *out = new(string) **out = **in } + if in.Etag != nil { + in, out := &in.Etag, &out.Etag + *out = new(string) + **out = **in + } if in.NetworkConfig != nil { in, out := &in.NetworkConfig, &out.NetworkConfig *out = new(WorkerpoolNetworkConfigStatus) diff --git a/pkg/controller/direct/cloudbuild/workerpool_controller.go b/pkg/controller/direct/cloudbuild/workerpool_controller.go index 41e998dace..760b76eca3 100644 --- a/pkg/controller/direct/cloudbuild/workerpool_controller.go +++ b/pkg/controller/direct/cloudbuild/workerpool_controller.go @@ -259,6 +259,7 @@ func (a *Adapter) Update(ctx context.Context, u *unstructured.Unstructured) erro wp := &cloudbuildpb.WorkerPool{ Name: a.fullyQualifiedName(), + Etag: a.actual.Etag, } desired := a.desired.DeepCopy() err := krm.Convert_WorkerPool_KRM_To_API_v1(desired, wp) diff --git a/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_generated_object_cloudbuildworkerpool.golden.yaml b/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_generated_object_cloudbuildworkerpool.golden.yaml index 3acbb4ad51..88dcb35386 100644 --- a/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_generated_object_cloudbuildworkerpool.golden.yaml +++ b/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_generated_object_cloudbuildworkerpool.golden.yaml @@ -33,6 +33,7 @@ status: observedGeneration: 2 observedState: createTime: "1970-01-01T00:00:00Z" + etag: W/"pwzCjvRptOAz64ZilGUmgNZjYVh0LzD3Oh+zEbMPULw=" networkConfig: egressOption: NO_PUBLIC_EGRESS peeredNetwork: projects/${projectId}/global/networks/computenetwork-${uniqueId} diff --git a/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_http.log b/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_http.log index d38bcce2cf..69b98084c3 100644 --- a/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_http.log +++ b/pkg/test/resourcefixture/testdata/basic/cloudbuild/v1alpha1/cloudbuildworkerpool/_http.log @@ -593,6 +593,7 @@ X-Xss-Protection: 0 "@type": "type.googleapis.com/google.devtools.cloudbuild.v1.WorkerPool", "createTime": "2024-04-01T12:34:56.123456Z", "displayName": "New CloudBuild WorkerPool", + "etag": "abcdef0123A=", "name": "projects/${projectId}/locations/us-central1/workerPools/cloudbuildworkerpool-${uniqueId}", "privatePoolV1Config": { "networkConfig": { @@ -715,6 +716,7 @@ X-Xss-Protection: 0 "response": { "@type": "type.googleapis.com/google.devtools.cloudbuild.v1.WorkerPool", "displayName": "New CloudBuild WorkerPool", + "etag": "abcdef0123A=", "name": "projects/${projectId}/locations/us-central1/workerPools/cloudbuildworkerpool-${uniqueId}", "privatePoolV1Config": { "networkConfig": {