Skip to content

Commit ad9dbd2

Browse files
authored
Greenfield: Implement MockGCP and Alignment for CloudBatchResourceAllowance (#11108)
This PR implements MockGCP and Alignment for the `CloudBatchResourceAllowance` resource. ### Summary of Changes - Registered the `mockbatch` service under the mockgcp environment (`mockgcp/register.go`). - Added support for the `v1alpha` Batch API to the `mockbatch` MockService (`mockgcp/mockbatch/service.go`). - Implemented gRPC endpoints for `CreateResourceAllowance`, `GetResourceAllowance`, `UpdateResourceAllowance`, `DeleteResourceAllowance`, and `ListResourceAllowances` under `mockgcp/mockbatch/resourceallowance.go`. - Registered the `batch/resourceallowance` direct controller package (`pkg/controller/direct/register/register.go`), which was previously missing and prevented model registration. - Added `CloudBatchResourceAllowance` to the allowed GroupKinds in the test harness (`config/tests/samples/create/harness.go`). - Fixed a bug in `cloudbatchresourceallowance_controller.go` where the target endpoint for gRPC client initialization was omitted from `grpc.Dial`. - Fixed a re-reconciliation bug in the same controller where comparing desired with actual state incorrectly flagged a diff on the `name` field, causing unnecessary Updates. - Generated and validated the mock golden files and HTTP logs for both minimal and maximal test fixtures. This PR was generated by the **overseer** agent (powered by the gemini-3.5-flash model). Fixes #11097
2 parents 250a3b0 + 96f7030 commit ad9dbd2

17 files changed

Lines changed: 1330 additions & 3 deletions

File tree

config/tests/samples/create/harness.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,7 @@ func MaybeSkip(t *testing.T, testKey string, resources []*unstructured.Unstructu
989989
case schema.GroupKind{Group: "backupdr.cnrm.cloud.google.com", Kind: "BackupDRBackupPlanAssociation"}:
990990

991991
case schema.GroupKind{Group: "batch.cnrm.cloud.google.com", Kind: "BatchJob"}:
992+
case schema.GroupKind{Group: "batch.cnrm.cloud.google.com", Kind: "CloudBatchResourceAllowance"}:
992993

993994
case schema.GroupKind{Group: "gkebackup.cnrm.cloud.google.com", Kind: "GKEBackupBackup"}:
994995
case schema.GroupKind{Group: "gkebackup.cnrm.cloud.google.com", Kind: "GKEBackupBackupPlan"}:
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// +tool:mockgcp-support
16+
// proto.service: google.cloud.batch.v1alpha.BatchService
17+
// proto.message: google.cloud.batch.v1alpha.ResourceAllowance
18+
19+
package mockbatch
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"regexp"
25+
"time"
26+
27+
"cloud.google.com/go/longrunning/autogen/longrunningpb"
28+
"google.golang.org/grpc/codes"
29+
"google.golang.org/grpc/status"
30+
"google.golang.org/protobuf/proto"
31+
"google.golang.org/protobuf/types/known/timestamppb"
32+
33+
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects"
34+
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/pkg/storage"
35+
pb_v1alpha "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/batch/resourceallowance/pb"
36+
)
37+
38+
type resourceAllowanceName struct {
39+
Project *projects.ProjectData
40+
Location string
41+
ResourceAllowance string
42+
}
43+
44+
func (n *resourceAllowanceName) String() string {
45+
return fmt.Sprintf("projects/%s/locations/%s/resourceAllowances/%s", n.Project.ID, n.Location, n.ResourceAllowance)
46+
}
47+
48+
func (s *MockService) parseResourceAllowanceName(name string) (*resourceAllowanceName, error) {
49+
r := regexp.MustCompile("^projects/(?P<project>[^/]+)/locations/(?P<location>[^/]+)/resourceAllowances/(?P<resource_allowance>[^/]+)$")
50+
match := r.FindStringSubmatch(name)
51+
if len(match) == 0 {
52+
return nil, status.Errorf(codes.InvalidArgument, "name %q is not valid", name)
53+
}
54+
m := make(map[string]string)
55+
for i, n := range r.SubexpNames() {
56+
if len(n) > 0 {
57+
m[n] = match[i]
58+
}
59+
}
60+
61+
project, err := s.Projects.GetProjectByID(m["project"])
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
return &resourceAllowanceName{
67+
Project: project,
68+
Location: m["location"],
69+
ResourceAllowance: m["resource_allowance"],
70+
}, nil
71+
}
72+
73+
func (s *BatchV1Alpha) CreateResourceAllowance(ctx context.Context, req *pb_v1alpha.CreateResourceAllowanceRequest) (*pb_v1alpha.ResourceAllowance, error) {
74+
reqName := req.Parent + "/resourceAllowances/" + req.ResourceAllowanceId
75+
name, err := s.parseResourceAllowanceName(reqName)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
fqn := name.String()
81+
82+
obj := proto.Clone(req.ResourceAllowance).(*pb_v1alpha.ResourceAllowance)
83+
obj.Name = fqn
84+
obj.Uid = "b9a676df-c595-4c81-9963-f44b8e44e50c"
85+
obj.CreateTime = timestamppb.Now()
86+
87+
if obj.GetUsageResourceAllowance() != nil {
88+
if obj.GetUsageResourceAllowance().Status == nil {
89+
obj.GetUsageResourceAllowance().Status = &pb_v1alpha.UsageResourceAllowanceStatus{}
90+
}
91+
obj.GetUsageResourceAllowance().Status.State = pb_v1alpha.ResourceAllowanceState_RESOURCE_ALLOWANCE_ACTIVE
92+
93+
if obj.GetUsageResourceAllowance().Spec != nil && obj.GetUsageResourceAllowance().Spec.Limit != nil {
94+
limitVal := obj.GetUsageResourceAllowance().Spec.Limit.Limit
95+
obj.GetUsageResourceAllowance().Status.LimitStatus = &pb_v1alpha.UsageResourceAllowanceStatus_LimitStatus{
96+
Limit: limitVal,
97+
Consumed: float64Ptr(0.0),
98+
}
99+
}
100+
}
101+
102+
if err := s.storage.Create(ctx, fqn, obj); err != nil {
103+
return nil, err
104+
}
105+
106+
return obj, nil
107+
}
108+
109+
func (s *BatchV1Alpha) GetResourceAllowance(ctx context.Context, req *pb_v1alpha.GetResourceAllowanceRequest) (*pb_v1alpha.ResourceAllowance, error) {
110+
name, err := s.parseResourceAllowanceName(req.Name)
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
fqn := name.String()
116+
117+
obj := &pb_v1alpha.ResourceAllowance{}
118+
if err := s.storage.Get(ctx, fqn, obj); err != nil {
119+
return nil, err
120+
}
121+
122+
return obj, nil
123+
}
124+
125+
func (s *BatchV1Alpha) UpdateResourceAllowance(ctx context.Context, req *pb_v1alpha.UpdateResourceAllowanceRequest) (*pb_v1alpha.ResourceAllowance, error) {
126+
name, err := s.parseResourceAllowanceName(req.ResourceAllowance.Name)
127+
if err != nil {
128+
return nil, err
129+
}
130+
fqn := name.String()
131+
132+
obj := &pb_v1alpha.ResourceAllowance{}
133+
if err := s.storage.Get(ctx, fqn, obj); err != nil {
134+
return nil, err
135+
}
136+
137+
updated := proto.Clone(req.ResourceAllowance).(*pb_v1alpha.ResourceAllowance)
138+
updated.Uid = obj.Uid
139+
updated.CreateTime = obj.CreateTime
140+
141+
if updated.GetUsageResourceAllowance() != nil {
142+
if updated.GetUsageResourceAllowance().Status == nil {
143+
updated.GetUsageResourceAllowance().Status = &pb_v1alpha.UsageResourceAllowanceStatus{}
144+
}
145+
updated.GetUsageResourceAllowance().Status.State = pb_v1alpha.ResourceAllowanceState_RESOURCE_ALLOWANCE_ACTIVE
146+
147+
if updated.GetUsageResourceAllowance().Spec != nil && updated.GetUsageResourceAllowance().Spec.Limit != nil {
148+
limitVal := updated.GetUsageResourceAllowance().Spec.Limit.Limit
149+
updated.GetUsageResourceAllowance().Status.LimitStatus = &pb_v1alpha.UsageResourceAllowanceStatus_LimitStatus{
150+
Limit: limitVal,
151+
Consumed: float64Ptr(0.0),
152+
}
153+
if obj.GetUsageResourceAllowance() != nil && obj.GetUsageResourceAllowance().Status != nil && obj.GetUsageResourceAllowance().Status.LimitStatus != nil {
154+
updated.GetUsageResourceAllowance().Status.LimitStatus.Consumed = obj.GetUsageResourceAllowance().Status.LimitStatus.Consumed
155+
}
156+
}
157+
}
158+
159+
if err := s.storage.Update(ctx, fqn, updated); err != nil {
160+
return nil, err
161+
}
162+
163+
return updated, nil
164+
}
165+
166+
func (s *BatchV1Alpha) DeleteResourceAllowance(ctx context.Context, req *pb_v1alpha.DeleteResourceAllowanceRequest) (*longrunningpb.Operation, error) {
167+
name, err := s.parseResourceAllowanceName(req.Name)
168+
if err != nil {
169+
return nil, err
170+
}
171+
172+
fqn := name.String()
173+
now := time.Now()
174+
175+
deleted := &pb_v1alpha.ResourceAllowance{}
176+
if err := s.storage.Delete(ctx, fqn, deleted); err != nil {
177+
return nil, err
178+
}
179+
180+
operationMetadata := &pb_v1alpha.OperationMetadata{
181+
CreateTime: timestamppb.New(now),
182+
EndTime: timestamppb.New(now),
183+
ApiVersion: "v1alpha",
184+
RequestedCancellation: false,
185+
Verb: "delete",
186+
Target: fqn,
187+
}
188+
return s.operations.DoneLRO(ctx, fqn, operationMetadata, nil)
189+
}
190+
191+
func (s *BatchV1Alpha) ListResourceAllowances(ctx context.Context, req *pb_v1alpha.ListResourceAllowancesRequest) (*pb_v1alpha.ListResourceAllowancesResponse, error) {
192+
parent := req.GetParent()
193+
if parent == "" {
194+
return nil, status.Errorf(codes.InvalidArgument, "parent must be specified")
195+
}
196+
197+
var resourceAllowances []*pb_v1alpha.ResourceAllowance
198+
if err := s.storage.List(ctx, (&pb_v1alpha.ResourceAllowance{}).ProtoReflect().Descriptor(), storage.ListOptions{
199+
Prefix: parent + "/resourceAllowances/",
200+
}, func(obj proto.Message) error {
201+
allowance := obj.(*pb_v1alpha.ResourceAllowance)
202+
resourceAllowances = append(resourceAllowances, allowance)
203+
return nil
204+
}); err != nil {
205+
return nil, err
206+
}
207+
208+
return &pb_v1alpha.ListResourceAllowancesResponse{
209+
ResourceAllowances: resourceAllowances,
210+
}, nil
211+
}
212+
213+
func float64Ptr(v float64) *float64 {
214+
return &v
215+
}

mockgcp/mockbatch/service.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/httptogrpc"
3131
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/operations"
3232
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/pkg/storage"
33+
pb_v1alpha "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/batch/resourceallowance/pb"
3334
)
3435

