From 373408f806e1d6dfb61ddc01f0a457a24dc93cdb Mon Sep 17 00:00:00 2001 From: Aravind Warrier Date: Tue, 28 Jul 2026 16:07:12 -0400 Subject: [PATCH 1/6] feat(api): Define TriageRun CRD --- PROJECT | 8 + api/v2/triagerun_types.go | 183 +++++++++++++++ api/v2/zz_generated.deepcopy.go | 211 +++++++++++++++++ .../crd/bases/apps.wandb.com_triageruns.yaml | 212 ++++++++++++++++++ config/crd/bases/kustomization.yaml | 1 + config/dev-common/delete-triageruns-crd.yaml | 5 + config/dev-common/kustomization.yaml | 6 + config/rbac/kustomization.yaml | 4 +- config/rbac/triagerun_admin_role.yaml | 25 +++ config/rbac/triagerun_editor_role.yaml | 31 +++ config/rbac/triagerun_viewer_role.yaml | 25 +++ config/samples/apps_v2_triagerun.yaml | 11 + config/samples/kustomization.yaml | 1 + internal/crdinstaller/compose_test.go | 19 +- .../operator/apps.wandb.com_triageruns.yaml | 212 ++++++++++++++++++ 15 files changed, 949 insertions(+), 5 deletions(-) create mode 100644 api/v2/triagerun_types.go create mode 100644 config/crd/bases/apps.wandb.com_triageruns.yaml create mode 100644 config/dev-common/delete-triageruns-crd.yaml create mode 100644 config/rbac/triagerun_admin_role.yaml create mode 100644 config/rbac/triagerun_editor_role.yaml create mode 100644 config/rbac/triagerun_viewer_role.yaml create mode 100644 config/samples/apps_v2_triagerun.yaml create mode 100644 internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml diff --git a/PROJECT b/PROJECT index a801132e..2911d122 100644 --- a/PROJECT +++ b/PROJECT @@ -49,4 +49,12 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + domain: wandb.com + group: apps + kind: TriageRun + path: github.com/wandb/operator/api/v2 + version: v2 version: "3" diff --git a/api/v2/triagerun_types.go b/api/v2/triagerun_types.go new file mode 100644 index 00000000..4ec9b6fd --- /dev/null +++ b/api/v2/triagerun_types.go @@ -0,0 +1,183 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TriageRunPhase describes the execution lifecycle of a TriageRun. A +// Succeeded run means that the diagnostic command completed successfully; it +// does not mean that every diagnostic check passed. +// +kubebuilder:validation:Enum=Pending;Running;Succeeded;Failed +type TriageRunPhase string + +const ( + TriageRunPhasePending TriageRunPhase = "Pending" + TriageRunPhaseRunning TriageRunPhase = "Running" + TriageRunPhaseSucceeded TriageRunPhase = "Succeeded" + TriageRunPhaseFailed TriageRunPhase = "Failed" +) + +// TriageSeverity is the verdict emitted by an individual diagnostic check. +// +kubebuilder:validation:Enum=pass;warn;fail;error +type TriageSeverity string + +const ( + TriageSeverityPass TriageSeverity = "pass" + TriageSeverityWarn TriageSeverity = "warn" + TriageSeverityFail TriageSeverity = "fail" + TriageSeverityError TriageSeverity = "error" +) + +// TriageApplicationReference identifies an Application in the TriageRun's +// namespace. +type TriageApplicationReference struct { + // Name is the name of the Application to diagnose. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` +} + +// TriageRunSpec defines one immutable request to run a diagnostic action. +// Creating another run requires creating another TriageRun. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec is immutable" +type TriageRunSpec struct { + // ApplicationRef identifies the Application to diagnose. Cross-namespace + // references are intentionally unsupported. + ApplicationRef TriageApplicationReference `json:"applicationRef"` + + // Action selects a triage action declared by the referenced Application. + // +kubebuilder:default=default + // +kubebuilder:validation:MinLength=1 + Action string `json:"action,omitempty"` +} + +// TriageResolvedExecution records the concrete execution selected from the +// Application at reconciliation time. It is an audit snapshot, not user input. +type TriageResolvedExecution struct { + // ApplicationGeneration is the Application generation used to resolve this + // execution. + ApplicationGeneration int64 `json:"applicationGeneration,omitempty"` + + // ContainerName is the application container whose image and runtime + // configuration were selected. + ContainerName string `json:"containerName,omitempty"` + + // Image is the concrete container image used by the diagnostic Job. + Image string `json:"image,omitempty"` + + // Command is the resolved container entrypoint. + Command []string `json:"command,omitempty"` + + // Args are the resolved arguments passed to Command. + Args []string `json:"args,omitempty"` + + // TimeoutSeconds is the resolved execution deadline. + TimeoutSeconds int64 `json:"timeoutSeconds,omitempty"` +} + +// TriageRunSummary contains aggregate verdict counts for a completed run. +type TriageRunSummary struct { + Total int32 `json:"total,omitempty"` + Pass int32 `json:"pass,omitempty"` + Warn int32 `json:"warn,omitempty"` + Fail int32 `json:"fail,omitempty"` + Error int32 `json:"error,omitempty"` + + // OverallSeverity is the most severe check verdict in the run. + OverallSeverity TriageSeverity `json:"overallSeverity,omitempty"` +} + +// TriageCheckResult contains one structured record emitted by the diagnostic +// command. +type TriageCheckResult struct { + Name string `json:"name"` + + // Umbrella is an optional logical grouping for related checks. + Umbrella string `json:"umbrella,omitempty"` + + Severity TriageSeverity `json:"severity"` + Message string `json:"message,omitempty"` + + // Evidence preserves application-defined structured evidence. + // +kubebuilder:pruning:PreserveUnknownFields + Evidence *apiextensionsv1.JSON `json:"evidence,omitempty"` + + Remediation string `json:"remediation,omitempty"` + + StartedAt *metav1.Time `json:"startedAt,omitempty"` + EndedAt *metav1.Time `json:"endedAt,omitempty"` + + DurationMilliseconds int64 `json:"durationMs,omitempty"` +} + +// TriageRunStatus defines the observed execution state and diagnostic output. +type TriageRunStatus struct { + Phase TriageRunPhase `json:"phase,omitempty"` + + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // JobRef identifies the Kubernetes Job executing this run. + JobRef *corev1.LocalObjectReference `json:"jobRef,omitempty"` + + // ResolvedExecution is the execution snapshot selected from the referenced + // Application. + ResolvedExecution *TriageResolvedExecution `json:"resolvedExecution,omitempty"` + + StartedAt *metav1.Time `json:"startedAt,omitempty"` + CompletedAt *metav1.Time `json:"completedAt,omitempty"` + + Summary *TriageRunSummary `json:"summary,omitempty"` + Results []TriageCheckResult `json:"results,omitempty"` + + // Conditions represent the latest available observations of the run. + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Application",type=string,JSONPath=`.spec.applicationRef.name` +// +kubebuilder:printcolumn:name="Action",type=string,JSONPath=`.spec.action` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Severity",type=string,JSONPath=`.status.summary.overallSeverity` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// TriageRun is one immutable request to diagnose an Application. +type TriageRun struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TriageRunSpec `json:"spec,omitempty"` + Status TriageRunStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// TriageRunList contains a list of TriageRun. +type TriageRunList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TriageRun `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TriageRun{}, &TriageRunList{}) +} diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 76ab543b..70bb0399 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -29,6 +29,7 @@ import ( "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" apisv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -1478,6 +1479,216 @@ func (in *TelemetryInfraStatus) DeepCopy() *TelemetryInfraStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageApplicationReference) DeepCopyInto(out *TriageApplicationReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageApplicationReference. +func (in *TriageApplicationReference) DeepCopy() *TriageApplicationReference { + if in == nil { + return nil + } + out := new(TriageApplicationReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageCheckResult) DeepCopyInto(out *TriageCheckResult) { + *out = *in + if in.Evidence != nil { + in, out := &in.Evidence, &out.Evidence + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + if in.EndedAt != nil { + in, out := &in.EndedAt, &out.EndedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageCheckResult. +func (in *TriageCheckResult) DeepCopy() *TriageCheckResult { + if in == nil { + return nil + } + out := new(TriageCheckResult) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageResolvedExecution) DeepCopyInto(out *TriageResolvedExecution) { + *out = *in + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageResolvedExecution. +func (in *TriageResolvedExecution) DeepCopy() *TriageResolvedExecution { + if in == nil { + return nil + } + out := new(TriageResolvedExecution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageRun) DeepCopyInto(out *TriageRun) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageRun. +func (in *TriageRun) DeepCopy() *TriageRun { + if in == nil { + return nil + } + out := new(TriageRun) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TriageRun) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageRunList) DeepCopyInto(out *TriageRunList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TriageRun, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageRunList. +func (in *TriageRunList) DeepCopy() *TriageRunList { + if in == nil { + return nil + } + out := new(TriageRunList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TriageRunList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageRunSpec) DeepCopyInto(out *TriageRunSpec) { + *out = *in + out.ApplicationRef = in.ApplicationRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageRunSpec. +func (in *TriageRunSpec) DeepCopy() *TriageRunSpec { + if in == nil { + return nil + } + out := new(TriageRunSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageRunStatus) DeepCopyInto(out *TriageRunStatus) { + *out = *in + if in.JobRef != nil { + in, out := &in.JobRef, &out.JobRef + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.ResolvedExecution != nil { + in, out := &in.ResolvedExecution, &out.ResolvedExecution + *out = new(TriageResolvedExecution) + (*in).DeepCopyInto(*out) + } + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + if in.CompletedAt != nil { + in, out := &in.CompletedAt, &out.CompletedAt + *out = (*in).DeepCopy() + } + if in.Summary != nil { + in, out := &in.Summary, &out.Summary + *out = new(TriageRunSummary) + **out = **in + } + if in.Results != nil { + in, out := &in.Results, &out.Results + *out = make([]TriageCheckResult, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageRunStatus. +func (in *TriageRunStatus) DeepCopy() *TriageRunStatus { + if in == nil { + return nil + } + out := new(TriageRunStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageRunSummary) DeepCopyInto(out *TriageRunSummary) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageRunSummary. +func (in *TriageRunSummary) DeepCopy() *TriageRunSummary { + if in == nil { + return nil + } + out := new(TriageRunSummary) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WBInfraStatus) DeepCopyInto(out *WBInfraStatus) { *out = *in diff --git a/config/crd/bases/apps.wandb.com_triageruns.yaml b/config/crd/bases/apps.wandb.com_triageruns.yaml new file mode 100644 index 00000000..3fb0de30 --- /dev/null +++ b/config/crd/bases/apps.wandb.com_triageruns.yaml @@ -0,0 +1,212 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: triageruns.apps.wandb.com +spec: + group: apps.wandb.com + names: + kind: TriageRun + listKind: TriageRunList + plural: triageruns + singular: triagerun + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.applicationRef.name + name: Application + type: string + - jsonPath: .spec.action + name: Action + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.summary.overallSeverity + name: Severity + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v2 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + action: + default: default + minLength: 1 + type: string + applicationRef: + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + required: + - applicationRef + type: object + x-kubernetes-validations: + - message: spec is immutable + rule: self == oldSelf + status: + properties: + completedAt: + format: date-time + type: string + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + jobRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + observedGeneration: + format: int64 + type: integer + phase: + enum: + - Pending + - Running + - Succeeded + - Failed + type: string + resolvedExecution: + properties: + applicationGeneration: + format: int64 + type: integer + args: + items: + type: string + type: array + command: + items: + type: string + type: array + containerName: + type: string + image: + type: string + timeoutSeconds: + format: int64 + type: integer + type: object + results: + items: + properties: + durationMs: + format: int64 + type: integer + endedAt: + format: date-time + type: string + evidence: + x-kubernetes-preserve-unknown-fields: true + message: + type: string + name: + type: string + remediation: + type: string + severity: + enum: + - pass + - warn + - fail + - error + type: string + startedAt: + format: date-time + type: string + umbrella: + type: string + required: + - name + - severity + type: object + type: array + startedAt: + format: date-time + type: string + summary: + properties: + error: + format: int32 + type: integer + fail: + format: int32 + type: integer + overallSeverity: + enum: + - pass + - warn + - fail + - error + type: string + pass: + format: int32 + type: integer + total: + format: int32 + type: integer + warn: + format: int32 + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/kustomization.yaml b/config/crd/bases/kustomization.yaml index 1ab16d8b..b1777997 100644 --- a/config/crd/bases/kustomization.yaml +++ b/config/crd/bases/kustomization.yaml @@ -1,3 +1,4 @@ resources: - apps.wandb.com_weightsandbiases.yaml - apps.wandb.com_applications.yaml + - apps.wandb.com_triageruns.yaml diff --git a/config/dev-common/delete-triageruns-crd.yaml b/config/dev-common/delete-triageruns-crd.yaml new file mode 100644 index 00000000..808fbe9f --- /dev/null +++ b/config/dev-common/delete-triageruns-crd.yaml @@ -0,0 +1,5 @@ +$patch: delete +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: triageruns.apps.wandb.com diff --git a/config/dev-common/kustomization.yaml b/config/dev-common/kustomization.yaml index 409db598..22ef5b11 100644 --- a/config/dev-common/kustomization.yaml +++ b/config/dev-common/kustomization.yaml @@ -8,6 +8,12 @@ patches: kind: CustomResourceDefinition name: applications.apps.wandb.com path: delete-applications-crd.yaml + - target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: triageruns.apps.wandb.com + path: delete-triageruns-crd.yaml - target: group: apiextensions.k8s.io version: v1 diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 35662c43..dde429c2 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -25,7 +25,9 @@ resources: - application_admin_role.yaml - application_editor_role.yaml - application_viewer_role.yaml +- triagerun_admin_role.yaml +- triagerun_editor_role.yaml +- triagerun_viewer_role.yaml - weightsandbiases_admin_role.yaml - weightsandbiases_editor_role.yaml - weightsandbiases_viewer_role.yaml - diff --git a/config/rbac/triagerun_admin_role.yaml b/config/rbac/triagerun_admin_role.yaml new file mode 100644 index 00000000..c702a22c --- /dev/null +++ b/config/rbac/triagerun_admin_role.yaml @@ -0,0 +1,25 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over TriageRun resources. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: triagerun-admin-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - triageruns + verbs: + - '*' +- apiGroups: + - apps.wandb.com + resources: + - triageruns/status + verbs: + - get diff --git a/config/rbac/triagerun_editor_role.yaml b/config/rbac/triagerun_editor_role.yaml new file mode 100644 index 00000000..2a31161a --- /dev/null +++ b/config/rbac/triagerun_editor_role.yaml @@ -0,0 +1,31 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create and manage TriageRun resources. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: triagerun-editor-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - triageruns + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.wandb.com + resources: + - triageruns/status + verbs: + - get diff --git a/config/rbac/triagerun_viewer_role.yaml b/config/rbac/triagerun_viewer_role.yaml new file mode 100644 index 00000000..f65bfd4d --- /dev/null +++ b/config/rbac/triagerun_viewer_role.yaml @@ -0,0 +1,25 @@ +# This rule is not used by the project operator itself. +# It is provided to allow read-only access to TriageRun resources. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: triagerun-viewer-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - triageruns + verbs: + - get + - list + - watch +- apiGroups: + - apps.wandb.com + resources: + - triageruns/status + verbs: + - get diff --git a/config/samples/apps_v2_triagerun.yaml b/config/samples/apps_v2_triagerun.yaml new file mode 100644 index 00000000..f994ac7d --- /dev/null +++ b/config/samples/apps_v2_triagerun.yaml @@ -0,0 +1,11 @@ +apiVersion: apps.wandb.com/v2 +kind: TriageRun +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: triagerun-sample +spec: + applicationRef: + name: application-sample + action: default diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 12dd6d2f..7e2e1f1f 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -3,4 +3,5 @@ resources: - apps_v1_weightsandbiases.yaml - apps_v2_weightsandbiases.yaml - apps_v2_application.yaml +- apps_v2_triagerun.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/internal/crdinstaller/compose_test.go b/internal/crdinstaller/compose_test.go index 689de64d..8c76065a 100644 --- a/internal/crdinstaller/compose_test.go +++ b/internal/crdinstaller/compose_test.go @@ -69,10 +69,12 @@ func TestComposeOperatorOnly(t *testing.T) { if err != nil { t.Fatalf("compose failed: %v", err) } - if len(crds) != 2 { - t.Fatalf("expected 2 operator CRDs, got %d", len(crds)) + if len(crds) != 3 { + t.Fatalf("expected 3 operator CRDs, got %d", len(crds)) } + names := make(map[string]bool, len(crds)) for _, crd := range crds { + names[crd.Name] = true if got := crd.Annotations["cert-manager.io/inject-ca-from"]; got != validOpts.CertInjectReference { t.Errorf("%s: cert-manager annotation = %q, want %q", crd.Name, got, validOpts.CertInjectReference) } @@ -84,6 +86,15 @@ func TestComposeOperatorOnly(t *testing.T) { } } } + for _, name := range []string{ + "applications.apps.wandb.com", + "triageruns.apps.wandb.com", + "weightsandbiases.apps.wandb.com", + } { + if !names[name] { + t.Errorf("expected operator CRD %s to be included", name) + } + } } func TestComposeIncludesOptionalGroup(t *testing.T) { @@ -93,8 +104,8 @@ func TestComposeIncludesOptionalGroup(t *testing.T) { if err != nil { t.Fatalf("compose failed: %v", err) } - if len(crds) <= 2 { - t.Fatalf("expected >2 CRDs when redis group included, got %d", len(crds)) + if len(crds) <= 3 { + t.Fatalf("expected >3 CRDs when redis group included, got %d", len(crds)) } // Redis CRDs must NOT have the cert-manager annotation we inject for operator CRDs. for _, crd := range crds { diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml new file mode 100644 index 00000000..3fb0de30 --- /dev/null +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml @@ -0,0 +1,212 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: triageruns.apps.wandb.com +spec: + group: apps.wandb.com + names: + kind: TriageRun + listKind: TriageRunList + plural: triageruns + singular: triagerun + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.applicationRef.name + name: Application + type: string + - jsonPath: .spec.action + name: Action + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.summary.overallSeverity + name: Severity + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v2 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + action: + default: default + minLength: 1 + type: string + applicationRef: + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + required: + - applicationRef + type: object + x-kubernetes-validations: + - message: spec is immutable + rule: self == oldSelf + status: + properties: + completedAt: + format: date-time + type: string + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + jobRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + observedGeneration: + format: int64 + type: integer + phase: + enum: + - Pending + - Running + - Succeeded + - Failed + type: string + resolvedExecution: + properties: + applicationGeneration: + format: int64 + type: integer + args: + items: + type: string + type: array + command: + items: + type: string + type: array + containerName: + type: string + image: + type: string + timeoutSeconds: + format: int64 + type: integer + type: object + results: + items: + properties: + durationMs: + format: int64 + type: integer + endedAt: + format: date-time + type: string + evidence: + x-kubernetes-preserve-unknown-fields: true + message: + type: string + name: + type: string + remediation: + type: string + severity: + enum: + - pass + - warn + - fail + - error + type: string + startedAt: + format: date-time + type: string + umbrella: + type: string + required: + - name + - severity + type: object + type: array + startedAt: + format: date-time + type: string + summary: + properties: + error: + format: int32 + type: integer + fail: + format: int32 + type: integer + overallSeverity: + enum: + - pass + - warn + - fail + - error + type: string + pass: + format: int32 + type: integer + total: + format: int32 + type: integer + warn: + format: int32 + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} From e81e9c2c1729755ac52a21f39d9579f399c3809b Mon Sep 17 00:00:00 2001 From: Aravind Warrier Date: Tue, 4 Aug 2026 11:27:41 -0400 Subject: [PATCH 2/6] feat(api): Support multiple TriageRun actions --- api/v2/triagerun_types.go | 49 +++-- api/v2/zz_generated.deepcopy.go | 68 ++++-- .../crd/bases/apps.wandb.com_triageruns.yaml | 193 ++++++++++++------ config/samples/apps_v2_triagerun.yaml | 3 +- .../operator/apps.wandb.com_triageruns.yaml | 193 ++++++++++++------ 5 files changed, 343 insertions(+), 163 deletions(-) diff --git a/api/v2/triagerun_types.go b/api/v2/triagerun_types.go index 4ec9b6fd..7db687ac 100644 --- a/api/v2/triagerun_types.go +++ b/api/v2/triagerun_types.go @@ -54,7 +54,12 @@ type TriageApplicationReference struct { Name string `json:"name"` } -// TriageRunSpec defines one immutable request to run a diagnostic action. +// TriageActionName identifies an action declared by an Application. +// +kubebuilder:validation:MinLength=1 +type TriageActionName string + +// TriageRunSpec defines one immutable request to run one or more diagnostic +// actions for an Application. // Creating another run requires creating another TriageRun. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec is immutable" type TriageRunSpec struct { @@ -62,10 +67,11 @@ type TriageRunSpec struct { // references are intentionally unsupported. ApplicationRef TriageApplicationReference `json:"applicationRef"` - // Action selects a triage action declared by the referenced Application. - // +kubebuilder:default=default - // +kubebuilder:validation:MinLength=1 - Action string `json:"action,omitempty"` + // Actions selects one or more triage actions declared by the referenced + // Application. Each action is executed independently. + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:UniqueItems=true + Actions []TriageActionName `json:"actions"` } // TriageResolvedExecution records the concrete execution selected from the @@ -127,13 +133,15 @@ type TriageCheckResult struct { DurationMilliseconds int64 `json:"durationMs,omitempty"` } -// TriageRunStatus defines the observed execution state and diagnostic output. -type TriageRunStatus struct { - Phase TriageRunPhase `json:"phase,omitempty"` +// TriageActionStatus records the execution and structured diagnostic output +// for one selected action. +type TriageActionStatus struct { + // Action is the selected Application action represented by this status. + Action TriageActionName `json:"action"` - ObservedGeneration int64 `json:"observedGeneration,omitempty"` + Phase TriageRunPhase `json:"phase,omitempty"` - // JobRef identifies the Kubernetes Job executing this run. + // JobRef identifies the Kubernetes Job executing this action. JobRef *corev1.LocalObjectReference `json:"jobRef,omitempty"` // ResolvedExecution is the execution snapshot selected from the referenced @@ -145,6 +153,23 @@ type TriageRunStatus struct { Summary *TriageRunSummary `json:"summary,omitempty"` Results []TriageCheckResult `json:"results,omitempty"` +} + +// TriageRunStatus defines the observed execution state and diagnostic output. +type TriageRunStatus struct { + Phase TriageRunPhase `json:"phase,omitempty"` + + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + StartedAt *metav1.Time `json:"startedAt,omitempty"` + CompletedAt *metav1.Time `json:"completedAt,omitempty"` + + Summary *TriageRunSummary `json:"summary,omitempty"` + + // ActionStatuses contains one entry for every selected action. + // +listType=map + // +listMapKey=action + ActionStatuses []TriageActionStatus `json:"actionStatuses,omitempty"` // Conditions represent the latest available observations of the run. // +listType=map @@ -155,7 +180,7 @@ type TriageRunStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Application",type=string,JSONPath=`.spec.applicationRef.name` -// +kubebuilder:printcolumn:name="Action",type=string,JSONPath=`.spec.action` +// +kubebuilder:printcolumn:name="Actions",type=string,JSONPath=`.spec.actions` // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Severity",type=string,JSONPath=`.status.summary.overallSeverity` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` @@ -165,7 +190,7 @@ type TriageRun struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec TriageRunSpec `json:"spec,omitempty"` + Spec TriageRunSpec `json:"spec"` Status TriageRunStatus `json:"status,omitempty"` } diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 70bb0399..1b7d1952 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -1479,6 +1479,51 @@ func (in *TelemetryInfraStatus) DeepCopy() *TelemetryInfraStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageActionStatus) DeepCopyInto(out *TriageActionStatus) { + *out = *in + if in.JobRef != nil { + in, out := &in.JobRef, &out.JobRef + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.ResolvedExecution != nil { + in, out := &in.ResolvedExecution, &out.ResolvedExecution + *out = new(TriageResolvedExecution) + (*in).DeepCopyInto(*out) + } + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + if in.CompletedAt != nil { + in, out := &in.CompletedAt, &out.CompletedAt + *out = (*in).DeepCopy() + } + if in.Summary != nil { + in, out := &in.Summary, &out.Summary + *out = new(TriageRunSummary) + **out = **in + } + if in.Results != nil { + in, out := &in.Results, &out.Results + *out = make([]TriageCheckResult, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageActionStatus. +func (in *TriageActionStatus) DeepCopy() *TriageActionStatus { + if in == nil { + return nil + } + out := new(TriageActionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TriageApplicationReference) DeepCopyInto(out *TriageApplicationReference) { *out = *in @@ -1552,7 +1597,7 @@ func (in *TriageRun) DeepCopyInto(out *TriageRun) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -1610,6 +1655,11 @@ func (in *TriageRunList) DeepCopyObject() runtime.Object { func (in *TriageRunSpec) DeepCopyInto(out *TriageRunSpec) { *out = *in out.ApplicationRef = in.ApplicationRef + if in.Actions != nil { + in, out := &in.Actions, &out.Actions + *out = make([]TriageActionName, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageRunSpec. @@ -1625,16 +1675,6 @@ func (in *TriageRunSpec) DeepCopy() *TriageRunSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TriageRunStatus) DeepCopyInto(out *TriageRunStatus) { *out = *in - if in.JobRef != nil { - in, out := &in.JobRef, &out.JobRef - *out = new(v1.LocalObjectReference) - **out = **in - } - if in.ResolvedExecution != nil { - in, out := &in.ResolvedExecution, &out.ResolvedExecution - *out = new(TriageResolvedExecution) - (*in).DeepCopyInto(*out) - } if in.StartedAt != nil { in, out := &in.StartedAt, &out.StartedAt *out = (*in).DeepCopy() @@ -1648,9 +1688,9 @@ func (in *TriageRunStatus) DeepCopyInto(out *TriageRunStatus) { *out = new(TriageRunSummary) **out = **in } - if in.Results != nil { - in, out := &in.Results, &out.Results - *out = make([]TriageCheckResult, len(*in)) + if in.ActionStatuses != nil { + in, out := &in.ActionStatuses, &out.ActionStatuses + *out = make([]TriageActionStatus, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/config/crd/bases/apps.wandb.com_triageruns.yaml b/config/crd/bases/apps.wandb.com_triageruns.yaml index 3fb0de30..a3787bf2 100644 --- a/config/crd/bases/apps.wandb.com_triageruns.yaml +++ b/config/crd/bases/apps.wandb.com_triageruns.yaml @@ -18,8 +18,8 @@ spec: - jsonPath: .spec.applicationRef.name name: Application type: string - - jsonPath: .spec.action - name: Action + - jsonPath: .spec.actions + name: Actions type: string - jsonPath: .status.phase name: Phase @@ -42,10 +42,13 @@ spec: type: object spec: properties: - action: - default: default - minLength: 1 - type: string + actions: + items: + minLength: 1 + type: string + minItems: 1 + type: array + uniqueItems: true applicationRef: properties: name: @@ -55,6 +58,7 @@ spec: - name type: object required: + - actions - applicationRef type: object x-kubernetes-validations: @@ -62,6 +66,119 @@ spec: rule: self == oldSelf status: properties: + actionStatuses: + items: + properties: + action: + minLength: 1 + type: string + completedAt: + format: date-time + type: string + jobRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + phase: + enum: + - Pending + - Running + - Succeeded + - Failed + type: string + resolvedExecution: + properties: + applicationGeneration: + format: int64 + type: integer + args: + items: + type: string + type: array + command: + items: + type: string + type: array + containerName: + type: string + image: + type: string + timeoutSeconds: + format: int64 + type: integer + type: object + results: + items: + properties: + durationMs: + format: int64 + type: integer + endedAt: + format: date-time + type: string + evidence: + x-kubernetes-preserve-unknown-fields: true + message: + type: string + name: + type: string + remediation: + type: string + severity: + enum: + - pass + - warn + - fail + - error + type: string + startedAt: + format: date-time + type: string + umbrella: + type: string + required: + - name + - severity + type: object + type: array + startedAt: + format: date-time + type: string + summary: + properties: + error: + format: int32 + type: integer + fail: + format: int32 + type: integer + overallSeverity: + enum: + - pass + - warn + - fail + - error + type: string + pass: + format: int32 + type: integer + total: + format: int32 + type: integer + warn: + format: int32 + type: integer + type: object + required: + - action + type: object + type: array + x-kubernetes-list-map-keys: + - action + x-kubernetes-list-type: map completedAt: format: date-time type: string @@ -104,13 +221,6 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map - jobRef: - properties: - name: - default: "" - type: string - type: object - x-kubernetes-map-type: atomic observedGeneration: format: int64 type: integer @@ -121,61 +231,6 @@ spec: - Succeeded - Failed type: string - resolvedExecution: - properties: - applicationGeneration: - format: int64 - type: integer - args: - items: - type: string - type: array - command: - items: - type: string - type: array - containerName: - type: string - image: - type: string - timeoutSeconds: - format: int64 - type: integer - type: object - results: - items: - properties: - durationMs: - format: int64 - type: integer - endedAt: - format: date-time - type: string - evidence: - x-kubernetes-preserve-unknown-fields: true - message: - type: string - name: - type: string - remediation: - type: string - severity: - enum: - - pass - - warn - - fail - - error - type: string - startedAt: - format: date-time - type: string - umbrella: - type: string - required: - - name - - severity - type: object - type: array startedAt: format: date-time type: string @@ -205,6 +260,8 @@ spec: type: integer type: object type: object + required: + - spec type: object served: true storage: true diff --git a/config/samples/apps_v2_triagerun.yaml b/config/samples/apps_v2_triagerun.yaml index f994ac7d..a22be88e 100644 --- a/config/samples/apps_v2_triagerun.yaml +++ b/config/samples/apps_v2_triagerun.yaml @@ -8,4 +8,5 @@ metadata: spec: applicationRef: name: application-sample - action: default + actions: + - default diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml index 3fb0de30..a3787bf2 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml @@ -18,8 +18,8 @@ spec: - jsonPath: .spec.applicationRef.name name: Application type: string - - jsonPath: .spec.action - name: Action + - jsonPath: .spec.actions + name: Actions type: string - jsonPath: .status.phase name: Phase @@ -42,10 +42,13 @@ spec: type: object spec: properties: - action: - default: default - minLength: 1 - type: string + actions: + items: + minLength: 1 + type: string + minItems: 1 + type: array + uniqueItems: true applicationRef: properties: name: @@ -55,6 +58,7 @@ spec: - name type: object required: + - actions - applicationRef type: object x-kubernetes-validations: @@ -62,6 +66,119 @@ spec: rule: self == oldSelf status: properties: + actionStatuses: + items: + properties: + action: + minLength: 1 + type: string + completedAt: + format: date-time + type: string + jobRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + phase: + enum: + - Pending + - Running + - Succeeded + - Failed + type: string + resolvedExecution: + properties: + applicationGeneration: + format: int64 + type: integer + args: + items: + type: string + type: array + command: + items: + type: string + type: array + containerName: + type: string + image: + type: string + timeoutSeconds: + format: int64 + type: integer + type: object + results: + items: + properties: + durationMs: + format: int64 + type: integer + endedAt: + format: date-time + type: string + evidence: + x-kubernetes-preserve-unknown-fields: true + message: + type: string + name: + type: string + remediation: + type: string + severity: + enum: + - pass + - warn + - fail + - error + type: string + startedAt: + format: date-time + type: string + umbrella: + type: string + required: + - name + - severity + type: object + type: array + startedAt: + format: date-time + type: string + summary: + properties: + error: + format: int32 + type: integer + fail: + format: int32 + type: integer + overallSeverity: + enum: + - pass + - warn + - fail + - error + type: string + pass: + format: int32 + type: integer + total: + format: int32 + type: integer + warn: + format: int32 + type: integer + type: object + required: + - action + type: object + type: array + x-kubernetes-list-map-keys: + - action + x-kubernetes-list-type: map completedAt: format: date-time type: string @@ -104,13 +221,6 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map - jobRef: - properties: - name: - default: "" - type: string - type: object - x-kubernetes-map-type: atomic observedGeneration: format: int64 type: integer @@ -121,61 +231,6 @@ spec: - Succeeded - Failed type: string - resolvedExecution: - properties: - applicationGeneration: - format: int64 - type: integer - args: - items: - type: string - type: array - command: - items: - type: string - type: array - containerName: - type: string - image: - type: string - timeoutSeconds: - format: int64 - type: integer - type: object - results: - items: - properties: - durationMs: - format: int64 - type: integer - endedAt: - format: date-time - type: string - evidence: - x-kubernetes-preserve-unknown-fields: true - message: - type: string - name: - type: string - remediation: - type: string - severity: - enum: - - pass - - warn - - fail - - error - type: string - startedAt: - format: date-time - type: string - umbrella: - type: string - required: - - name - - severity - type: object - type: array startedAt: format: date-time type: string @@ -205,6 +260,8 @@ spec: type: integer type: object type: object + required: + - spec type: object served: true storage: true From 58280ccf58fad74f12db814437b01b23b80150f6 Mon Sep 17 00:00:00 2001 From: Aravind Warrier Date: Tue, 4 Aug 2026 11:47:41 -0400 Subject: [PATCH 3/6] fix(api): use Kubernetes set semantics for actions --- api/v2/triagerun_types.go | 2 +- config/crd/bases/apps.wandb.com_triageruns.yaml | 2 +- go.mod | 6 +++--- go.sum | 6 ++++++ .../crds/operator/apps.wandb.com_triageruns.yaml | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/v2/triagerun_types.go b/api/v2/triagerun_types.go index 7db687ac..b020b849 100644 --- a/api/v2/triagerun_types.go +++ b/api/v2/triagerun_types.go @@ -70,7 +70,7 @@ type TriageRunSpec struct { // Actions selects one or more triage actions declared by the referenced // Application. Each action is executed independently. // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:UniqueItems=true + // +listType=set Actions []TriageActionName `json:"actions"` } diff --git a/config/crd/bases/apps.wandb.com_triageruns.yaml b/config/crd/bases/apps.wandb.com_triageruns.yaml index a3787bf2..1dfe49a7 100644 --- a/config/crd/bases/apps.wandb.com_triageruns.yaml +++ b/config/crd/bases/apps.wandb.com_triageruns.yaml @@ -48,7 +48,7 @@ spec: type: string minItems: 1 type: array - uniqueItems: true + x-kubernetes-list-type: set applicationRef: properties: name: diff --git a/go.mod b/go.mod index 22fb427a..4f5ebbaa 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/twmb/franz-go v1.21.3 github.com/twmb/franz-go/pkg/kadm v1.18.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 - golang.org/x/text v0.38.0 + golang.org/x/text v0.39.0 gopkg.in/d4l3k/messagediff.v1 v1.2.1 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.19.2 @@ -172,14 +172,14 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect - golang.org/x/mod v0.36.0 // indirect + golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/tools v0.47.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect diff --git a/go.sum b/go.sum index 615f09c7..b68d78e2 100644 --- a/go.sum +++ b/go.sum @@ -447,6 +447,8 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5Z golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -461,10 +463,14 @@ golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml index a3787bf2..1dfe49a7 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml @@ -48,7 +48,7 @@ spec: type: string minItems: 1 type: array - uniqueItems: true + x-kubernetes-list-type: set applicationRef: properties: name: From 7314a42f5ae0125ff849a3a537a159ebb09f3d6c Mon Sep 17 00:00:00 2001 From: Aravind Warrier Date: Tue, 4 Aug 2026 11:51:30 -0400 Subject: [PATCH 4/6] chore(deps): tidy upgraded Go modules --- go.sum | 6 ------ 1 file changed, 6 deletions(-) diff --git a/go.sum b/go.sum index b68d78e2..cef3cf68 100644 --- a/go.sum +++ b/go.sum @@ -445,8 +445,6 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= @@ -461,14 +459,10 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= From ac76a9558c4c721ab81e4079c1a25af7362d5953 Mon Sep 17 00:00:00 2001 From: Aravind Warrier Date: Tue, 4 Aug 2026 12:25:37 -0400 Subject: [PATCH 5/6] refactor(api): model TriageRun actions as references --- api/v2/triagerun_types.go | 13 +++++++++++-- api/v2/zz_generated.deepcopy.go | 17 ++++++++++++++++- config/crd/bases/apps.wandb.com_triageruns.yaml | 13 ++++++++++--- config/samples/apps_v2_triagerun.yaml | 2 +- .../operator/apps.wandb.com_triageruns.yaml | 13 ++++++++++--- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/api/v2/triagerun_types.go b/api/v2/triagerun_types.go index b020b849..e341ece5 100644 --- a/api/v2/triagerun_types.go +++ b/api/v2/triagerun_types.go @@ -58,6 +58,14 @@ type TriageApplicationReference struct { // +kubebuilder:validation:MinLength=1 type TriageActionName string +// TriageActionReference selects one action declared by the referenced +// Application. Descriptive and execution metadata remain owned by the +// Application and are resolved by the controller. +type TriageActionReference struct { + // Name is the stable action name exposed by the Application. + Name TriageActionName `json:"name"` +} + // TriageRunSpec defines one immutable request to run one or more diagnostic // actions for an Application. // Creating another run requires creating another TriageRun. @@ -70,8 +78,9 @@ type TriageRunSpec struct { // Actions selects one or more triage actions declared by the referenced // Application. Each action is executed independently. // +kubebuilder:validation:MinItems=1 - // +listType=set - Actions []TriageActionName `json:"actions"` + // +listType=map + // +listMapKey=name + Actions []TriageActionReference `json:"actions"` } // TriageResolvedExecution records the concrete execution selected from the diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 1b7d1952..93c5bd13 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -1479,6 +1479,21 @@ func (in *TelemetryInfraStatus) DeepCopy() *TelemetryInfraStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriageActionReference) DeepCopyInto(out *TriageActionReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriageActionReference. +func (in *TriageActionReference) DeepCopy() *TriageActionReference { + if in == nil { + return nil + } + out := new(TriageActionReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TriageActionStatus) DeepCopyInto(out *TriageActionStatus) { *out = *in @@ -1657,7 +1672,7 @@ func (in *TriageRunSpec) DeepCopyInto(out *TriageRunSpec) { out.ApplicationRef = in.ApplicationRef if in.Actions != nil { in, out := &in.Actions, &out.Actions - *out = make([]TriageActionName, len(*in)) + *out = make([]TriageActionReference, len(*in)) copy(*out, *in) } } diff --git a/config/crd/bases/apps.wandb.com_triageruns.yaml b/config/crd/bases/apps.wandb.com_triageruns.yaml index 1dfe49a7..b245a252 100644 --- a/config/crd/bases/apps.wandb.com_triageruns.yaml +++ b/config/crd/bases/apps.wandb.com_triageruns.yaml @@ -44,11 +44,18 @@ spec: properties: actions: items: - minLength: 1 - type: string + properties: + name: + minLength: 1 + type: string + required: + - name + type: object minItems: 1 type: array - x-kubernetes-list-type: set + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map applicationRef: properties: name: diff --git a/config/samples/apps_v2_triagerun.yaml b/config/samples/apps_v2_triagerun.yaml index a22be88e..3b20474b 100644 --- a/config/samples/apps_v2_triagerun.yaml +++ b/config/samples/apps_v2_triagerun.yaml @@ -9,4 +9,4 @@ spec: applicationRef: name: application-sample actions: - - default + - name: default diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml index 1dfe49a7..b245a252 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml @@ -44,11 +44,18 @@ spec: properties: actions: items: - minLength: 1 - type: string + properties: + name: + minLength: 1 + type: string + required: + - name + type: object minItems: 1 type: array - x-kubernetes-list-type: set + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map applicationRef: properties: name: From c22cc8fab06da0b3c383d807164f7770bf9bcf0a Mon Sep 17 00:00:00 2001 From: Aravind Warrier Date: Thu, 6 Aug 2026 12:31:55 -0400 Subject: [PATCH 6/6] fix(api): validate triage application references --- api/v2/triagerun_types.go | 2 ++ config/crd/bases/apps.wandb.com_triageruns.yaml | 2 ++ .../crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/api/v2/triagerun_types.go b/api/v2/triagerun_types.go index e341ece5..9eb165be 100644 --- a/api/v2/triagerun_types.go +++ b/api/v2/triagerun_types.go @@ -51,6 +51,8 @@ const ( type TriageApplicationReference struct { // Name is the name of the Application to diagnose. // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` Name string `json:"name"` } diff --git a/config/crd/bases/apps.wandb.com_triageruns.yaml b/config/crd/bases/apps.wandb.com_triageruns.yaml index b245a252..a66dc52d 100644 --- a/config/crd/bases/apps.wandb.com_triageruns.yaml +++ b/config/crd/bases/apps.wandb.com_triageruns.yaml @@ -59,7 +59,9 @@ spec: applicationRef: properties: name: + maxLength: 253 minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - name diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml index b245a252..a66dc52d 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_triageruns.yaml @@ -59,7 +59,9 @@ spec: applicationRef: properties: name: + maxLength: 253 minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - name