Skip to content

Commit 1333fe1

Browse files
committed
Address review feedback for MonitoringAlertPolicy direct controller
1 parent 9762444 commit 1333fe1

43 files changed

Lines changed: 5789 additions & 1341 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gemini/skills/kcc-direct-controller-implementer/SKILL.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,29 @@ This skill guides the implementation of the controller, mappers, and fuzzer for
4343
```
4444
- **Diff Comparison & Structured Diff (`tags.DiffForTopLevelFields`)**: Always prefer using top-level field tags-based diff comparison via `tags.DiffForTopLevelFields` over recursive/magical comparison functions (such as `common.CompareProtoMessageStructuredDiff` or `common.CompareProtoMessage` which are deprecated/discouraged due to unpredictable behaviors in `BasicDiff`).
4545
```go
46-
diffs, updateMask, err := tags.DiffForTopLevelFields(ctx, clonedDesired.ProtoReflect(), maskedActual.ProtoReflect())
47-
if err != nil {
48-
return nil, nil, err
46+
func compareResource(ctx context.Context, actual, desired *pb.MyResource) (*structuredreporting.Diff, *fieldmaskpb.FieldMask, error) {
47+
maskedActual, err := mappers.OnlySpecFields(actual, MyResourceSpec_FromProto, MyResourceSpec_ToProto)
48+
if err != nil {
49+
return nil, nil, err
50+
}
51+
maskedActual.Name = desired.Name // Restore any non-spec identifier fields if needed
52+
53+
clonedDesired := proto.CloneOf(desired)
54+
55+
populateDefaults := func(obj *pb.MyResource) {
56+
// Even if empty, it's a good pattern to define and populate GCP/server defaults here
57+
if obj.SomeDefaultedField == nil {
58+
obj.SomeDefaultedField = ...
59+
}
60+
}
61+
populateDefaults(maskedActual)
62+
populateDefaults(clonedDesired)
63+
64+
diffs, updateMask, err := tags.DiffForTopLevelFields(ctx, clonedDesired.ProtoReflect(), maskedActual.ProtoReflect())
65+
if err != nil {
66+
return nil, nil, err
67+
}
68+
return diffs, updateMask, nil
4969
}
5070
```
5171
- **Reconciling Empty or Incomplete LRO Responses**: Many GCP APIs (such as Dataproc's `UpdateCluster` LRO) return an empty response (`google.protobuf.Empty`), or do not fully populate read-only status fields (such as state, metrics, or instance names) during resource creation. If you map status directly from such incomplete/empty LRO responses, you will inadvertently clear status fields in Kubernetes.

apis/monitoring/v1beta1/monitoringalertpolicy_identity.go

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ import (
1919
"fmt"
2020
"strings"
2121

22+
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common"
2223
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common/identity"
2324
refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
2425
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcpurls"
2526
"sigs.k8s.io/controller-runtime/pkg/client"
2627
)
2728

2829
var (
29-
_ identity.IdentityV2 = &MonitoringAlertPolicyIdentity{}
30-
_ identity.Resource = &MonitoringAlertPolicy{}
30+
_ identity.ServerGeneratedIdentity = &MonitoringAlertPolicyIdentity{}
31+
_ identity.Resource = &MonitoringAlertPolicy{}
3132
)
3233

3334
var MonitoringAlertPolicyIdentityFormat = gcpurls.Template[MonitoringAlertPolicyIdentity]("monitoring.googleapis.com", "projects/{project}/alertPolicies/{alertpolicy}")
@@ -48,24 +49,58 @@ func (i *MonitoringAlertPolicyIdentity) ParentString() string {
4849
}
4950

5051
func (obj *MonitoringAlertPolicy) GetIdentity(ctx context.Context, reader client.Reader) (identity.Identity, error) {
51-
return getIdentityFromMonitoringAlertPolicySpec(ctx, reader, obj)
52+
specIdentity, err := getIdentityFromMonitoringAlertPolicySpec(ctx, reader, obj)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
// Cross-check the identity against the status value, if present.
58+
statusName := common.ValueOf(obj.Status.Name)
59+
if statusName != "" {
60+
statusIdentity := &MonitoringAlertPolicyIdentity{}
61+
if err := statusIdentity.FromExternal(statusName); err != nil {
62+
return nil, err
63+
}
64+
65+
if specIdentity.AlertPolicy == "" {
66+
specIdentity.AlertPolicy = statusIdentity.AlertPolicy
67+
}
68+
69+
if statusIdentity.String() != specIdentity.String() {
70+
return nil, fmt.Errorf("cannot change MonitoringAlertPolicy identity (old=%q, new=%q)", statusIdentity.String(), specIdentity.String())
71+
}
72+
}
73+
74+
return specIdentity, nil
5275
}
5376

5477
func (i *MonitoringAlertPolicyIdentity) FromExternal(ref string) error {
78+
normalized := ref
79+
normalized = strings.TrimPrefix(normalized, "https:")
80+
normalized = strings.TrimPrefix(normalized, "http:")
81+
normalized = strings.TrimPrefix(normalized, "//")
82+
normalized = strings.TrimPrefix(normalized, "monitoring.googleapis.com/")
83+
normalized = strings.TrimPrefix(normalized, "v3/")
84+
5585
// This relative format is a very special case and unusual for a GCP API.
5686
// For example, the IncidentList resource under Monitoring Dashboard explicitly
5787
// asks that we do NOT pass the project in the policy name (e.g. use alertPolicies/utilization).
5888
// Reference: https://docs.cloud.google.com/monitoring/api/ref_v3/rest/v1/projects.dashboards#incidentlist
59-
if strings.HasPrefix(ref, "alertPolicies/") {
60-
parts := strings.Split(ref, "/")
89+
//
90+
// NOTE: This is a design mistake in the KCC API, but we have to support it for legacy compatibility.
91+
// The mistake is this: Just because the GCP API requires a particular format, that does not mean
92+
// that we should mirror that format in the Ref. KCC is supposed to be consistent, regardless of
93+
// the underlying GCP API.
94+
if strings.HasPrefix(normalized, "alertPolicies/") {
95+
parts := strings.Split(normalized, "/")
6196
if len(parts) == 2 && parts[1] != "" {
6297
i.Project = ""
6398
i.AlertPolicy = parts[1]
6499
return nil
65100
}
66101
}
67102

68-
parsed, match, err := MonitoringAlertPolicyIdentityFormat.Parse(ref)
103+
parsed, match, err := MonitoringAlertPolicyIdentityFormat.Parse(normalized)
69104
if err != nil {
70105
return fmt.Errorf("format of MonitoringAlertPolicy external=%q was not known (use %s): %w", ref, MonitoringAlertPolicyIdentityFormat.CanonicalForm(), err)
71106
}
@@ -81,11 +116,12 @@ func (i *MonitoringAlertPolicyIdentity) Host() string {
81116
return MonitoringAlertPolicyIdentityFormat.Host()
82117
}
83118

84-
func getIdentityFromMonitoringAlertPolicySpec(ctx context.Context, reader client.Reader, obj client.Object) (*MonitoringAlertPolicyIdentity, error) {
85-
resourceID, err := refs.GetResourceID(obj)
86-
if err != nil {
87-
return nil, fmt.Errorf("cannot resolve resource ID: %w", err)
88-
}
119+
func (i *MonitoringAlertPolicyIdentity) HasIdentitySpecified() bool {
120+
return i.AlertPolicy != ""
121+
}
122+
123+
func getIdentityFromMonitoringAlertPolicySpec(ctx context.Context, reader client.Reader, obj *MonitoringAlertPolicy) (*MonitoringAlertPolicyIdentity, error) {
124+
resourceID := common.ValueOf(obj.Spec.ResourceID)
89125

90126
projectID, err := refs.ResolveProjectID(ctx, reader, obj)
91127
if err != nil {

apis/monitoring/v1beta1/monitoringalertpolicy_reference.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package v1beta1
1717
import (
1818
"context"
1919

20+
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common"
2021
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common/identity"
2122
refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
2223
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -41,7 +42,7 @@ type MonitoringAlertPolicyRef struct {
4142
}
4243

4344
func init() {
44-
refs.Register(&MonitoringAlertPolicyRef{}, nil)
45+
refs.Register(&MonitoringAlertPolicyRef{}, &MonitoringAlertPolicy{})
4546
}
4647

4748
func (r *MonitoringAlertPolicyRef) GetGVK() schema.GroupVersionKind {
@@ -83,7 +84,11 @@ func (r *MonitoringAlertPolicyRef) ParseExternalToIdentity() (identity.Identity,
8384

8485
func (r *MonitoringAlertPolicyRef) Normalize(ctx context.Context, reader client.Reader, defaultNamespace string) error {
8586
fallback := func(u *unstructured.Unstructured) string {
86-
identity, err := getIdentityFromMonitoringAlertPolicySpec(ctx, reader, u)
87+
typedObj, err := common.ToStructuredType[*MonitoringAlertPolicy](u)
88+
if err != nil {
89+
return ""
90+
}
91+
identity, err := getIdentityFromMonitoringAlertPolicySpec(ctx, reader, typedObj)
8792
if err != nil {
8893
return ""
8994
}

pkg/cais/caistesting/testing.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ func NormalizeDynamicIDs(s string) string {
108108
if idx := strings.Index(line, "/budgets/"); idx != -1 {
109109
lines[i] = line[:idx+len("/budgets/")] + "billingbudgetsbudget-${uniqueId}"
110110
}
111+
// Normalize Monitoring Alert Policy numeric IDs
112+
if idx := strings.Index(line, "/alertPolicies/"); idx != -1 {
113+
lines[i] = line[:idx+len("/alertPolicies/")]
114+
}
111115
// Normalize reCAPTCHA Enterprise Key IDs: projects/.../keys/<keyId>
112116
if idx := strings.Index(line, "/keys/"); idx != -1 && strings.Contains(line, "recaptchaenterprise") {
113117
lines[i] = line[:idx+len("/keys/")] + "${keyID}"

pkg/cli/cmd/export/parameters/parameters.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ type Parameters struct {
4444
// DisableDirectExport can be set to true to bypass direct-reconciliation exporters.
4545
DisableDirectExport bool
4646

47-
// GRPCUnaryClientInterceptor is the GRPC interceptor for use in tests.
47+
// GRPCUnaryClientInterceptor allows for overriding the default GRPC Unary Interceptor
4848
GRPCUnaryClientInterceptor grpc.UnaryClientInterceptor
4949
}
5050

5151
func (p *Parameters) NewControllerConfig(ctx context.Context) (*config.ControllerConfig, error) {
5252
c := &config.ControllerConfig{
53-
HTTPClient: p.HTTPClient,
54-
UserAgent: gcp.KCCUserAgent(),
53+
HTTPClient: p.HTTPClient,
54+
UserAgent: gcp.KCCUserAgent(),
55+
GRPCUnaryClientInterceptor: p.GRPCUnaryClientInterceptor,
5556
}
5657
if p.GCPAccessToken != "" {
5758
c.GCPTokenSource = oauth2.StaticTokenSource(

pkg/cli/powertools/cais/cmd_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,17 @@ func TestGoldenIdentitiesYamlFiles(t *testing.T) {
268268
tempReader := cais.NewInMemoryReader(scheme, []*unstructured.Unstructured{u})
269269
if id, err := resource.GetIdentity(ctx, tempReader); err == nil && id != nil {
270270
if sgId, ok := id.(identity.ServerGeneratedIdentity); ok {
271-
if !sgId.HasIdentitySpecified() && gk.Kind == "KMSKeyHandle" {
272-
placeholder := "${keyHandleID}"
273-
if err := unstructured.SetNestedField(u.Object, placeholder, "spec", "resourceID"); err != nil {
274-
t.Fatalf("failed to set spec.resourceID for %s: %v", u.GetName(), err)
271+
if !sgId.HasIdentitySpecified() {
272+
if gk.Kind == "KMSKeyHandle" {
273+
placeholder := "${keyHandleID}"
274+
if err := unstructured.SetNestedField(u.Object, placeholder, "spec", "resourceID"); err != nil {
275+
t.Fatalf("failed to set spec.resourceID for %s: %v", u.GetName(), err)
276+
}
277+
} else if gk.Kind == "MonitoringAlertPolicy" {
278+
placeholder := "alertpolicy-placeholder"
279+
if err := unstructured.SetNestedField(u.Object, placeholder, "spec", "resourceID"); err != nil {
280+
t.Fatalf("failed to set spec.resourceID for %s: %v", u.GetName(), err)
281+
}
275282
}
276283
}
277284
}

pkg/controller/direct/monitoring/monitoringalertpolicy_controller.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
pb "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb"
2424
"google.golang.org/protobuf/proto"
2525
"google.golang.org/protobuf/types/known/fieldmaskpb"
26+
"google.golang.org/protobuf/types/known/wrapperspb"
2627
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2728
"k8s.io/apimachinery/pkg/runtime"
2829
"k8s.io/klog/v2"
@@ -34,6 +35,8 @@ import (
3435
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
3536
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
3637
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/tags"
38+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/export"
39+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/label"
3740
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/mappers"
3841
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/structuredreporting"
3942
)
@@ -84,12 +87,12 @@ func (m *alertPolicyModel) AdapterForObject(ctx context.Context, op *directbase.
8487
return nil, fmt.Errorf("error converting to %T: %w", obj, err)
8588
}
8689

87-
id, err := obj.GetIdentity(ctx, kube)
88-
if err != nil {
90+
if err := common.NormalizeReferences(ctx, kube, obj, nil); err != nil {
8991
return nil, err
9092
}
9193

92-
if err := common.NormalizeReferences(ctx, kube, obj, nil); err != nil {
94+
id, err := obj.GetIdentity(ctx, kube)
95+
if err != nil {
9396
return nil, err
9497
}
9598

@@ -99,6 +102,8 @@ func (m *alertPolicyModel) AdapterForObject(ctx context.Context, op *directbase.
99102
return nil, mapCtx.Err()
100103
}
101104

105+
desiredProto.UserLabels = label.NewGCPLabelsFromK8sLabels(u.GetLabels())
106+
102107
return &alertPolicyAdapter{
103108
id: id.(*krm.MonitoringAlertPolicyIdentity),
104109
desired: desiredProto,
@@ -113,7 +118,11 @@ func (m *alertPolicyModel) AdapterForURL(ctx context.Context, url string) (direc
113118
}
114119

115120
id := &krm.MonitoringAlertPolicyIdentity{}
116-
if err := id.FromExternal(strings.TrimPrefix(url, "//monitoring.googleapis.com/")); err != nil {
121+
if err := id.FromExternal(url); err != nil {
122+
return nil, nil
123+
}
124+
125+
if !id.HasIdentitySpecified() {
117126
return nil, nil
118127
}
119128

@@ -135,7 +144,7 @@ func (m *alertPolicyModel) AdapterForURL(ctx context.Context, url string) (direc
135144

136145
// Find implements the Adapter interface.
137146
func (a *alertPolicyAdapter) Find(ctx context.Context) (bool, error) {
138-
if a.id.AlertPolicy == "" {
147+
if !a.id.HasIdentitySpecified() {
139148
return false, nil
140149
}
141150

@@ -157,11 +166,7 @@ func (a *alertPolicyAdapter) Find(ctx context.Context) (bool, error) {
157166

158167
// Delete implements the Adapter interface.
159168
func (a *alertPolicyAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) {
160-
exists, err := a.Find(ctx)
161-
if err != nil {
162-
return false, err
163-
}
164-
if !exists {
169+
if !a.id.HasIdentitySpecified() {
165170
return false, nil
166171
}
167172

@@ -181,6 +186,10 @@ func (a *alertPolicyAdapter) Delete(ctx context.Context, deleteOp *directbase.De
181186

182187
// Create implements the Adapter interface.
183188
func (a *alertPolicyAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
189+
if a.id.HasIdentitySpecified() {
190+
return fmt.Errorf("cannot create alert policy %q: server-generated identity is already specified", a.id.String())
191+
}
192+
184193
u := createOp.GetUnstructured()
185194

186195
log := klog.FromContext(ctx)
@@ -200,11 +209,6 @@ func (a *alertPolicyAdapter) Create(ctx context.Context, createOp *directbase.Cr
200209
}
201210
log.V(2).Info("created alert policy", "alertPolicy", created)
202211

203-
resourceID := lastComponent(created.Name)
204-
if err := unstructured.SetNestedField(u.Object, resourceID, "spec", "resourceID"); err != nil {
205-
return fmt.Errorf("setting spec.resourceID: %w", err)
206-
}
207-
208212
return a.updateStatus(ctx, createOp, created)
209213
}
210214

@@ -256,6 +260,8 @@ func (a *alertPolicyAdapter) Export(ctx context.Context) (*unstructured.Unstruct
256260
return nil, fmt.Errorf("error converting alertPolicy from API %w", err)
257261
}
258262

263+
obj.Spec.ResourceID = direct.LazyPtr(a.id.AlertPolicy)
264+
259265
uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
260266
if err != nil {
261267
return nil, err
@@ -265,6 +271,9 @@ func (a *alertPolicyAdapter) Export(ctx context.Context) (*unstructured.Unstruct
265271
u.SetName(a.id.AlertPolicy)
266272
u.SetGroupVersionKind(krm.MonitoringAlertPolicyGVK)
267273

274+
export.SetProjectID(u, a.id.Project)
275+
export.SetLabels(u, a.actual.UserLabels)
276+
268277
return u, nil
269278
}
270279

@@ -287,12 +296,15 @@ func compareAlertPolicy(ctx context.Context, actual, desired *pb.AlertPolicy) (*
287296
return nil, nil, err
288297
}
289298
maskedActual.Name = actual.Name
299+
maskedActual.UserLabels = actual.UserLabels
290300

291301
populateDefaults := func(obj *pb.AlertPolicy) {
292-
// Even if empty, it's a good pattern to define and populate GCP/server defaults here
302+
if obj.Enabled == nil {
303+
obj.Enabled = &wrapperspb.BoolValue{Value: true}
304+
}
293305
}
294306

295-
clonedDesired := proto.Clone(desired).(*pb.AlertPolicy)
307+
clonedDesired := proto.CloneOf(desired)
296308
clonedDesired.Name = actual.Name
297309

298310
populateDefaults(clonedDesired)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
apiVersion: monitoring.cnrm.cloud.google.com/v1beta1
2+
kind: MonitoringAlertPolicy
3+
metadata:
4+
annotations:
5+
cnrm.cloud.google.com/project-id: ${projectId}
6+
labels:
7+
cnrm-test: "true"
8+
managed-by-cnrm: "true"
9+
name: ${alertPolicyId}
10+
spec:
11+
combiner: OR
12+
conditions:
13+
- conditionThreshold:
14+
aggregations:
15+
- alignmentPeriod: 1m0s
16+
crossSeriesReducer: REDUCE_MEAN
17+
groupByFields:
18+
- project
19+
- resource.label.instance_id
20+
- resource.label.zone
21+
perSeriesAligner: ALIGN_MAX
22+
comparison: COMPARISON_LT
23+
duration: 15m0s
24+
filter: metric.type="compute.googleapis.com/instance/cpu/utilization" AND resource.type="gce_instance"
25+
thresholdValue: 0.1
26+
trigger:
27+
count: 3
28+
displayName: Very low CPU usage
29+
displayName: Updated Test Alert Policy
30+
documentation:
31+
content: |-
32+
“Just the place for a Snark!” the Bellman cried,
33+
As he monitored his resources with care;
34+
Supporting each metric on the top of the tide
35+
By a finger entwined in his hair.
36+
37+
“Just the place for a Snark! I have measured it twice:
38+
That alone should discourage the crew.
39+
Just the place for a Snark! I have measured it thrice:
40+
What I measure three times is true.”
41+
enabled: false
42+
notificationChannels:
43+
- external: projects/${projectId}/notificationChannels/${notificationChannelID}
44+
- external: projects/${projectId}/notificationChannels/${notificationChannelID}
45+
resourceID: ${alertPolicyId}
46+
severity: ERROR

0 commit comments

Comments
 (0)