3536
// MockService represents a mocked batch service.
@@ -39,14 +40,20 @@ type MockService struct {
3940

4041
operations *operations.Operations
4142

42-
v1 *BatchV1
43+
v1 *BatchV1
44+
v1alpha *BatchV1Alpha
4345
}
4446

4547
type BatchV1 struct {
4648
*MockService
4749
pb.UnimplementedBatchServiceServer
4850
}
4951

52+
type BatchV1Alpha struct {
53+
*MockService
54+
pb_v1alpha.UnimplementedBatchServiceServer
55+
}
56+
5057
// New creates a MockService.
5158
func New(env *common.MockEnvironment, storage storage.Storage) *MockService {
5259
s := &MockService{
@@ -55,6 +62,7 @@ func New(env *common.MockEnvironment, storage storage.Storage) *MockService {
5562
operations: operations.NewOperationsService(storage),
5663
}
5764
s.v1 = &BatchV1{MockService: s}
65+
s.v1alpha = &BatchV1Alpha{MockService: s}
5866
return s
5967
}
6068

@@ -64,6 +72,7 @@ func (s *MockService) ExpectedHosts() []string {
6472

6573
func (s *MockService) Register(grpcServer *grpc.Server) {
6674
pb.RegisterBatchServiceServer(grpcServer, s.v1)
75+
pb_v1alpha.RegisterBatchServiceServer(grpcServer, s.v1alpha)
6776
}
6877

6978
func (s *MockService) NewHTTPMux(ctx context.Context, conn *grpc.ClientConn) (http.Handler, error) {

mockgcp/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
_ "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/mockartifactregistry"
2626
_ "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/mockasset"
2727
_ "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/mockbackupdr"
28+
_ "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/mockbatch"
2829
_ "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/mockbigquery"
2930
_ "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/mockbigqueryreservation"
3031
_ "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/mockbigtable"

pkg/controller/direct/batch/resourceallowance/cloudbatchresourceallowance_controller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ func (m *model) client(ctx context.Context) (pb.BatchServiceClient, error) {
6767
return nil, err
6868
}
6969

70+
opts = append(opts, option.WithEndpoint("batch.googleapis.com:443"))
71+
7072
conn, err := grpc.Dial(ctx, opts...)
7173
if err != nil {
7274
return nil, fmt.Errorf("dialing batch service: %w", err)
@@ -169,6 +171,7 @@ func (a *adapter) Update(ctx context.Context, updateOp *directbase.UpdateOperati
169171
log.Info("updating batch resource allowance", "name", a.id)
170172

171173
desiredpb := proto.CloneOf(a.desired)
174+
desiredpb.Name = a.id.String()
172175
paths, err := common.CompareProtoMessage(desiredpb, a.actual, common.BasicDiff)
173176
if err != nil {
174177
return err
@@ -184,7 +187,6 @@ func (a *adapter) Update(ctx context.Context, updateOp *directbase.UpdateOperati
184187
req := &pb.UpdateResourceAllowanceRequest{
185188
ResourceAllowance: desiredpb,
186189
}
187-
desiredpb.Name = a.id.String()
188190

189191
updated, err := a.gcpClient.UpdateResourceAllowance(ctx, req)
190192
if err != nil {

pkg/controller/direct/batch/resourceallowance/cloudbatchresourceallowance_fuzzer.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ func batchResourceAllowanceFuzzer() fuzztesting.KRMFuzzer {
3737
f.SpecFields.Insert(".notifications")
3838
f.SpecFields.Insert(".usage_resource_allowance")
3939

40-
f.StatusFields.Insert(".name")
4140
f.StatusFields.Insert(".uid")
4241
f.StatusFields.Insert(".create_time")
4342
f.StatusFields.Insert(".usage_resource_allowance")
4443

44+
f.Unimplemented_NotYetTriaged(".name")
45+
4546
return f
4647
}

pkg/controller/direct/maputils.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ func HasHTTPCode(err error, code int) bool {
318318
}
319319
}
320320
} else {
321+
if s, ok := grpcStatus.FromError(err); ok {
322+
if s.Code() == grpcCode.NotFound && code == 404 {
323+
return true
324+
}
325+
return false
326+
}
321327
klog.Warningf("unexpected error type %T", err)
322328
}
323329
return false

pkg/controller/direct/maputils_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,64 @@ func TestIsAlreadyExists(t *testing.T) {
108108
})
109109
}
110110
}
111+
112+
func TestIsNotFound(t *testing.T) {
113+
tests := []struct {
114+
name string
115+
err error
116+
want bool
117+
}{
118+
{
119+
name: "nil error",
120+
err: nil,
121+
want: false,
122+
},
123+
{
124+
name: "unrelated error",
125+
err: fmt.Errorf("something went wrong"),
126+
want: false,
127+
},
128+
{
129+
name: "gRPC NotFound",
130+
err: status.Error(codes.NotFound, "not found"),
131+
want: true,
132+
},
133+
{
134+
name: "gRPC AlreadyExists",
135+
err: status.Error(codes.AlreadyExists, "already exists"),
136+
want: false,
137+
},
138+
{
139+
name: "gRPC NotFound wrapped with apierror",
140+
err: func() error {
141+
grpcErr := status.Error(codes.NotFound, "not found")
142+
apiErr, _ := apierror.ParseError(grpcErr, true)
143+
return apiErr
144+
}(),
145+
want: true,
146+
},
147+
{
148+
name: "gRPC AlreadyExists wrapped with apierror",
149+
err: func() error {
150+
grpcErr := status.Error(codes.AlreadyExists, "already exists")
151+
apiErr, _ := apierror.ParseError(grpcErr, true)
152+
return apiErr
153+
}(),
154+
want: false,
155+
},
156+
{
157+
name: "wrapped gRPC NotFound",
158+
err: fmt.Errorf("getting resource: %w", status.Error(codes.NotFound, "not found")),
159+
want: true,
160+
},
161+
}
162+
163+
for _, tc := range tests {
164+
t.Run(tc.name, func(t *testing.T) {
165+
got := IsNotFound(tc.err)
166+
if got != tc.want {
167+
t.Errorf("IsNotFound(%v) = %v, want %v", tc.err, got, tc.want)
168+
}
169+
})
170+
}
171+
}

0 commit comments

Comments
 (0)