Skip to content

Commit a1d8bc7

Browse files
committed
ServiceIdentity: support cloudservices.gserviceaccount.com
This is an unusual P4SA, that is the P4SA for Managed Instance Groups (MIGs)
1 parent 4b291bc commit a1d8bc7

File tree

6 files changed

+168
-52
lines changed

6 files changed

+168
-52
lines changed

pkg/controller/direct/registry/registry.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ type registration struct {
3636
gvk schema.GroupVersionKind
3737
factory ModelFactoryFunc
3838
model directbase.Model
39-
rg predicate.ReconcileGate
39+
40+
// reconcileGate is a condition, we will reconcile only objects matching the specified condition
41+
reconcileGate predicate.ReconcileGate
4042
}
4143

4244
type ModelFactoryFunc func(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error)
@@ -51,7 +53,7 @@ func GetModel(gk schema.GroupKind) (directbase.Model, error) {
5153

5254
func GetReconcileGate(gk schema.GroupKind) predicate.ReconcileGate {
5355
registration := singleton.registrations[gk]
54-
return registration.rg
56+
return registration.reconcileGate
5557
}
5658

5759
func PreferredGVK(gk schema.GroupKind) (schema.GroupVersionKind, bool) {
@@ -97,14 +99,14 @@ func RegisterModel(gvk schema.GroupVersionKind, modelFn ModelFactoryFunc) {
9799
RegisterModelWithReconcileGate(gvk, modelFn, rg)
98100
}
99101

100-
func RegisterModelWithReconcileGate(gvk schema.GroupVersionKind, modelFn ModelFactoryFunc, rg predicate.ReconcileGate) {
102+
func RegisterModelWithReconcileGate(gvk schema.GroupVersionKind, modelFn ModelFactoryFunc, reconcileGate predicate.ReconcileGate) {
101103
if singleton.registrations == nil {
102104
singleton.registrations = make(map[schema.GroupKind]*registration)
103105
}
104106
singleton.registrations[gvk.GroupKind()] = &registration{
105-
gvk: gvk,
106-
factory: modelFn,
107-
rg: rg,
107+
gvk: gvk,
108+
factory: modelFn,
109+
reconcileGate: reconcileGate,
108110
}
109111
}
110112

pkg/controller/direct/secretmanager/secret_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ const (
4444
)
4545

4646
func init() {
47-
rg := &SecretReconcileGate{}
48-
registry.RegisterModelWithReconcileGate(krm.SecretManagerSecretGVK, NewModel, rg)
47+
reconcileGate := &SecretReconcileGate{}
48+
registry.RegisterModelWithReconcileGate(krm.SecretManagerSecretGVK, NewModel, reconcileGate)
4949
}
5050

5151
type SecretReconcileGate struct {

pkg/controller/direct/serviceusage/serviceidentity_controller.go

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ import (
2626
"fmt"
2727
"time"
2828

29+
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common/projects"
2930
krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/serviceusage/v1beta1"
3031
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
3132
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
3233
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
3334
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
35+
kccpredicate "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/predicate"
3436

3537
gcp "google.golang.org/api/serviceusage/v1beta1"
3638
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -39,8 +41,38 @@ import (
3941
"sigs.k8s.io/controller-runtime/pkg/client"
4042
)
4143

44+
// GoogleApisServiceAgent was historically used by multiple services, but for more granular permission control we moved to P4SAs instead.
45+
// MIGs were tricky to move though, and so instead of switching to a P4SA, they adopted the GoogleApisServiceAgent as their P4SA
46+
// We still want to support this though, so we recognize this "magic value"
47+
const GoogleApisServiceAgent = "cloudservices.gserviceaccount.com"
48+
4249
func init() {
43-
registry.RegisterModel(krm.ServiceIdentityGVK, NewServiceIdentityModel)
50+
reconcileGate := &serviceIdentityReconcileGate{}
51+
registry.RegisterModelWithReconcileGate(krm.ServiceIdentityGVK, NewServiceIdentityModel, reconcileGate)
52+
}
53+
54+
// serviceIdentityReconcileGate opts in some reconciliation to direct.
55+
// Specifically if the service is the "magic" cloudservices.gserviceaccount.com P4SA,
56+
// we use direct - it is not supported by legacy reconcilers.
57+
type serviceIdentityReconcileGate struct {
58+
optIn kccpredicate.OptInToDirectReconciliation
59+
}
60+
61+
var _ kccpredicate.ReconcileGate = &serviceIdentityReconcileGate{}
62+
63+
func (r *serviceIdentityReconcileGate) ShouldReconcile(o *unstructured.Unstructured) bool {
64+
if r.optIn.ShouldReconcile(o) {
65+
return true
66+
}
67+
obj := &krm.ServiceIdentity{}
68+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &obj); err != nil {
69+
return false
70+
}
71+
serviceName := direct.ValueOf(obj.Spec.ResourceID)
72+
if serviceName == "" {
73+
serviceName = obj.GetName()
74+
}
75+
return serviceName == GoogleApisServiceAgent
4476
}
4577

4678
func NewServiceIdentityModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
@@ -74,9 +106,10 @@ func (m *serviceIdentityModel) AdapterForObject(ctx context.Context, reader clie
74106
return nil, err
75107
}
76108
return &serviceIdentityAdapter{
77-
gcpBeta: gcpBeta,
78-
id: id.(*krm.ServiceIdentityIdentity),
79-
desired: obj,
109+
gcpBeta: gcpBeta,
110+
id: id.(*krm.ServiceIdentityIdentity),
111+
desired: obj,
112+
projectMapper: m.config.ProjectMapper,
80113
}, nil
81114
}
82115

@@ -86,7 +119,9 @@ func (m *serviceIdentityModel) AdapterForURL(ctx context.Context, url string) (d
86119
}
87120

88121
type serviceIdentityAdapter struct {
89-
gcpBeta *gcp.APIService
122+
projectMapper *projects.ProjectMapper
123+
gcpBeta *gcp.APIService
124+
90125
id *krm.ServiceIdentityIdentity
91126
desired *krm.ServiceIdentity
92127
actual *gcp.ServiceIdentity // This will be populated from status or after generation
@@ -122,55 +157,64 @@ func (a *serviceIdentityAdapter) Create(ctx context.Context, createOp *directbas
122157
fqn := a.id.String()
123158
log.V(2).Info("generating service identity", "fqn", fqn)
124159

125-
// - parent: Name of the consumer and service to generate an identity for. The
126-
// `GenerateServiceIdentity` methods currently support projects, folders,
127-
// organizations. Example parents would be:
128-
// `projects/123/services/example.googleapis.com`
129-
// `folders/123/services/example.googleapis.com`
130-
// `organizations/123/services/example.googleapis.com`.
131-
132-
op, err := a.gcpBeta.Services.GenerateServiceIdentity(fqn).Context(ctx).Do()
133-
if err != nil {
134-
return fmt.Errorf("generating service identity for %q: %w", fqn, err)
135-
}
160+
status := &krm.ServiceIdentityStatus{}
161+
if a.id.Service == GoogleApisServiceAgent {
162+
// Special case: the MIG "P4SA" (which is also the "Google APIs Service Agent")
163+
// Format is always <projectNumber>@cloudservices.gserviceaccount.com
136164

137-
var serviceIdentity *gcp.GoogleApiServiceusageV1beta1ServiceIdentity
138-
for {
139-
if op.Done {
140-
klog.Warningf("RESPONSE IS %v", string(op.Response))
141-
klog.Warningf("FULL OPERATION IS %v", op)
142-
identity := &gcp.GoogleApiServiceusageV1beta1ServiceIdentity{}
143-
if err := json.Unmarshal(op.Response, identity); err != nil {
144-
return fmt.Errorf("error parsing response: %w", err)
145-
}
146-
serviceIdentity = identity
147-
break
148-
}
149-
time.Sleep(2 * time.Second)
150-
if err := ctx.Err(); err != nil {
151-
return err
165+
projectNumber, err := a.projectMapper.LookupProjectNumber(ctx, a.id.ParentID.ProjectID)
166+
if err != nil {
167+
return fmt.Errorf("looking up project number for %q: %w", a.id.ParentID.ProjectID, err)
152168
}
153-
op, err = a.gcpBeta.Operations.Get(op.Name).Context(ctx).Do()
169+
email := fmt.Sprintf("%[email protected]", projectNumber)
170+
status.Email = &email
171+
} else {
172+
// - parent: Name of the consumer and service to generate an identity for. The
173+
// `GenerateServiceIdentity` methods currently support projects, folders,
174+
// organizations. Example parents would be:
175+
// `projects/123/services/example.googleapis.com`
176+
// `folders/123/services/example.googleapis.com`
177+
// `organizations/123/services/example.googleapis.com`.
178+
179+
op, err := a.gcpBeta.Services.GenerateServiceIdentity(fqn).Context(ctx).Do()
154180
if err != nil {
155-
return fmt.Errorf("waiting for service identity generation for %q: %w", fqn, err)
181+
return fmt.Errorf("generating service identity for %q: %w", fqn, err)
156182
}
157-
}
158183

159-
log.V(2).Info("successfully generated service identity", "fqn", fqn, "identity.email", serviceIdentity.Email, "identity.uniqueId", serviceIdentity.UniqueId)
184+
var serviceIdentity *gcp.GoogleApiServiceusageV1beta1ServiceIdentity
185+
for {
186+
if op.Done {
187+
identity := &gcp.GoogleApiServiceusageV1beta1ServiceIdentity{}
188+
if err := json.Unmarshal(op.Response, identity); err != nil {
189+
return fmt.Errorf("error parsing response: %w", err)
190+
}
191+
serviceIdentity = identity
192+
break
193+
}
194+
time.Sleep(2 * time.Second)
195+
if err := ctx.Err(); err != nil {
196+
return err
197+
}
198+
op, err = a.gcpBeta.Operations.Get(op.Name).Context(ctx).Do()
199+
if err != nil {
200+
return fmt.Errorf("waiting for service identity generation for %q: %w", fqn, err)
201+
}
202+
}
160203

161-
// It really doesn't seem worthwhile to use the mapper here
204+
log.V(2).Info("successfully generated service identity", "fqn", fqn, "identity.email", serviceIdentity.Email, "identity.uniqueId", serviceIdentity.UniqueId)
162205

163-
status := &krm.ServiceIdentityStatus{}
206+
// It really doesn't seem worthwhile to use the mapper here
164207

165-
// observedState := krm.ServiceIdentityObservedState{
166-
// Email: direct.ValueOf(serviceIdentity.Email),
167-
// UniqueID: direct.ValueOf(serviceIdentity.UniqueId),
168-
// }
208+
// observedState := krm.ServiceIdentityObservedState{
209+
// Email: direct.ValueOf(serviceIdentity.Email),
210+
// UniqueID: direct.ValueOf(serviceIdentity.UniqueId),
211+
// }
169212

170-
// status.ObservedState = &observedState
213+
// status.ObservedState = &observedState
171214

172-
status.Email = direct.LazyPtr(serviceIdentity.Email)
173-
// status.UniqueID = direct.LazyPtr(serviceIdentity.UniqueId)
215+
status.Email = direct.LazyPtr(serviceIdentity.Email)
216+
// status.UniqueID = direct.LazyPtr(serviceIdentity.UniqueId)
217+
}
174218

175219
// status.ExternalRef = direct.LazyPtr(parent)
176220
return createOp.UpdateStatus(ctx, status, nil)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1
2+
kind: ServiceIdentity
3+
metadata:
4+
annotations:
5+
cnrm.cloud.google.com/management-conflict-prevention-policy: none
6+
finalizers:
7+
- cnrm.cloud.google.com/finalizer
8+
- cnrm.cloud.google.com/deletion-defender
9+
generation: 1
10+
labels:
11+
cnrm-test: "true"
12+
name: example
13+
namespace: ${uniqueId}
14+
spec:
15+
projectRef:
16+
external: ${projectId}
17+
resourceID: cloudservices.gserviceaccount.com
18+
status:
19+
conditions:
20+
- lastTransitionTime: "1970-01-01T00:00:00Z"
21+
message: The resource is up to date
22+
reason: UpToDate
23+
status: "True"
24+
type: Ready
25+
email: ${projectNumber}@cloudservices.gserviceaccount.com
26+
observedGeneration: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
GET https://cloudresourcemanager.googleapis.com/v3/projects/${projectId}?%24alt=json%3Benum-encoding%3Dint
2+
Content-Type: application/json
3+
User-Agent: kcc/${kccVersion} (+https://github.com/GoogleCloudPlatform/k8s-config-connector) kcc/controller-manager/${kccVersion}
4+
X-Goog-Request-Params: name=projects%2F${projectId}
5+
6+
200 OK
7+
Content-Type: application/json; charset=UTF-8
8+
Server: ESF
9+
Vary: Origin
10+
Vary: X-Origin
11+
Vary: Referer
12+
X-Content-Type-Options: nosniff
13+
X-Frame-Options: SAMEORIGIN
14+
X-Xss-Protection: 0
15+
16+
{
17+
"createTime": "2024-04-01T12:34:56.123456Z",
18+
"etag": "abcdef0123A=",
19+
"name": "projects/${projectNumber}",
20+
"projectId": "${projectId}",
21+
"state": 1
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
apiVersion: serviceusage.cnrm.cloud.google.com/v1beta1
16+
kind: ServiceIdentity
17+
metadata:
18+
name: example
19+
spec:
20+
projectRef:
21+
external: ${projectId}
22+
resourceID: cloudservices.gserviceaccount.com

0 commit comments

Comments
 (0)