@@ -20,7 +20,7 @@ import (
2020 "k8s.io/apimachinery/pkg/runtime"
2121 "k8s.io/apimachinery/pkg/types"
2222 "k8s.io/client-go/rest"
23- "k8s.io/client-go/tools/record "
23+ "k8s.io/client-go/tools/events "
2424 ctrl "sigs.k8s.io/controller-runtime"
2525 "sigs.k8s.io/controller-runtime/pkg/client"
2626 "sigs.k8s.io/controller-runtime/pkg/cluster"
@@ -92,6 +92,23 @@ const (
9292 reasonNetworkFailedToCreate = "NetworkFailedToCreate"
9393)
9494
95+ // Event action strings name the controller operation an event describes. The
96+ // events.k8s.io API separates the machine-readable action (what the controller
97+ // was doing, UpperCamelCase) from the reason (the outcome), so the reason
98+ // constants carry the failure mode while these name the operation.
99+ const (
100+ eventActionResolvingReferencedData = "ResolvingReferencedData"
101+ eventActionRemovingSchedulingGate = "RemovingSchedulingGate"
102+ eventActionClaimingQuota = "ClaimingQuota"
103+ eventActionReleasingQuota = "ReleasingQuota"
104+ )
105+
106+ // maxEventNoteLen bounds an event note's length. events.k8s.io/v1 rejects notes
107+ // longer than 1024 characters server-side, and the awaiting-propagation note
108+ // joins an unbounded list of missing companions, so the note is truncated
109+ // before emission to keep events from being silently dropped.
110+ const maxEventNoteLen = 1024
111+
95112// instanceTypeD1Standard2 is the platform instance type name for the
96113// 1 vCPU / 2 GiB size used as the catalog baseline for quota accounting.
97114const instanceTypeD1Standard2 = "datumcloud/d1-standard-2"
@@ -190,7 +207,7 @@ type InstanceReconciler struct {
190207 edgeClusterName string
191208 // recorder emits Kubernetes events on the Instance object for quota failure
192209 // modes so operators can diagnose issues via `kubectl describe`.
193- recorder record .EventRecorder
210+ recorder events .EventRecorder
194211 // projectIDForInstance derives the Milo project ID used for quota
195212 // ResourceClaim management. In Milo mode it returns string(clusterName); in
196213 // single-cell mode it reads the upstream-cluster-name label from the edge
@@ -224,7 +241,7 @@ type InstanceReconciler struct {
224241// +kubebuilder:rbac:groups=compute.datumapis.com,resources=instances/finalizers,verbs=update
225242// +kubebuilder:rbac:groups=quota.miloapis.com,resources=resourceclaims,verbs=get;list;watch;create;delete
226243// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get
227- // +kubebuilder:rbac:groups="" ,resources=events,verbs=create;patch
244+ // +kubebuilder:rbac:groups=events.k8s.io ,resources=events,verbs=create;patch
228245
229246//nolint:gocyclo // conditions are reconciled, persisted, then returned as errors; the ordered pipeline is inherently branchy
230247func (r * InstanceReconciler ) Reconcile (ctx context.Context , req mcreconcile.Request ) (_ ctrl.Result , err error ) {
@@ -588,7 +605,8 @@ func (r *InstanceReconciler) reconcileReferencedDataCondition(
588605 computev1alpha .ReferencedDataReasonAwaitingPropagation , msg )
589606 if prevReason != computev1alpha .ReferencedDataReasonAwaitingPropagation {
590607 r .emitEvent (instance , corev1 .EventTypeWarning ,
591- computev1alpha .ReferencedDataReasonAwaitingPropagation , msg )
608+ computev1alpha .ReferencedDataReasonAwaitingPropagation ,
609+ eventActionResolvingReferencedData , msg )
592610 }
593611 return referencedDataResult {conditionChanged : changed }, 0 , nil
594612 }
@@ -902,16 +920,23 @@ func (r *InstanceReconciler) observeGateWaitDuration(instance *computev1alpha.In
902920func (r * InstanceReconciler ) emitReferencedDataClearedEvent (instance * computev1alpha.Instance ) {
903921 r .emitEvent (instance , corev1 .EventTypeNormal ,
904922 computev1alpha .ReferencedDataReasonReady ,
923+ eventActionRemovingSchedulingGate ,
905924 "All referenced companion ConfigMaps/Secrets are present; ReferencedData gate cleared" )
906925}
907926
908927// emitEvent emits a Kubernetes event if a recorder is available. Guard against
909928// a nil recorder so that unit tests that don't wire up a recorder don't panic.
910- func (r * InstanceReconciler ) emitEvent (obj * computev1alpha.Instance , eventType , reason , message string ) {
929+ // The pre-built message is passed as the "%s" argument, never as the note format
930+ // string itself, because the events API treats the note as a printf format and
931+ // the quota messages embed backend error text that can contain literal '%'.
932+ func (r * InstanceReconciler ) emitEvent (obj * computev1alpha.Instance , eventType , reason , action , message string ) {
911933 if r .recorder == nil {
912934 return
913935 }
914- r .recorder .Event (obj , eventType , reason , message )
936+ if len (message ) > maxEventNoteLen {
937+ message = strings .ToValidUTF8 (message [:maxEventNoteLen - 3 ], "" ) + "..."
938+ }
939+ r .recorder .Eventf (obj , nil , eventType , reason , action , "%s" , message )
915940}
916941
917942// reconcileDeletion handles quota-claim cleanup when an Instance is being
@@ -936,8 +961,9 @@ func (r *InstanceReconciler) reconcileDeletion(ctx context.Context, cl client.Cl
936961 // claim will count against project budget until Milo's TTL/GC removes it.
937962 log .FromContext (ctx ).Error (err , "project identity unresolvable during deletion; ResourceClaim may be orphaned — budget leak possible" ,
938963 "instance" , instance .Name , "namespace" , instance .Namespace )
939- r .recorder . Event (instance , corev1 .EventTypeWarning ,
964+ r .emitEvent (instance , corev1 .EventTypeWarning ,
940965 "QuotaClaimOrphaned" ,
966+ eventActionReleasingQuota ,
941967 "Skipping ResourceClaim cleanup: project identity could not be resolved; claim may be orphaned in Milo project control plane" )
942968 quotametrics .ClaimOrphanedTotal .Inc ()
943969 }
@@ -1055,8 +1081,9 @@ func (r *InstanceReconciler) reconcileQuotaCondition(ctx context.Context, cluste
10551081 Message : "ResourceClaim is pending: no AllowanceBucket configured for this project" ,
10561082 ObservedGeneration : instance .Generation ,
10571083 })
1058- r .recorder . Event (instance , corev1 .EventTypeWarning ,
1084+ r .emitEvent (instance , corev1 .EventTypeWarning ,
10591085 computev1alpha .InstanceQuotaGrantedReasonNoBudget ,
1086+ eventActionClaimingQuota ,
10601087 "ResourceClaim pending: no AllowanceBucket configured for this project" )
10611088 quotametrics .EvalFailuresTotal .WithLabelValues (quotametrics .ReasonNoBudget ).Inc ()
10621089 return changed , claimErr
@@ -1281,8 +1308,8 @@ func (r *InstanceReconciler) reconcileQuotaClaim(ctx context.Context, clusterNam
12811308 // structured condition + warning event + error return, instead of
12821309 // silently parking the instance at PendingEvaluation.
12831310 msg := fmt .Sprintf ("Could not resolve project ID: %v" , err )
1284- r .recorder . Event (instance , corev1 .EventTypeWarning ,
1285- computev1alpha .InstanceQuotaGrantedReasonProjectIDUnresolvable , msg )
1311+ r .emitEvent (instance , corev1 .EventTypeWarning ,
1312+ computev1alpha .InstanceQuotaGrantedReasonProjectIDUnresolvable , eventActionClaimingQuota , msg )
12861313 quotametrics .EvalFailuresTotal .WithLabelValues (quotametrics .ReasonProjectIDUnresolvable ).Inc ()
12871314 return & metav1.Condition {
12881315 Type : computev1alpha .InstanceQuotaGranted ,
@@ -1295,8 +1322,8 @@ func (r *InstanceReconciler) reconcileQuotaClaim(ctx context.Context, clusterNam
12951322 projectClient , err := r .quotaClientManager .ClientForProject (ctx , projectID , r .scheme )
12961323 if err != nil {
12971324 msg := fmt .Sprintf ("Failed to build quota client for project %q: %v" , projectID , err )
1298- r .recorder . Event (instance , corev1 .EventTypeWarning ,
1299- computev1alpha .InstanceQuotaGrantedReasonBackendUnavailable , msg )
1325+ r .emitEvent (instance , corev1 .EventTypeWarning ,
1326+ computev1alpha .InstanceQuotaGrantedReasonBackendUnavailable , eventActionClaimingQuota , msg )
13001327 quotametrics .EvalFailuresTotal .WithLabelValues (quotametrics .ReasonBackendUnavailable ).Inc ()
13011328 return & metav1.Condition {
13021329 Type : computev1alpha .InstanceQuotaGranted ,
@@ -1309,8 +1336,8 @@ func (r *InstanceReconciler) reconcileQuotaClaim(ctx context.Context, clusterNam
13091336 claimNamespace , err := r .resolveProjectNamespace (ctx , clusterName , instance )
13101337 if err != nil {
13111338 msg := fmt .Sprintf ("Could not resolve project namespace: %v" , err )
1312- r .recorder . Event (instance , corev1 .EventTypeWarning ,
1313- computev1alpha .InstanceQuotaGrantedReasonProjectIDUnresolvable , msg )
1339+ r .emitEvent (instance , corev1 .EventTypeWarning ,
1340+ computev1alpha .InstanceQuotaGrantedReasonProjectIDUnresolvable , eventActionClaimingQuota , msg )
13141341 quotametrics .EvalFailuresTotal .WithLabelValues (quotametrics .ReasonProjectIDUnresolvable ).Inc ()
13151342 return & metav1.Condition {
13161343 Type : computev1alpha .InstanceQuotaGranted ,
@@ -1381,8 +1408,8 @@ func (r *InstanceReconciler) reconcileQuotaClaim(ctx context.Context, clusterNam
13811408 }
13821409 // GET itself failed — treat as backend unavailable.
13831410 msg := fmt .Sprintf ("Quota backend unreachable getting ResourceClaim: %v" , err )
1384- r .recorder . Event (instance , corev1 .EventTypeWarning ,
1385- computev1alpha .InstanceQuotaGrantedReasonBackendUnavailable , msg )
1411+ r .emitEvent (instance , corev1 .EventTypeWarning ,
1412+ computev1alpha .InstanceQuotaGrantedReasonBackendUnavailable , eventActionClaimingQuota , msg )
13861413 quotametrics .EvalFailuresTotal .WithLabelValues (quotametrics .ReasonBackendUnavailable ).Inc ()
13871414 return & metav1.Condition {
13881415 Type : computev1alpha .InstanceQuotaGranted ,
@@ -1431,7 +1458,7 @@ func (r *InstanceReconciler) classifyCreateError(
14311458 msg = fmt .Sprintf ("Quota backend unreachable creating ResourceClaim: %v" , err )
14321459 }
14331460
1434- r .recorder . Event (instance , corev1 .EventTypeWarning , reason , msg )
1461+ r .emitEvent (instance , corev1 .EventTypeWarning , reason , eventActionClaimingQuota , msg )
14351462 quotametrics .EvalFailuresTotal .WithLabelValues (metricLabel ).Inc ()
14361463 return & metav1.Condition {
14371464 Type : computev1alpha .InstanceQuotaGranted ,
@@ -1854,10 +1881,7 @@ func (r *InstanceReconciler) SetupWithManager(
18541881) error {
18551882 r .mgr = mgr
18561883 r .scheme = mgr .GetLocalManager ().GetScheme ()
1857- //nolint:staticcheck // GetEventRecorder (new events API) has an incompatible Eventf
1858- // signature (requires related object + action args) that would require migrating
1859- // all emit sites. GetEventRecorderFor remains correct; migration is deferred.
1860- r .recorder = mgr .GetLocalManager ().GetEventRecorderFor ("instance-controller" )
1884+ r .recorder = mgr .GetLocalManager ().GetEventRecorder ("instance-controller" )
18611885 r .edgeClusterName = edgeClusterName
18621886 r .projectIDForInstance = projectIDForInstance
18631887 r .projectNamespaceForInstance = projectNamespaceForInstance
0 commit comments