Skip to content

Commit 15c7729

Browse files
authored
Implement direct controller for NetworkSecurityInterceptEndpointGroup (#11437)
This Pull Request implements Phase 2 (Direct Controller and E2E Tests) for the `NetworkSecurityInterceptEndpointGroup` resource. ### Summary of Changes: 1. **Direct Controller**: Added `NetworkSecurityInterceptEndpointGroup` direct controller implementation under `pkg/controller/direct/networksecurity/interceptendpointgroup/` following the expert instructions, pattern isolation, and using top-level field diff comparison based on the custom spec-roundtrip masking technique. 2. **Mappers**: Added focused and hand-picked mappers in `pkg/controller/direct/networksecurity/interceptendpointgroup/mapper.generated.go`. 3. **Fuzzer**: Added KRM fuzzer implementation under `pkg/controller/direct/networksecurity/interceptendpointgroup/` registered with the fuzztesting runner. 4. **Identity Coverage**: Added unit tests in `apis/networksecurity/v1alpha1/networksecurityinterceptendpointgroup_identity_test.go` to ensure IdentityV2 and Resource interface correctness. 5. **MockGCP Support**: Implemented mock service support for `InterceptEndpointGroup` (`Create`, `Get`, `Update`, `Delete`) under `mockgcp/mocknetworksecurity/` to enable fully isolated, high-speed end-to-end testing. 6. **E2E Tests**: Added minimal and full E2E test fixtures and generated golden objects and logs, passing 100% green against MockGCP. --- This PR was generated by the **overseer,overseer,priority/medium,area/direct,greenfield,step/controller,chore/ai** agent (powered by the gemini-3.5-flash model). For more context, see the chore instructions in [.agents/greenfield-direct-controller.md](/.agents/greenfield-direct-controller.md). Fixes #11425
2 parents abcd4dc + 6c189dc commit 15c7729

23 files changed

Lines changed: 2039 additions & 5 deletions

File tree

apis/networksecurity/v1alpha1/networksecurityinterceptendpointgroup_identity.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ type NetworkSecurityInterceptEndpointGroupIdentity struct {
3939
Interceptendpointgroup string
4040
}
4141

42+
func (i *NetworkSecurityInterceptEndpointGroupIdentity) ParentString() string {
43+
return fmt.Sprintf("projects/%s/locations/%s", i.Project, i.Location)
44+
}
45+
4246
func (i *NetworkSecurityInterceptEndpointGroupIdentity) String() string {
4347
return NetworkSecurityInterceptEndpointGroupIdentityFormat.ToString(*i)
4448
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
package v1alpha1
16+
17+
import (
18+
"testing"
19+
20+
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common/identity"
21+
)
22+
23+
func TestNetworkSecurityInterceptEndpointGroupIdentity(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
external string
27+
want *NetworkSecurityInterceptEndpointGroupIdentity
28+
wantErr bool
29+
}{
30+
{
31+
name: "basic",
32+
external: "projects/my-project/locations/us-central1/interceptEndpointGroups/my-group",
33+
want: &NetworkSecurityInterceptEndpointGroupIdentity{
34+
Project: "my-project",
35+
Location: "us-central1",
36+
Interceptendpointgroup: "my-group",
37+
},
38+
},
39+
{
40+
name: "invalid format",
41+
external: "invalid/my-project/locations/us-central1/interceptEndpointGroups/my-group",
42+
wantErr: true,
43+
},
44+
}
45+
46+
for _, tc := range tests {
47+
t.Run(tc.name, func(t *testing.T) {
48+
got := &NetworkSecurityInterceptEndpointGroupIdentity{}
49+
err := got.FromExternal(tc.external)
50+
if (err != nil) != tc.wantErr {
51+
t.Errorf("FromExternal() error = %v, wantErr %v", err, tc.wantErr)
52+
return
53+
}
54+
if !tc.wantErr {
55+
if got.String() != tc.external {
56+
t.Errorf("String() = %v, want %v", got.String(), tc.external)
57+
}
58+
}
59+
})
60+
}
61+
}
62+
63+
func TestNetworkSecurityInterceptEndpointGroupIdentity_Interfaces(t *testing.T) {
64+
var _ identity.IdentityV2 = &NetworkSecurityInterceptEndpointGroupIdentity{}
65+
var _ identity.Resource = &NetworkSecurityInterceptEndpointGroup{}
66+
}

config/tests/samples/create/harness.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,7 @@ func MaybeSkip(t *testing.T, testKey string, resources []*unstructured.Unstructu
12261226
case schema.GroupKind{Group: "networksecurity.cnrm.cloud.google.com", Kind: "NetworkSecurityServerTLSPolicy"}:
12271227
case schema.GroupKind{Group: "networksecurity.cnrm.cloud.google.com", Kind: "NetworkSecurityMirroringDeployment"}:
12281228
case schema.GroupKind{Group: "networksecurity.cnrm.cloud.google.com", Kind: "NetworkSecurityMirroringEndpointGroup"}:
1229+
case schema.GroupKind{Group: "networksecurity.cnrm.cloud.google.com", Kind: "NetworkSecurityInterceptEndpointGroup"}:
12291230
case schema.GroupKind{Group: "networksecurity.cnrm.cloud.google.com", Kind: "NetworkSecuritySACRealm"}:
12301231
case schema.GroupKind{Group: "networksecurity.cnrm.cloud.google.com", Kind: "NetworkSecurityFirewallEndpointAssociation"}:
12311232

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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+
package mocknetworksecurity
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"strings"
21+
"time"
22+
23+
pbv1 "cloud.google.com/go/networksecurity/apiv1/networksecuritypb"
24+
"github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects"
25+
longrunningpb "google.golang.org/genproto/googleapis/longrunning"
26+
"google.golang.org/grpc/codes"
27+
"google.golang.org/grpc/status"
28+
"google.golang.org/protobuf/proto"
29+
"google.golang.org/protobuf/reflect/protoreflect"
30+
"google.golang.org/protobuf/types/known/timestamppb"
31+
)
32+
33+
type InterceptServer struct {
34+
*MockService
35+
pbv1.UnimplementedInterceptServer
36+
}
37+
38+
func (s *InterceptServer) CreateInterceptEndpointGroup(ctx context.Context, req *pbv1.CreateInterceptEndpointGroupRequest) (*longrunningpb.Operation, error) {
39+
name := req.Parent + "/interceptEndpointGroups/" + req.InterceptEndpointGroupId
40+
41+
fqn := name
42+
43+
obj := proto.CloneOf(req.InterceptEndpointGroup)
44+
obj.Name = fqn
45+
obj.CreateTime = timestamppb.New(time.Now())
46+
obj.UpdateTime = timestamppb.New(time.Now())
47+
obj.State = pbv1.InterceptEndpointGroup_ACTIVE
48+
49+
if err := s.storage.Create(ctx, fqn, obj); err != nil {
50+
return nil, err
51+
}
52+
53+
now := time.Now()
54+
lroMetadata := &pbv1.OperationMetadata{
55+
CreateTime: timestamppb.New(now),
56+
RequestedCancellation: false,
57+
Target: name,
58+
Verb: "create",
59+
ApiVersion: "v1",
60+
}
61+
return s.operations.StartLRO(ctx, req.Parent, lroMetadata, func() (protoreflect.ProtoMessage, error) {
62+
lroMetadata.EndTime = timestamppb.New(time.Now())
63+
result := proto.CloneOf(obj)
64+
return result, nil
65+
})
66+
}
67+
68+
func (s *InterceptServer) GetInterceptEndpointGroup(ctx context.Context, req *pbv1.GetInterceptEndpointGroupRequest) (*pbv1.InterceptEndpointGroup, error) {
69+
name, err := s.parseInterceptEndpointGroupName(req.Name)
70+
if err != nil {
71+
return nil, err
72+
}
73+
74+
fqn := name.String()
75+
76+
obj := &pbv1.InterceptEndpointGroup{}
77+
if err := s.storage.Get(ctx, fqn, obj); err != nil {
78+
if status.Code(err) == codes.NotFound {
79+
return nil, status.Errorf(codes.NotFound, "Resource '%s' was not found", fqn)
80+
}
81+
return nil, err
82+
}
83+
return obj, nil
84+
}
85+
86+
func (s *InterceptServer) UpdateInterceptEndpointGroup(ctx context.Context, req *pbv1.UpdateInterceptEndpointGroupRequest) (*longrunningpb.Operation, error) {
87+
name, err := s.parseInterceptEndpointGroupName(req.GetInterceptEndpointGroup().GetName())
88+
if err != nil {
89+
return nil, err
90+
}
91+
obj := &pbv1.InterceptEndpointGroup{}
92+
if err := s.storage.Get(ctx, name.String(), obj); err != nil {
93+
return nil, err
94+
}
95+
96+
updated := proto.CloneOf(obj)
97+
updated.UpdateTime = timestamppb.New(time.Now())
98+
99+
// Apply field mask updates
100+
paths := req.GetUpdateMask().GetPaths()
101+
if len(paths) == 0 {
102+
// Default to all updateable spec fields if empty
103+
paths = []string{"labels", "description"}
104+
}
105+
106+
for _, path := range paths {
107+
switch path {
108+
case "labels":
109+
updated.Labels = req.GetInterceptEndpointGroup().GetLabels()
110+
case "description":
111+
updated.Description = req.GetInterceptEndpointGroup().GetDescription()
112+
default:
113+
return nil, status.Errorf(codes.InvalidArgument, "field %q is not updateable", path)
114+
}
115+
}
116+
117+
if err := s.storage.Update(ctx, name.String(), updated); err != nil {
118+
return nil, err
119+
}
120+
121+
lroPrefix := fmt.Sprintf("projects/%s/locations/%s", name.Project.ID, name.Location)
122+
now := time.Now()
123+
lroMetadata := &pbv1.OperationMetadata{
124+
CreateTime: timestamppb.New(now),
125+
Target: name.String(),
126+
Verb: "update",
127+
ApiVersion: "v1",
128+
}
129+
return s.operations.StartLRO(ctx, lroPrefix, lroMetadata, func() (protoreflect.ProtoMessage, error) {
130+
lroMetadata.EndTime = timestamppb.New(time.Now())
131+
return updated, nil
132+
})
133+
}
134+
135+
func (s *InterceptServer) DeleteInterceptEndpointGroup(ctx context.Context, req *pbv1.DeleteInterceptEndpointGroupRequest) (*longrunningpb.Operation, error) {
136+
name, err := s.parseInterceptEndpointGroupName(req.Name)
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
fqn := name.String()
142+
143+
obj := &pbv1.InterceptEndpointGroup{}
144+
if err := s.storage.Delete(ctx, fqn, obj); err != nil {
145+
if status.Code(err) == codes.NotFound {
146+
return nil, status.Errorf(codes.NotFound, "Resource '%s' was not found", fqn)
147+
}
148+
return nil, err
149+
}
150+
151+
lroPrefix := fmt.Sprintf("projects/%s/locations/%s", name.Project.ID, name.Location)
152+
now := time.Now()
153+
lroMetadata := &pbv1.OperationMetadata{
154+
CreateTime: timestamppb.New(now),
155+
Target: fqn,
156+
Verb: "delete",
157+
ApiVersion: "v1",
158+
}
159+
return s.operations.StartLRO(ctx, lroPrefix, lroMetadata, func() (protoreflect.ProtoMessage, error) {
160+
lroMetadata.EndTime = timestamppb.New(time.Now())
161+
return obj, nil
162+
})
163+
}
164+
165+
type interceptEndpointGroupName struct {
166+
Project *projects.ProjectData
167+
Location string
168+
InterceptEndpointGroup string
169+
}
170+
171+
func (n *interceptEndpointGroupName) String() string {
172+
return fmt.Sprintf("projects/%s/locations/%s/interceptEndpointGroups/%s", n.Project.ID, n.Location, n.InterceptEndpointGroup)
173+
}
174+
175+
func (s *MockService) parseInterceptEndpointGroupName(name string) (*interceptEndpointGroupName, error) {
176+
tokens := strings.Split(name, "/")
177+
if len(tokens) == 6 && tokens[0] == "projects" && tokens[2] == "locations" && tokens[4] == "interceptEndpointGroups" {
178+
project, err := s.Projects.GetProject(&projects.ProjectName{ProjectID: tokens[1]})
179+
if err != nil {
180+
return nil, err
181+
}
182+
return &interceptEndpointGroupName{
183+
Project: project,
184+
Location: tokens[3],
185+
InterceptEndpointGroup: tokens[5],
186+
}, nil
187+
}
188+
return nil, status.Errorf(codes.InvalidArgument, "name %q is not valid", name)
189+
}

mockgcp/mocknetworksecurity/service.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func (s *MockService) ExpectedHosts() []string {
5858
func (s *MockService) Register(grpcServer *grpc.Server) {
5959
pb.RegisterNetworkSecurityServer(grpcServer, &NetworkSecurityServer{MockService: s})
6060
pbv1.RegisterMirroringServer(grpcServer, &MirroringServer{MockService: s})
61+
pbv1.RegisterInterceptServer(grpcServer, &InterceptServer{MockService: s})
6162
pbv1.RegisterSSERealmServiceServer(grpcServer, &SSERealmServer{MockService: s})
6263
pbv1.RegisterFirewallActivationServer(grpcServer, &FirewallActivationServer{MockService: s})
6364
}
@@ -70,6 +71,7 @@ func (s *MockService) NewHTTPMux(ctx context.Context, conn *grpc.ClientConn) (ht
7071

7172
mux.AddService(pb.NewNetworkSecurityClient(conn))
7273
mux.AddService(pbv1.NewMirroringClient(conn))
74+
mux.AddService(pbv1.NewInterceptClient(conn))
7375
mux.AddService(pbv1.NewSSERealmServiceClient(conn))
7476
mux.AddService(pbv1.NewFirewallActivationClient(conn))
7577
mux.AddOperationsPath("/v1beta1/{prefix=**}/operations/{name}", conn)

0 commit comments

Comments
 (0)