diff --git a/PROJECT b/PROJECT index a801132e..20ad4479 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: ActionRun + path: github.com/wandb/operator/api/v2 + version: v2 version: "3" diff --git a/api/v2/actionrun_types.go b/api/v2/actionrun_types.go new file mode 100644 index 00000000..0f4d72e3 --- /dev/null +++ b/api/v2/actionrun_types.go @@ -0,0 +1,209 @@ +/* +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" +) + +// ActionRunPhase describes the execution lifecycle of an ActionRun. A +// Succeeded run means that the action command completed successfully; it does +// not mean that every result reported a passing verdict. +// +kubebuilder:validation:Enum=Pending;Running;Succeeded;Failed +type ActionRunPhase string + +const ( + ActionRunPhasePending ActionRunPhase = "Pending" + ActionRunPhaseRunning ActionRunPhase = "Running" + ActionRunPhaseSucceeded ActionRunPhase = "Succeeded" + ActionRunPhaseFailed ActionRunPhase = "Failed" +) + +// ActionSeverity is the verdict emitted by an individual action result. +// +kubebuilder:validation:Enum=pass;warn;fail;error +type ActionSeverity string + +const ( + ActionSeverityPass ActionSeverity = "pass" + ActionSeverityWarn ActionSeverity = "warn" + ActionSeverityFail ActionSeverity = "fail" + ActionSeverityError ActionSeverity = "error" +) + +// ActionType identifies the class of action selected from an Application. +// +kubebuilder:validation:Enum=triage;maintenance +type ActionType string + +const ( + ActionTypeTriage ActionType = "triage" + ActionTypeMaintenance ActionType = "maintenance" +) + +// ApplicationReference identifies an Application in the ActionRun's namespace. +type ApplicationReference struct { + // Name is the name of the Application that declares the selected action. + // +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"` +} + +// ActionName identifies an action declared by an Application. +// +kubebuilder:validation:MinLength=1 +type ActionName string + +// ActionReference selects one action declared by the referenced +// Application. Descriptive and execution metadata remain owned by the +// Application and are resolved by the controller. +type ActionReference struct { + // Name is the stable action name exposed by the Application. + Name ActionName `json:"name"` +} + +// ActionRunSpec defines one immutable request to run one Application action. +// Creating another run requires creating another ActionRun. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec is immutable" +type ActionRunSpec struct { + // Type selects the Application action catalog. Triage is executable in this + // release; maintenance is reserved for its future safety contract. + Type ActionType `json:"type"` + + // ApplicationRef identifies the Application whose action will run. Cross-namespace + // references are intentionally unsupported. + ApplicationRef ApplicationReference `json:"applicationRef"` + + // Action selects exactly one action declared by the referenced Application. + Action ActionReference `json:"action"` +} + +// ActionResolvedExecution records the concrete execution selected from the +// Application at reconciliation time. It is an audit snapshot, not user input. +type ActionResolvedExecution 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"` +} + +// ActionRunSummary contains aggregate verdict counts for a completed run. +type ActionRunSummary 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 ActionSeverity `json:"overallSeverity,omitempty"` +} + +// ActionResult contains one structured record emitted by the action command. +type ActionResult struct { + Name string `json:"name"` + + // Umbrella is an optional logical grouping for related checks. + Umbrella string `json:"umbrella,omitempty"` + + Severity ActionSeverity `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"` +} + +// ActionRunStatus defines the observed execution state and structured output. +type ActionRunStatus struct { + Phase ActionRunPhase `json:"phase,omitempty"` + + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // JobRef identifies the Kubernetes Job executing the selected action. + JobRef *corev1.LocalObjectReference `json:"jobRef,omitempty"` + + // ResolvedExecution is the execution snapshot selected from the referenced + // Application. + ResolvedExecution *ActionResolvedExecution `json:"resolvedExecution,omitempty"` + + StartedAt *metav1.Time `json:"startedAt,omitempty"` + CompletedAt *metav1.Time `json:"completedAt,omitempty"` + + Summary *ActionRunSummary `json:"summary,omitempty"` + Results []ActionResult `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:selectablefield:JSONPath=.spec.type +// +kubebuilder:selectablefield:JSONPath=.spec.applicationRef.name +// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type` +// +kubebuilder:printcolumn:name="Application",type=string,JSONPath=`.spec.applicationRef.name` +// +kubebuilder:printcolumn:name="Action",type=string,JSONPath=`.spec.action.name` +// +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` + +// ActionRun is one immutable request to execute an Application action. +type ActionRun struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ActionRunSpec `json:"spec"` + Status ActionRunStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ActionRunList contains a list of ActionRun. +type ActionRunList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ActionRun `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ActionRun{}, &ActionRunList{}) +} diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 9b6bbcec..a6c4d885 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -26,14 +26,226 @@ import ( appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" - "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" + "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" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionReference) DeepCopyInto(out *ActionReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionReference. +func (in *ActionReference) DeepCopy() *ActionReference { + if in == nil { + return nil + } + out := new(ActionReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionResolvedExecution) DeepCopyInto(out *ActionResolvedExecution) { + *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 ActionResolvedExecution. +func (in *ActionResolvedExecution) DeepCopy() *ActionResolvedExecution { + if in == nil { + return nil + } + out := new(ActionResolvedExecution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionResult) DeepCopyInto(out *ActionResult) { + *out = *in + if in.Evidence != nil { + in, out := &in.Evidence, &out.Evidence + *out = new(v1.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 ActionResult. +func (in *ActionResult) DeepCopy() *ActionResult { + if in == nil { + return nil + } + out := new(ActionResult) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionRun) DeepCopyInto(out *ActionRun) { + *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 ActionRun. +func (in *ActionRun) DeepCopy() *ActionRun { + if in == nil { + return nil + } + out := new(ActionRun) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActionRun) 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 *ActionRunList) DeepCopyInto(out *ActionRunList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActionRun, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionRunList. +func (in *ActionRunList) DeepCopy() *ActionRunList { + if in == nil { + return nil + } + out := new(ActionRunList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActionRunList) 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 *ActionRunSpec) DeepCopyInto(out *ActionRunSpec) { + *out = *in + out.ApplicationRef = in.ApplicationRef + out.Action = in.Action +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionRunSpec. +func (in *ActionRunSpec) DeepCopy() *ActionRunSpec { + if in == nil { + return nil + } + out := new(ActionRunSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionRunStatus) DeepCopyInto(out *ActionRunStatus) { + *out = *in + if in.JobRef != nil { + in, out := &in.JobRef, &out.JobRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.ResolvedExecution != nil { + in, out := &in.ResolvedExecution, &out.ResolvedExecution + *out = new(ActionResolvedExecution) + (*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(ActionRunSummary) + **out = **in + } + if in.Results != nil { + in, out := &in.Results, &out.Results + *out = make([]ActionResult, 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 ActionRunStatus. +func (in *ActionRunStatus) DeepCopy() *ActionRunStatus { + if in == nil { + return nil + } + out := new(ActionRunStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionRunSummary) DeepCopyInto(out *ActionRunSummary) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionRunSummary. +func (in *ActionRunSummary) DeepCopy() *ActionRunSummary { + if in == nil { + return nil + } + out := new(ActionRunSummary) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Application) DeepCopyInto(out *Application) { *out = *in @@ -118,6 +330,21 @@ func (in *ApplicationList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationReference) DeepCopyInto(out *ApplicationReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationReference. +func (in *ApplicationReference) DeepCopy() *ApplicationReference { + if in == nil { + return nil + } + out := new(ApplicationReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { *out = *in @@ -130,14 +357,14 @@ func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { in.PodTemplate.DeepCopyInto(&out.PodTemplate) if in.VolumeClaimTemplates != nil { in, out := &in.VolumeClaimTemplates, &out.VolumeClaimTemplates - *out = make([]v1.PersistentVolumeClaim, len(*in)) + *out = make([]corev1.PersistentVolumeClaim, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.ServiceTemplate != nil { in, out := &in.ServiceTemplate, &out.ServiceTemplate - *out = new(v1.ServiceSpec) + *out = new(corev1.ServiceSpec) (*in).DeepCopyInto(*out) } if in.IngressTemplate != nil { @@ -230,7 +457,7 @@ func (in *ApplicationStatus) DeepCopyInto(out *ApplicationStatus) { } if in.ServiceStatus != nil { in, out := &in.ServiceStatus, &out.ServiceStatus - *out = new(v1.ServiceStatus) + *out = new(corev1.ServiceStatus) (*in).DeepCopyInto(*out) } if in.HPAStatus != nil { @@ -511,7 +738,7 @@ func (in *GlobalSpec) DeepCopyInto(out *GlobalSpec) { *out = *in if in.ImagePullSecrets != nil { in, out := &in.ImagePullSecrets, &out.ImagePullSecrets - *out = make([]v1.LocalObjectReference, len(*in)) + *out = make([]corev1.LocalObjectReference, len(*in)) copy(*out, *in) } if in.CustomCACerts != nil { @@ -613,7 +840,7 @@ func (in *IngressStatusSummary) DeepCopyInto(out *IngressStatusSummary) { *out = *in if in.LoadBalancerIngress != nil { in, out := &in.LoadBalancerIngress, &out.LoadBalancerIngress - *out = make([]v1.LoadBalancerIngress, len(*in)) + *out = make([]corev1.LoadBalancerIngress, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -744,14 +971,14 @@ func (in *LegacyOverrides) DeepCopyInto(out *LegacyOverrides) { *out = *in if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + *out = make([]corev1.EnvVar, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.Resources != nil { in, out := &in.Resources, &out.Resources - *out = new(v1.ResourceRequirements) + *out = new(corev1.ResourceRequirements) (*in).DeepCopyInto(*out) } } @@ -822,15 +1049,15 @@ func (in *ManagedInfraSpec) DeepCopyInto(out *ManagedInfraSpec) { } if in.Affinity != nil { in, out := &in.Affinity, &out.Affinity - *out = new(v1.Affinity) + *out = new(corev1.Affinity) (*in).DeepCopyInto(*out) } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations - *out = new([]v1.Toleration) + *out = new([]corev1.Toleration) if **in != nil { in, out := *in, *out - *out = make([]v1.Toleration, len(*in)) + *out = make([]corev1.Toleration, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1243,7 +1470,7 @@ func (in *ProxyValueSource) DeepCopyInto(out *ProxyValueSource) { *out = *in if in.SecretKeyRef != nil { in, out := &in.SecretKeyRef, &out.SecretKeyRef - *out = new(v1.SecretKeySelector) + *out = new(corev1.SecretKeySelector) (*in).DeepCopyInto(*out) } } @@ -1622,17 +1849,17 @@ func (in *WandbProbeDefaults) DeepCopyInto(out *WandbProbeDefaults) { *out = *in if in.StartupProbe != nil { in, out := &in.StartupProbe, &out.StartupProbe - *out = new(v1.Probe) + *out = new(corev1.Probe) (*in).DeepCopyInto(*out) } if in.LivenessProbe != nil { in, out := &in.LivenessProbe, &out.LivenessProbe - *out = new(v1.Probe) + *out = new(corev1.Probe) (*in).DeepCopyInto(*out) } if in.ReadinessProbe != nil { in, out := &in.ReadinessProbe, &out.ReadinessProbe - *out = new(v1.Probe) + *out = new(corev1.Probe) (*in).DeepCopyInto(*out) } } @@ -1744,15 +1971,15 @@ func (in *WeightsAndBiasesSpec) DeepCopyInto(out *WeightsAndBiasesSpec) { in.Wandb.DeepCopyInto(&out.Wandb) if in.Affinity != nil { in, out := &in.Affinity, &out.Affinity - *out = new(v1.Affinity) + *out = new(corev1.Affinity) (*in).DeepCopyInto(*out) } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations - *out = new([]v1.Toleration) + *out = new([]corev1.Toleration) if **in != nil { in, out := *in, *out - *out = make([]v1.Toleration, len(*in)) + *out = make([]corev1.Toleration, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1843,7 +2070,7 @@ func (in *WeightsAndBiasesStatus) DeepCopyInto(out *WeightsAndBiasesStatus) { in.TelemetryStatus.DeepCopyInto(&out.TelemetryStatus) if in.GeneratedSecrets != nil { in, out := &in.GeneratedSecrets, &out.GeneratedSecrets - *out = make(map[string]v1.SecretKeySelector, len(*in)) + *out = make(map[string]corev1.SecretKeySelector, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } diff --git a/config/crd/bases/apps.wandb.com_actionruns.yaml b/config/crd/bases/apps.wandb.com_actionruns.yaml new file mode 100644 index 00000000..f6362b67 --- /dev/null +++ b/config/crd/bases/apps.wandb.com_actionruns.yaml @@ -0,0 +1,233 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: actionruns.apps.wandb.com +spec: + group: apps.wandb.com + names: + kind: ActionRun + listKind: ActionRunList + plural: actionruns + singular: actionrun + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.type + name: Type + type: string + - jsonPath: .spec.applicationRef.name + name: Application + type: string + - jsonPath: .spec.action.name + 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: + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + 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 + type: object + type: + enum: + - triage + - maintenance + type: string + required: + - action + - applicationRef + - type + 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 + required: + - spec + type: object + selectableFields: + - jsonPath: .spec.type + - jsonPath: .spec.applicationRef.name + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/kustomization.yaml b/config/crd/bases/kustomization.yaml index 1ab16d8b..7f2df9c0 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_actionruns.yaml diff --git a/config/dev-common/delete-actionruns-crd.yaml b/config/dev-common/delete-actionruns-crd.yaml new file mode 100644 index 00000000..b4a94ab9 --- /dev/null +++ b/config/dev-common/delete-actionruns-crd.yaml @@ -0,0 +1,5 @@ +$patch: delete +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: actionruns.apps.wandb.com diff --git a/config/dev-common/kustomization.yaml b/config/dev-common/kustomization.yaml index 409db598..e242d5ba 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: actionruns.apps.wandb.com + path: delete-actionruns-crd.yaml - target: group: apiextensions.k8s.io version: v1 diff --git a/config/rbac/actionrun_admin_role.yaml b/config/rbac/actionrun_admin_role.yaml new file mode 100644 index 00000000..2ce12aec --- /dev/null +++ b/config/rbac/actionrun_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 ActionRun resources. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: actionrun-admin-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - actionruns + verbs: + - '*' +- apiGroups: + - apps.wandb.com + resources: + - actionruns/status + verbs: + - get diff --git a/config/rbac/actionrun_editor_role.yaml b/config/rbac/actionrun_editor_role.yaml new file mode 100644 index 00000000..5ded6c21 --- /dev/null +++ b/config/rbac/actionrun_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 ActionRun resources. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: actionrun-editor-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - actionruns + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.wandb.com + resources: + - actionruns/status + verbs: + - get diff --git a/config/rbac/actionrun_viewer_role.yaml b/config/rbac/actionrun_viewer_role.yaml new file mode 100644 index 00000000..b51ba8e4 --- /dev/null +++ b/config/rbac/actionrun_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 ActionRun resources. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: actionrun-viewer-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - actionruns + verbs: + - get + - list + - watch +- apiGroups: + - apps.wandb.com + resources: + - actionruns/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 35662c43..d91b50fc 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 +- actionrun_admin_role.yaml +- actionrun_editor_role.yaml +- actionrun_viewer_role.yaml - weightsandbiases_admin_role.yaml - weightsandbiases_editor_role.yaml - weightsandbiases_viewer_role.yaml - diff --git a/config/samples/apps_v2_actionrun.yaml b/config/samples/apps_v2_actionrun.yaml new file mode 100644 index 00000000..6e1823c9 --- /dev/null +++ b/config/samples/apps_v2_actionrun.yaml @@ -0,0 +1,13 @@ +apiVersion: apps.wandb.com/v2 +kind: ActionRun +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + generateName: actionrun-sample- +spec: + type: triage + applicationRef: + name: application-sample + action: + name: default diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 12dd6d2f..9eb80430 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_actionrun.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/internal/crdinstaller/compose_test.go b/internal/crdinstaller/compose_test.go index 689de64d..59632f8c 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", + "actionruns.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_actionruns.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_actionruns.yaml new file mode 100644 index 00000000..f6362b67 --- /dev/null +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_actionruns.yaml @@ -0,0 +1,233 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: actionruns.apps.wandb.com +spec: + group: apps.wandb.com + names: + kind: ActionRun + listKind: ActionRunList + plural: actionruns + singular: actionrun + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.type + name: Type + type: string + - jsonPath: .spec.applicationRef.name + name: Application + type: string + - jsonPath: .spec.action.name + 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: + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + 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 + type: object + type: + enum: + - triage + - maintenance + type: string + required: + - action + - applicationRef + - type + 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 + required: + - spec + type: object + selectableFields: + - jsonPath: .spec.type + - jsonPath: .spec.applicationRef.name + served: true + storage: true + subresources: + status: {}