notebooks/workspaces/controller/internal/webhook/workspace_webhook.go

233 lines
8.0 KiB
Go

/*
Copyright 2024.
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 webhook
import (
"context"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
)
// WorkspaceValidator validates a Workspace object
type WorkspaceValidator struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:webhook:path=/validate-kubeflow-org-v1beta1-workspace,mutating=false,failurePolicy=fail,sideEffects=None,groups=kubeflow.org,resources=workspaces,verbs=create;update,versions=v1beta1,name=vworkspace.kb.io,admissionReviewVersions=v1
// SetupWebhookWithManager sets up the webhook with the manager
func (v *WorkspaceValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(&kubefloworgv1beta1.Workspace{}).
WithValidator(v).
Complete()
}
// ValidateCreate validates the Workspace on creation.
// The optional warnings will be added to the response as warning messages.
// Return an error if the object is invalid.
func (v *WorkspaceValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
log := log.FromContext(ctx)
log.V(1).Info("validating Workspace create")
workspace, ok := obj.(*kubefloworgv1beta1.Workspace)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Workspace object but got %T", obj))
}
var allErrs field.ErrorList
// fetch the WorkspaceKind
workspaceKind, err := v.validateWorkspaceKind(ctx, workspace)
if err != nil {
allErrs = append(allErrs, err)
// if the WorkspaceKind is not found, we cannot validate the Workspace further
return nil, apierrors.NewInvalid(
schema.GroupKind{Group: kubefloworgv1beta1.GroupVersion.Group, Kind: "Workspace"},
workspace.Name,
allErrs,
)
}
// validate the Workspace
if err := v.validateImageConfig(workspace, workspaceKind); err != nil {
allErrs = append(allErrs, err)
}
if err := v.validatePodConfig(workspace, workspaceKind); err != nil {
allErrs = append(allErrs, err)
}
if len(allErrs) == 0 {
return nil, nil
}
return nil, apierrors.NewInvalid(
schema.GroupKind{Group: kubefloworgv1beta1.GroupVersion.Group, Kind: "Workspace"},
workspace.Name,
allErrs,
)
}
// ValidateUpdate validates the Workspace on update.
// The optional warnings will be added to the response as warning messages.
// Return an error if the object is invalid.
func (v *WorkspaceValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
log := log.FromContext(ctx)
log.V(1).Info("validating Workspace update")
newWorkspace, ok := newObj.(*kubefloworgv1beta1.Workspace)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Workspace object but got %T", newObj))
}
oldWorkspace, ok := oldObj.(*kubefloworgv1beta1.Workspace)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected old object to be a Workspace but got %T", oldObj))
}
var allErrs field.ErrorList
// check if workspace kind related fields have changed
var workspaceKindChange = false
var imageConfigChange = false
var podConfigChange = false
if newWorkspace.Spec.Kind != oldWorkspace.Spec.Kind {
workspaceKindChange = true
}
if newWorkspace.Spec.PodTemplate.Options.ImageConfig != oldWorkspace.Spec.PodTemplate.Options.ImageConfig {
imageConfigChange = true
}
if newWorkspace.Spec.PodTemplate.Options.PodConfig != oldWorkspace.Spec.PodTemplate.Options.PodConfig {
podConfigChange = true
}
// if any of the workspace kind related fields have changed, revalidate the workspace
if workspaceKindChange || imageConfigChange || podConfigChange {
// fetch the WorkspaceKind
workspaceKind, err := v.validateWorkspaceKind(ctx, newWorkspace)
if err != nil {
allErrs = append(allErrs, err)
// if the WorkspaceKind is not found, we cannot validate the Workspace further
return nil, apierrors.NewInvalid(
schema.GroupKind{Group: kubefloworgv1beta1.GroupVersion.Group, Kind: "Workspace"},
newWorkspace.Name,
allErrs,
)
}
// validate the new imageConfig
if imageConfigChange {
if err := v.validateImageConfig(newWorkspace, workspaceKind); err != nil {
allErrs = append(allErrs, err)
}
}
// validate the new podConfig
if podConfigChange {
if err := v.validatePodConfig(newWorkspace, workspaceKind); err != nil {
allErrs = append(allErrs, err)
}
}
}
if len(allErrs) == 0 {
return nil, nil
}
return nil, apierrors.NewInvalid(
schema.GroupKind{Group: kubefloworgv1beta1.GroupVersion.Group, Kind: "Workspace"},
newWorkspace.Name,
allErrs,
)
}
// ValidateDelete validates the Workspace on deletion.
// The optional warnings will be added to the response as warning messages.
// Return an error if the object is invalid.
func (v *WorkspaceValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
// no validation needed for deletion
// NOTE: add "delete" to the webhook configuration (+kubebuilder:webhook) if you want to enable deletion validation
return nil, nil
}
// validateWorkspaceKind fetches the WorkspaceKind for a Workspace and returns an error if it does not exist
func (v *WorkspaceValidator) validateWorkspaceKind(ctx context.Context, workspace *kubefloworgv1beta1.Workspace) (*kubefloworgv1beta1.WorkspaceKind, *field.Error) {
workspaceKindName := workspace.Spec.Kind
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
if err := v.Get(ctx, client.ObjectKey{Name: workspaceKindName}, workspaceKind); err != nil {
workspaceKindNamePath := field.NewPath("spec", "kind")
if apierrors.IsNotFound(err) {
return nil, field.Invalid(
workspaceKindNamePath,
workspaceKindName,
fmt.Sprintf("workspace kind %q not found", workspaceKindName),
)
} else {
return nil, field.InternalError(
workspaceKindNamePath,
err,
)
}
}
return workspaceKind, nil
}
// validateImageConfig checks if the imageConfig selected by a Workspace exists a WorkspaceKind
func (v *WorkspaceValidator) validateImageConfig(workspace *kubefloworgv1beta1.Workspace, workspaceKind *kubefloworgv1beta1.WorkspaceKind) *field.Error {
imageConfig := workspace.Spec.PodTemplate.Options.ImageConfig
for _, value := range workspaceKind.Spec.PodTemplate.Options.ImageConfig.Values {
if imageConfig == value.Id {
// imageConfig found
return nil
}
}
imageConfigPath := field.NewPath("spec", "podTemplate", "options", "imageConfig")
return field.Invalid(
imageConfigPath,
imageConfig,
fmt.Sprintf("imageConfig with id %q not found in workspace kind %q", imageConfig, workspaceKind.Name),
)
}
// validatePodConfig checks if the podConfig selected by a Workspace exists a WorkspaceKind
func (v *WorkspaceValidator) validatePodConfig(workspace *kubefloworgv1beta1.Workspace, workspaceKind *kubefloworgv1beta1.WorkspaceKind) *field.Error {
podConfig := workspace.Spec.PodTemplate.Options.PodConfig
for _, value := range workspaceKind.Spec.PodTemplate.Options.PodConfig.Values {
if podConfig == value.Id {
// podConfig found
return nil
}
}
podConfigPath := field.NewPath("spec", "podTemplate", "options", "podConfig")
return field.Invalid(
podConfigPath,
podConfig,
fmt.Sprintf("podConfig with id %q not found in workspace kind %q", podConfig, workspaceKind.Name),
)
}