Skip to content

Commit 23bdb84

Browse files
committed
Add CloudIdentityGroup direct controller
1 parent 0f24fae commit 23bdb84

File tree

13 files changed

+1056
-17
lines changed

13 files changed

+1056
-17
lines changed

apis/cloudidentity/v1beta1/group_identity.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type GroupIdentity struct {
3030
}
3131

3232
func (i *GroupIdentity) String() string {
33-
return "/groups/" + i.id
33+
return "groups/" + i.id
3434
}
3535

3636
func (i *GroupIdentity) ID() string {
@@ -50,15 +50,18 @@ func NewGroupIdentity(ctx context.Context, reader client.Reader, obj *CloudIdent
5050

5151
// Use approved External
5252
externalRef := common.ValueOf(obj.Status.ExternalRef)
53+
actualResourceID := ""
54+
var err error
5355
if externalRef != "" {
5456
// Validate desired with actual
55-
actualResourceID, err := ParseGroupExternal(externalRef)
57+
actualResourceID, err = ParseGroupExternal(externalRef)
5658
if err != nil {
5759
return nil, err
5860
}
5961
if actualResourceID != resourceID {
60-
return nil, fmt.Errorf("cannot reset `metadata.name` or `spec.resourceID` to %s, since it has already assigned to %s",
61-
resourceID, actualResourceID)
62+
return &GroupIdentity{
63+
id: actualResourceID,
64+
}, nil
6265
}
6366
}
6467
return &GroupIdentity{

mockgcp/mockcloudidentity/groups.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,14 @@ func (s *groupsServer) PatchGroup(ctx context.Context, req *pb.PatchGroupRequest
151151
// TODO: Some sort of helper for fieldmask?
152152
for _, path := range strings.Split(req.GetUpdateMask(), ",") {
153153
switch path {
154-
case "displayName":
154+
case "displayName": // TF controller uses displayName while direct controller uses display_name
155+
obj.DisplayName = req.GetGroup().DisplayName // TF
156+
case "display_name":
155157
obj.DisplayName = req.GetGroup().DisplayName
156-
case "description":
157-
obj.Description = req.GetGroup().Description
158158
case "labels":
159159
obj.Labels = req.GetGroup().Labels
160+
case "description":
161+
obj.Description = req.GetGroup().Description
160162
default:
161163
return nil, status.Errorf(codes.InvalidArgument, "update_mask path %q not valid", path)
162164
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// Copyright 2025 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 cloudidentity
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"fmt"
21+
"sort"
22+
"strings"
23+
24+
krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/cloudidentity/v1beta1"
25+
cloudidentitygrouppb "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/google/apps/cloudidentity/groups/v1beta1"
26+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
27+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
28+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/common"
29+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
30+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
31+
gcp "google.golang.org/api/cloudidentity/v1beta1"
32+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33+
"k8s.io/apimachinery/pkg/runtime"
34+
"k8s.io/klog/v2"
35+
"sigs.k8s.io/controller-runtime/pkg/client"
36+
)
37+
38+
func init() {
39+
registry.RegisterModel(krm.CloudIdentityGroupGVK, NewGroupModel)
40+
}
41+
42+
func NewGroupModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
43+
return &modelGroup{config: *config}, nil
44+
}
45+
46+
var _ directbase.Model = &modelGroup{}
47+
48+
type modelGroup struct {
49+
config config.ControllerConfig
50+
}
51+
52+
func (m *modelGroup) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
53+
obj := &krm.CloudIdentityGroup{}
54+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
55+
return nil, fmt.Errorf("error converting to %T: %w", obj, err)
56+
}
57+
58+
// Handle TF default values
59+
if obj.Spec.InitialGroupConfig == nil {
60+
obj.Spec.InitialGroupConfig = direct.LazyPtr("EMPTY")
61+
}
62+
63+
id, err := krm.NewGroupIdentity(ctx, reader, obj)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
// Get cloudidentitygroup GCP client
69+
opts, err := m.config.RESTClientOptions()
70+
if err != nil {
71+
return nil, err
72+
}
73+
gcpClient, err := gcp.NewService(ctx, opts...)
74+
if err != nil {
75+
return nil, err
76+
}
77+
return &GroupAdapter{
78+
id: id,
79+
gcpClient: gcpClient,
80+
desired: obj,
81+
}, nil
82+
}
83+
84+
func (m *modelGroup) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
85+
// TODO: Support URLs
86+
return nil, nil
87+
}
88+
89+
type GroupAdapter struct {
90+
id *krm.GroupIdentity
91+
gcpClient *gcp.Service
92+
desired *krm.CloudIdentityGroup
93+
actual *cloudidentitygrouppb.Group
94+
}
95+
96+
var _ directbase.Adapter = &GroupAdapter{}
97+
98+
// Find retrieves the GCP resource.
99+
// Return true means the object is found. This triggers Adapter `Update` call.
100+
// Return false means the object is not found. This triggers Adapter `Create` call.
101+
// Return a non-nil error requeues the requests.
102+
func (a *GroupAdapter) Find(ctx context.Context) (bool, error) {
103+
log := klog.FromContext(ctx)
104+
log.V(2).Info("getting Group", "name", a.id)
105+
106+
desired := a.desired.DeepCopy()
107+
// CreateTime is empty, indicating that resource does not exist
108+
if desired.Status.CreateTime == nil {
109+
return false, nil
110+
}
111+
112+
generatedId := direct.ValueOf(desired.Status.Name)
113+
resource, err := a.gcpClient.Groups.Get(generatedId).Context(ctx).Do()
114+
if err != nil {
115+
return false, fmt.Errorf("getting Group %q: %w", a.id, err)
116+
}
117+
118+
a.actual = convertAPIToProto(resource)
119+
return true, nil
120+
}
121+
122+
// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
123+
func (a *GroupAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
124+
log := klog.FromContext(ctx)
125+
log.V(2).Info("creating Group", "name", a.id)
126+
mapCtx := &direct.MapContext{}
127+
128+
desired := a.desired.DeepCopy()
129+
resource := CloudIdentityGroupSpec_ToProto(mapCtx, &desired.Spec)
130+
if mapCtx.Err() != nil {
131+
return mapCtx.Err()
132+
}
133+
134+
req := convertProtoToAPI(resource)
135+
136+
initialGroupConfig := direct.ValueOf(desired.Spec.InitialGroupConfig)
137+
op, err := a.gcpClient.Groups.Create(req).InitialGroupConfig(initialGroupConfig).Context(ctx).Do()
138+
if err != nil {
139+
return fmt.Errorf("creating Group %s: %w", a.id, err)
140+
}
141+
142+
// Get server generated group name
143+
var data interface{}
144+
err = json.Unmarshal(op.Response, &data)
145+
generatedName := data.(map[string]interface{})["name"].(string)
146+
147+
created, err := a.gcpClient.Groups.Get(generatedName).Context(ctx).Do()
148+
if err != nil {
149+
return fmt.Errorf("getting created Group %q: %w", a.id, err)
150+
}
151+
152+
createdPB := convertAPIToProto(created)
153+
154+
log.V(2).Info("successfully created Group", "name", a.id)
155+
156+
status := &krm.CloudIdentityGroupStatus{
157+
Name: direct.LazyPtr(created.Name),
158+
CreateTime: direct.LazyPtr(created.CreateTime),
159+
UpdateTime: direct.LazyPtr(created.UpdateTime),
160+
}
161+
status.ObservedState = CloudIdentityGroupObservedState_FromProto(mapCtx, createdPB)
162+
if mapCtx.Err() != nil {
163+
return mapCtx.Err()
164+
}
165+
166+
externalRef := generatedName
167+
status.ExternalRef = &externalRef
168+
return createOp.UpdateStatus(ctx, status, nil)
169+
}
170+
171+
// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
172+
func (a *GroupAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
173+
log := klog.FromContext(ctx)
174+
log.V(2).Info("updating Group", "name", a.id)
175+
mapCtx := &direct.MapContext{}
176+
177+
desired := a.desired.DeepCopy()
178+
resource := CloudIdentityGroupSpec_ToProto(mapCtx, &desired.Spec)
179+
if mapCtx.Err() != nil {
180+
return mapCtx.Err()
181+
}
182+
generatedId := desired.Status.Name
183+
//resource.Name = generatedId
184+
185+
paths, err := common.CompareProtoMessage(resource, a.actual, common.BasicDiff)
186+
if err != nil {
187+
return err
188+
}
189+
190+
if len(paths) == 0 {
191+
log.V(2).Info("no field needs update", "name", a.id)
192+
status := &krm.CloudIdentityGroupStatus{}
193+
status.ObservedState = CloudIdentityGroupObservedState_FromProto(mapCtx, a.actual)
194+
if mapCtx.Err() != nil {
195+
return mapCtx.Err()
196+
}
197+
return updateOp.UpdateStatus(ctx, status, nil)
198+
}
199+
200+
// updateMask is a comma-separated list of fully qualified names of fields.
201+
var stringSlice []string
202+
for path := range paths {
203+
stringSlice = append(stringSlice, path)
204+
}
205+
206+
sort.Strings(stringSlice)
207+
updateMask := strings.Join(stringSlice, ",")
208+
209+
req := convertProtoToAPI(resource)
210+
211+
_, err = a.gcpClient.Groups.Patch(direct.ValueOf(generatedId), req).UpdateMask(updateMask).Context(ctx).Do()
212+
if err != nil {
213+
return fmt.Errorf("updating Group %s: %w", a.id, err)
214+
}
215+
216+
updated, err := a.gcpClient.Groups.Get(direct.ValueOf(generatedId)).Context(ctx).Do()
217+
if err != nil {
218+
return fmt.Errorf("getting updated Group %q: %w", a.id, err)
219+
}
220+
updatedPB := convertAPIToProto(updated)
221+
log.V(2).Info("successfully updated Group", "name", a.id)
222+
223+
status := &krm.CloudIdentityGroupStatus{
224+
Name: direct.LazyPtr(updated.Name),
225+
CreateTime: direct.LazyPtr(updated.CreateTime),
226+
UpdateTime: direct.LazyPtr(updated.UpdateTime),
227+
}
228+
status.ObservedState = CloudIdentityGroupObservedState_FromProto(mapCtx, updatedPB)
229+
if mapCtx.Err() != nil {
230+
return mapCtx.Err()
231+
}
232+
return updateOp.UpdateStatus(ctx, status, nil)
233+
}
234+
235+
// Export maps the GCP object to a Config Connector resource `spec`.
236+
func (a *GroupAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) {
237+
if a.actual == nil {
238+
return nil, fmt.Errorf("Find() not called")
239+
}
240+
u := &unstructured.Unstructured{}
241+
242+
obj := &krm.CloudIdentityGroup{}
243+
mapCtx := &direct.MapContext{}
244+
obj.Spec = direct.ValueOf(CloudIdentityGroupSpec_FromProto(mapCtx, a.actual))
245+
if mapCtx.Err() != nil {
246+
return nil, mapCtx.Err()
247+
}
248+
uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
249+
if err != nil {
250+
return nil, err
251+
}
252+
253+
u.SetName(a.actual.GetName())
254+
u.SetGroupVersionKind(krm.CloudIdentityGroupGVK)
255+
256+
u.Object = uObj
257+
return u, nil
258+
}
259+
260+
// Delete the resource from GCP service when the corresponding Config Connector resource is deleted.
261+
func (a *GroupAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) {
262+
log := klog.FromContext(ctx)
263+
log.V(2).Info("deleting Group", "name", a.id)
264+
265+
desired := a.desired.DeepCopy()
266+
// CreateTime is empty, indicating that resource does not exist
267+
if desired.Status.CreateTime == nil {
268+
return false, nil
269+
}
270+
generatedId := direct.ValueOf(desired.Status.Name)
271+
272+
_, err := a.gcpClient.Groups.Delete(generatedId).Context(ctx).Do()
273+
if err != nil {
274+
if direct.IsNotFound(err) {
275+
return false, nil
276+
}
277+
return false, fmt.Errorf("deleting Group %q: %w", a.id, err)
278+
}
279+
280+
log.V(2).Info("successfully deleted Group", "name", a.id)
281+
return true, nil
282+
}

0 commit comments

Comments
 (0)