601 lines
26 KiB
Go
601 lines
26 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"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/fields"
|
|
"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/controller/controllerutil"
|
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
|
"github.com/kubeflow/notebooks/workspaces/controller/internal/controller"
|
|
"github.com/kubeflow/notebooks/workspaces/controller/internal/helper"
|
|
)
|
|
|
|
// WorkspaceKindValidator validates a Workspace object
|
|
type WorkspaceKindValidator struct {
|
|
client.Client
|
|
Scheme *runtime.Scheme
|
|
}
|
|
|
|
// +kubebuilder:webhook:path=/validate-kubeflow-org-v1beta1-workspacekind,mutating=false,failurePolicy=fail,sideEffects=None,groups=kubeflow.org,resources=workspacekinds,verbs=create;update;delete,versions=v1beta1,name=vworkspacekind.kb.io,admissionReviewVersions=v1
|
|
|
|
// SetupWebhookWithManager sets up the webhook with the manager
|
|
func (v *WorkspaceKindValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
|
return ctrl.NewWebhookManagedBy(mgr).
|
|
For(&kubefloworgv1beta1.WorkspaceKind{}).
|
|
WithValidator(v).
|
|
Complete()
|
|
}
|
|
|
|
// ValidateCreate validates the WorkspaceKind on creation.
|
|
// The optional warnings will be added to the response as warning messages.
|
|
// Return an error if the object is invalid.
|
|
func (v *WorkspaceKindValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
|
|
log := log.FromContext(ctx)
|
|
log.V(1).Info("validating WorkspaceKind create")
|
|
|
|
workspaceKind, ok := obj.(*kubefloworgv1beta1.WorkspaceKind)
|
|
if !ok {
|
|
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a WorkspaceKind object but got %T", obj))
|
|
}
|
|
|
|
var allErrs field.ErrorList
|
|
|
|
// validate the extra environment variables
|
|
allErrs = append(allErrs, validateExtraEnv(workspaceKind)...)
|
|
|
|
// generate helper maps for imageConfig values
|
|
imageConfigIdMap := make(map[string]kubefloworgv1beta1.ImageConfigValue)
|
|
imageConfigRedirectMap := make(map[string]string)
|
|
for _, imageConfigValue := range workspaceKind.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
imageConfigIdMap[imageConfigValue.Id] = imageConfigValue
|
|
if imageConfigValue.Redirect != nil {
|
|
imageConfigRedirectMap[imageConfigValue.Id] = imageConfigValue.Redirect.To
|
|
}
|
|
}
|
|
|
|
// generate helper maps for podConfig values
|
|
podConfigIdMap := make(map[string]kubefloworgv1beta1.PodConfigValue)
|
|
podConfigRedirectMap := make(map[string]string)
|
|
for _, podConfigValue := range workspaceKind.Spec.PodTemplate.Options.PodConfig.Values {
|
|
podConfigIdMap[podConfigValue.Id] = podConfigValue
|
|
if podConfigValue.Redirect != nil {
|
|
podConfigRedirectMap[podConfigValue.Id] = podConfigValue.Redirect.To
|
|
}
|
|
}
|
|
|
|
// validate default options
|
|
allErrs = append(allErrs, validateDefaultImageConfig(workspaceKind, imageConfigIdMap)...)
|
|
allErrs = append(allErrs, validateDefaultPodConfig(workspaceKind, podConfigIdMap)...)
|
|
|
|
// validate imageConfig values
|
|
for _, imageConfigValue := range imageConfigIdMap {
|
|
imageConfigValueId := imageConfigValue.Id
|
|
imageConfigValuePath := field.NewPath("spec", "podTemplate", "options", "imageConfig", "values").Key(imageConfigValueId)
|
|
allErrs = append(allErrs, validateImageConfigValue(&imageConfigValue, imageConfigValuePath)...)
|
|
}
|
|
|
|
// validate redirects
|
|
allErrs = append(allErrs, validateImageConfigRedirects(imageConfigIdMap, imageConfigRedirectMap)...)
|
|
allErrs = append(allErrs, validatePodConfigRedirects(podConfigIdMap, podConfigRedirectMap)...)
|
|
|
|
if len(allErrs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, apierrors.NewInvalid(
|
|
schema.GroupKind{Group: kubefloworgv1beta1.GroupVersion.Group, Kind: "WorkspaceKind"},
|
|
workspaceKind.Name,
|
|
allErrs,
|
|
)
|
|
}
|
|
|
|
// ValidateUpdate validates the WorkspaceKind on update.
|
|
// The optional warnings will be added to the response as warning messages.
|
|
// Return an error if the object is invalid.
|
|
func (v *WorkspaceKindValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { //nolint:gocyclo
|
|
log := log.FromContext(ctx)
|
|
log.V(1).Info("validating WorkspaceKind update")
|
|
|
|
newWorkspaceKind, ok := newObj.(*kubefloworgv1beta1.WorkspaceKind)
|
|
if !ok {
|
|
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a WorkspaceKind object but got %T", newObj))
|
|
}
|
|
oldWorkspaceKind, ok := oldObj.(*kubefloworgv1beta1.WorkspaceKind)
|
|
if !ok {
|
|
return nil, apierrors.NewInternalError(fmt.Errorf("old object is not a WorkspaceKind, but a %T", oldObj))
|
|
}
|
|
|
|
var allErrs field.ErrorList
|
|
|
|
// get functions to lazily fetch usage counts for imageConfig and podConfig values
|
|
// NOTE: the cluster is only queried when either function is called for the first time
|
|
getImageConfigUsageCount, getPodConfigUsageCount := v.getLazyOptionUsageCountFuncs(ctx, oldWorkspaceKind)
|
|
|
|
// validate the extra environment variables
|
|
if !reflect.DeepEqual(newWorkspaceKind.Spec.PodTemplate.ExtraEnv, oldWorkspaceKind.Spec.PodTemplate.ExtraEnv) {
|
|
allErrs = append(allErrs, validateExtraEnv(newWorkspaceKind)...)
|
|
}
|
|
|
|
// calculate changes to imageConfig values
|
|
var shouldValidateImageConfigRedirects = false
|
|
toValidateImageConfigIds := make(map[string]bool)
|
|
badChangedImageConfigIds := make(map[string]bool)
|
|
badRemovedImageConfigIds := make(map[string]bool)
|
|
oldImageConfigIdMap := make(map[string]kubefloworgv1beta1.ImageConfigValue)
|
|
newImageConfigIdMap := make(map[string]kubefloworgv1beta1.ImageConfigValue)
|
|
newImageConfigRedirectMap := make(map[string]string)
|
|
for _, imageConfigValue := range oldWorkspaceKind.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
oldImageConfigIdMap[imageConfigValue.Id] = imageConfigValue
|
|
}
|
|
for _, imageConfigValue := range newWorkspaceKind.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
newImageConfigIdMap[imageConfigValue.Id] = imageConfigValue
|
|
if imageConfigValue.Redirect != nil {
|
|
newImageConfigRedirectMap[imageConfigValue.Id] = imageConfigValue.Redirect.To
|
|
}
|
|
|
|
// check if the imageConfig value is new
|
|
if _, exists := oldImageConfigIdMap[imageConfigValue.Id]; !exists {
|
|
// we need to validate this imageConfig value since it is new
|
|
toValidateImageConfigIds[imageConfigValue.Id] = true
|
|
|
|
// we always need to validate the imageConfig redirects if an imageConfig value was added
|
|
// because the new imageConfig value could be used by a redirect or cause a cycle
|
|
shouldValidateImageConfigRedirects = true
|
|
} else {
|
|
// if we haven't already decided to validate the imageConfig redirects,
|
|
// check if the redirect has changed
|
|
if !shouldValidateImageConfigRedirects && !reflect.DeepEqual(oldImageConfigIdMap[imageConfigValue.Id].Redirect, imageConfigValue.Redirect) {
|
|
shouldValidateImageConfigRedirects = true
|
|
}
|
|
|
|
// check if the spec has changed
|
|
if !reflect.DeepEqual(oldImageConfigIdMap[imageConfigValue.Id].Spec, imageConfigValue.Spec) {
|
|
// we need to validate this imageConfig value since it has changed
|
|
toValidateImageConfigIds[imageConfigValue.Id] = true
|
|
|
|
// check how many workspaces are using this imageConfig value
|
|
usageCount, err := getImageConfigUsageCount(imageConfigValue.Id)
|
|
if err != nil {
|
|
// if the usage count is not found, we cannot validate the WorkspaceKind further
|
|
return nil, apierrors.NewInternalError(fmt.Errorf("failed to get usage count for imageConfig with id %q: %w", imageConfigValue.Id, err))
|
|
}
|
|
|
|
// if this imageConfig is used by any workspaces, mark this imageConfig as bad,
|
|
// the spec is immutable while in use
|
|
if usageCount > 0 {
|
|
badChangedImageConfigIds[imageConfigValue.Id] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for id := range oldImageConfigIdMap {
|
|
|
|
// check if this imageConfig value was removed
|
|
if _, exists := newImageConfigIdMap[id]; !exists {
|
|
// check how many workspaces are using this imageConfig value
|
|
usageCount, err := getImageConfigUsageCount(id)
|
|
if err != nil {
|
|
// if the usage count is not found, we cannot validate the WorkspaceKind further
|
|
return nil, apierrors.NewInternalError(fmt.Errorf("failed to get usage count for imageConfig with id %q: %w", id, err))
|
|
}
|
|
|
|
// if this imageConfig is used by any workspaces, mark this imageConfig as bad,
|
|
// it is not safe to remove an imageConfig value that is in use
|
|
if usageCount > 0 {
|
|
badRemovedImageConfigIds[id] = true
|
|
}
|
|
|
|
// we always need to validate the imageConfig redirects if an imageConfig was removed
|
|
// because an existing redirect could be pointing to the removed imageConfig value
|
|
shouldValidateImageConfigRedirects = true
|
|
}
|
|
}
|
|
|
|
// calculate changes to podConfig values
|
|
var shouldValidatePodConfigRedirects = false
|
|
badChangedPodConfigIds := make(map[string]bool)
|
|
badRemovedPodConfigIds := make(map[string]bool)
|
|
newPodConfigIdMap := make(map[string]kubefloworgv1beta1.PodConfigValue)
|
|
newPodConfigRedirectMap := make(map[string]string)
|
|
oldPodConfigIdMap := make(map[string]kubefloworgv1beta1.PodConfigValue)
|
|
for _, podConfigValue := range oldWorkspaceKind.Spec.PodTemplate.Options.PodConfig.Values {
|
|
oldPodConfigIdMap[podConfigValue.Id] = podConfigValue
|
|
}
|
|
for _, podConfigValue := range newWorkspaceKind.Spec.PodTemplate.Options.PodConfig.Values {
|
|
newPodConfigIdMap[podConfigValue.Id] = podConfigValue
|
|
if podConfigValue.Redirect != nil {
|
|
newPodConfigRedirectMap[podConfigValue.Id] = podConfigValue.Redirect.To
|
|
}
|
|
|
|
// check if the podConfig value is new
|
|
if _, exists := oldPodConfigIdMap[podConfigValue.Id]; !exists {
|
|
// we always need to validate the podConfig redirects if a podConfig was added
|
|
// because the new podConfig value could be used by a redirect or cause a cycle
|
|
shouldValidatePodConfigRedirects = true
|
|
} else {
|
|
// if we haven't already decided to validate the podConfig redirects,
|
|
// check if the redirect has changed
|
|
if !shouldValidatePodConfigRedirects && !reflect.DeepEqual(oldPodConfigIdMap[podConfigValue.Id].Redirect, podConfigValue.Redirect) {
|
|
shouldValidatePodConfigRedirects = true
|
|
}
|
|
|
|
// we must normalize the podConfig specs so that we can compare them
|
|
newPodConfigSpec := podConfigValue.Spec
|
|
err := helper.NormalizePodConfigSpec(newPodConfigSpec)
|
|
if err != nil {
|
|
podConfigValueSpecPath := field.NewPath("spec", "podTemplate", "options", "podConfig", "values").Key(podConfigValue.Id).Child("spec")
|
|
allErrs = append(allErrs, field.InternalError(podConfigValueSpecPath, fmt.Errorf("failed to normalize podConfig spec: %w", err)))
|
|
|
|
// if the spec could not be normalized, we cannot validate the WorkspaceKind further
|
|
return nil, apierrors.NewInvalid(
|
|
schema.GroupKind{Group: kubefloworgv1beta1.GroupVersion.Group, Kind: "WorkspaceKind"},
|
|
newWorkspaceKind.Name,
|
|
allErrs,
|
|
)
|
|
}
|
|
oldPodConfigSpec := oldPodConfigIdMap[podConfigValue.Id].Spec
|
|
err = helper.NormalizePodConfigSpec(oldPodConfigSpec)
|
|
if err != nil {
|
|
// this should never happen, as it would indicate that the old podConfig spec is invalid
|
|
return nil, apierrors.NewInternalError(fmt.Errorf("old podConfig spec of %q could not be normalized: %w", podConfigValue.Id, err))
|
|
}
|
|
|
|
// check if the spec has changed
|
|
if !reflect.DeepEqual(oldPodConfigSpec, newPodConfigSpec) {
|
|
// check how many workspaces are using this podConfig value
|
|
usageCount, err := getPodConfigUsageCount(podConfigValue.Id)
|
|
if err != nil {
|
|
// if the usage count is not found, we cannot validate the WorkspaceKind further
|
|
return nil, apierrors.NewInternalError(fmt.Errorf("failed to get usage count for podConfig with id %q: %w", podConfigValue.Id, err))
|
|
}
|
|
|
|
// if this podConfig is used by any workspaces, mark this podConfig as bad,
|
|
// the spec is immutable while in use
|
|
if usageCount > 0 {
|
|
badChangedPodConfigIds[podConfigValue.Id] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for id := range oldPodConfigIdMap {
|
|
|
|
// check if this podConfig value was removed
|
|
if _, exists := newPodConfigIdMap[id]; !exists {
|
|
// check how many workspaces are using this podConfig value
|
|
usageCount, err := getPodConfigUsageCount(id)
|
|
if err != nil {
|
|
// if the usage count is not found, we cannot validate the WorkspaceKind further
|
|
return nil, apierrors.NewInternalError(fmt.Errorf("failed to get usage count for podConfig with id %q: %w", id, err))
|
|
}
|
|
|
|
// if this podConfig is used by any workspaces, mark this podConfig as bad,
|
|
// it is not safe to remove a podConfig value that is in use
|
|
if usageCount > 0 {
|
|
badRemovedPodConfigIds[id] = true
|
|
}
|
|
|
|
// we always need to validate the podConfig redirects if a podConfig was removed
|
|
// because an existing redirect could be pointing to the removed imageConfig value
|
|
shouldValidatePodConfigRedirects = true
|
|
}
|
|
}
|
|
|
|
// validate default options
|
|
// NOTE: we always check this because it's cheap, and otherwise we would need to keep track of if
|
|
// any options were changed or removed
|
|
allErrs = append(allErrs, validateDefaultImageConfig(newWorkspaceKind, newImageConfigIdMap)...)
|
|
allErrs = append(allErrs, validateDefaultPodConfig(newWorkspaceKind, newPodConfigIdMap)...)
|
|
|
|
// validate imageConfig values
|
|
// NOTE: we only need to validate new or changed imageConfig values
|
|
for imageConfigValueId := range toValidateImageConfigIds {
|
|
imageConfigValue := newImageConfigIdMap[imageConfigValueId]
|
|
imageConfigValuePath := field.NewPath("spec", "podTemplate", "options", "imageConfig", "values").Key(imageConfigValueId)
|
|
allErrs = append(allErrs, validateImageConfigValue(&imageConfigValue, imageConfigValuePath)...)
|
|
}
|
|
|
|
// process bad imageConfig values
|
|
for id := range badChangedImageConfigIds {
|
|
imageConfigValuePath := field.NewPath("spec", "podTemplate", "options", "imageConfig", "values").Key(id)
|
|
allErrs = append(allErrs, field.Invalid(imageConfigValuePath, id, fmt.Sprintf("imageConfig value %q is in use and cannot be changed", id)))
|
|
}
|
|
for id := range badRemovedImageConfigIds {
|
|
imageConfigValuePath := field.NewPath("spec", "podTemplate", "options", "imageConfig", "values").Key(id)
|
|
allErrs = append(allErrs, field.Invalid(imageConfigValuePath, id, fmt.Sprintf("imageConfig value %q is in use and cannot be removed", id)))
|
|
}
|
|
|
|
// process bad podConfig values
|
|
for id := range badChangedPodConfigIds {
|
|
podConfigValuePath := field.NewPath("spec", "podTemplate", "options", "podConfig", "values").Key(id)
|
|
allErrs = append(allErrs, field.Invalid(podConfigValuePath, id, fmt.Sprintf("podConfig value %q is in use and cannot be changed", id)))
|
|
}
|
|
for id := range badRemovedPodConfigIds {
|
|
podConfigValuePath := field.NewPath("spec", "podTemplate", "options", "podConfig", "values").Key(id)
|
|
allErrs = append(allErrs, field.Invalid(podConfigValuePath, id, fmt.Sprintf("podConfig value %q is in use and cannot be removed", id)))
|
|
}
|
|
|
|
// validate redirects
|
|
if shouldValidateImageConfigRedirects {
|
|
allErrs = append(allErrs, validateImageConfigRedirects(newImageConfigIdMap, newImageConfigRedirectMap)...)
|
|
}
|
|
if shouldValidatePodConfigRedirects {
|
|
allErrs = append(allErrs, validatePodConfigRedirects(newPodConfigIdMap, newPodConfigRedirectMap)...)
|
|
}
|
|
|
|
if len(allErrs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, apierrors.NewInvalid(
|
|
schema.GroupKind{Group: kubefloworgv1beta1.GroupVersion.Group, Kind: "WorkspaceKind"},
|
|
newWorkspaceKind.Name,
|
|
allErrs,
|
|
)
|
|
}
|
|
|
|
// ValidateDelete validates the WorkspaceKind on deletion.
|
|
// The optional warnings will be added to the response as warning messages.
|
|
// Return an error if the object is invalid.
|
|
func (v *WorkspaceKindValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
|
|
log := log.FromContext(ctx)
|
|
log.V(1).Info("validating WorkspaceKind delete")
|
|
|
|
workspaceKind, ok := obj.(*kubefloworgv1beta1.WorkspaceKind)
|
|
if !ok {
|
|
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a WorkspaceKind object but got %T", obj))
|
|
}
|
|
|
|
// don't allow deletion of WorkspaceKind if it is used by any workspaces
|
|
if workspaceKind.Status.Workspaces > 0 {
|
|
return nil, apierrors.NewConflict(
|
|
schema.GroupResource{Group: kubefloworgv1beta1.GroupVersion.Group, Resource: "WorkspaceKind"},
|
|
workspaceKind.Name,
|
|
fmt.Errorf("WorkspaceKind is used by %d workspace(s)", workspaceKind.Status.Workspaces),
|
|
)
|
|
}
|
|
|
|
// don't allow deletion of WorkspaceKind if it has the protection finalizer
|
|
// NOTE: while the finalizer also protects the WorkspaceKind from deletion,
|
|
// it is impossible to "un-delete" a resource once it has started terminating
|
|
// and this is a bad user experience, so we prevent deletion in the first place
|
|
if controllerutil.ContainsFinalizer(workspaceKind, controller.WorkspaceKindFinalizer) {
|
|
return nil, apierrors.NewConflict(
|
|
schema.GroupResource{Group: kubefloworgv1beta1.GroupVersion.Group, Resource: "WorkspaceKind"},
|
|
workspaceKind.Name,
|
|
errors.New("WorkspaceKind has protection finalizer, indicating one or more workspaces are still using it"),
|
|
)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// getLazyOptionUsageCountFuncs returns functions that get usage counts for imageConfig and podConfig values in a WorkspaceKind
|
|
// the cluster is only queried when either function is called for the first time
|
|
func (v *WorkspaceKindValidator) getLazyOptionUsageCountFuncs(ctx context.Context, workspaceKind *kubefloworgv1beta1.WorkspaceKind) (func(string) (int32, error), func(string) (int32, error)) {
|
|
|
|
// usageCountWrapper is a wrapper for the usage count maps
|
|
// NOTE: this is needed because sync.OnceValues can only return 2 values
|
|
type usageCountWrapper struct {
|
|
imageConfigUsageCounts map[string]int32
|
|
podConfigUsageCounts map[string]int32
|
|
}
|
|
|
|
// this function will lazily fetch the usage counts for each option
|
|
lazyGetOptionsUsageCounts := sync.OnceValues(func() (usageCountWrapper, error) {
|
|
imageConfigUsageCounts, podConfigUsageCounts, err := v.getOptionsUsageCounts(ctx, workspaceKind)
|
|
if err != nil {
|
|
return usageCountWrapper{}, err
|
|
}
|
|
return usageCountWrapper{
|
|
imageConfigUsageCounts: imageConfigUsageCounts,
|
|
podConfigUsageCounts: podConfigUsageCounts,
|
|
}, nil
|
|
})
|
|
|
|
// getImageConfigUsageCount returns the usage count for an imageConfig value
|
|
getImageConfigUsageCount := func(id string) (int32, error) {
|
|
wrapper, err := lazyGetOptionsUsageCounts()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if count, exists := wrapper.imageConfigUsageCounts[id]; !exists {
|
|
return 0, errors.New("unknown imageConfig id")
|
|
} else {
|
|
return count, nil
|
|
}
|
|
}
|
|
|
|
// getPodConfigUsageCount returns the usage count for a podConfig value
|
|
getPodConfigUsageCount := func(id string) (int32, error) {
|
|
wrapper, err := lazyGetOptionsUsageCounts()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if count, exists := wrapper.podConfigUsageCounts[id]; !exists {
|
|
return 0, errors.New("unknown podConfig id")
|
|
} else {
|
|
return count, nil
|
|
}
|
|
}
|
|
|
|
return getImageConfigUsageCount, getPodConfigUsageCount
|
|
}
|
|
|
|
// getOptionsUsageCounts returns the usage counts for each imageConfig and podConfig value
|
|
func (v *WorkspaceKindValidator) getOptionsUsageCounts(ctx context.Context, workspaceKind *kubefloworgv1beta1.WorkspaceKind) (map[string]int32, map[string]int32, error) {
|
|
imageConfigUsageCount := make(map[string]int32)
|
|
podConfigUsageCount := make(map[string]int32)
|
|
|
|
// fetch all Workspaces that are using this WorkspaceKind
|
|
workspaces := &kubefloworgv1beta1.WorkspaceList{}
|
|
listOpts := &client.ListOptions{
|
|
FieldSelector: fields.OneTermEqualSelector(helper.IndexWorkspaceKindField, workspaceKind.Name),
|
|
Namespace: "", // fetch Workspaces in all namespaces
|
|
}
|
|
if err := v.List(ctx, workspaces, listOpts); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// count the number of Workspaces using each option
|
|
for _, imageConfig := range workspaceKind.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
imageConfigUsageCount[imageConfig.Id] = 0
|
|
}
|
|
for _, podConfig := range workspaceKind.Spec.PodTemplate.Options.PodConfig.Values {
|
|
podConfigUsageCount[podConfig.Id] = 0
|
|
}
|
|
for _, ws := range workspaces.Items {
|
|
imageConfigUsageCount[ws.Spec.PodTemplate.Options.ImageConfig]++
|
|
podConfigUsageCount[ws.Spec.PodTemplate.Options.PodConfig]++
|
|
}
|
|
|
|
return imageConfigUsageCount, podConfigUsageCount, nil
|
|
}
|
|
|
|
// validateExtraEnv validates the extra environment variables in a WorkspaceKind
|
|
func validateExtraEnv(workspaceKind *kubefloworgv1beta1.WorkspaceKind) []*field.Error {
|
|
var errs []*field.Error
|
|
|
|
// the real httpPathPrefix can't fail, so we return a dummy value
|
|
httpPathPrefixFunc := func(portId string) string {
|
|
return "DUMMY_HTTP_PATH_PREFIX"
|
|
}
|
|
|
|
// validate that each value template can be rendered successfully
|
|
for _, env := range workspaceKind.Spec.PodTemplate.ExtraEnv {
|
|
if env.Value != "" {
|
|
rawValue := env.Value
|
|
_, err := helper.RenderExtraEnvValueTemplate(rawValue, httpPathPrefixFunc)
|
|
if err != nil {
|
|
extraEnvPath := field.NewPath("spec", "podTemplate", "extraEnv").Key(env.Name).Child("value")
|
|
errs = append(errs, field.Invalid(extraEnvPath, rawValue, err.Error()))
|
|
}
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// validateDefaultImageConfig validates the default imageConfig in a WorkspaceKind
|
|
func validateDefaultImageConfig(workspaceKind *kubefloworgv1beta1.WorkspaceKind, imageConfigValueMap map[string]kubefloworgv1beta1.ImageConfigValue) []*field.Error {
|
|
var errs []*field.Error
|
|
|
|
// validate the default imageConfig
|
|
defaultImageConfig := workspaceKind.Spec.PodTemplate.Options.ImageConfig.Spawner.Default
|
|
if _, exists := imageConfigValueMap[defaultImageConfig]; !exists {
|
|
defaultImageConfigPath := field.NewPath("spec", "podTemplate", "options", "imageConfig", "spawner", "default")
|
|
errs = append(errs, field.Invalid(defaultImageConfigPath, defaultImageConfig, fmt.Sprintf("default imageConfig %q not found", defaultImageConfig)))
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// validateDefaultPodConfig validates the default podConfig in a WorkspaceKind
|
|
func validateDefaultPodConfig(workspaceKind *kubefloworgv1beta1.WorkspaceKind, podConfigValueMap map[string]kubefloworgv1beta1.PodConfigValue) []*field.Error {
|
|
var errs []*field.Error
|
|
|
|
// validate the default podConfig
|
|
defaultPodConfig := workspaceKind.Spec.PodTemplate.Options.PodConfig.Spawner.Default
|
|
if _, exists := podConfigValueMap[defaultPodConfig]; !exists {
|
|
defaultPodConfigPath := field.NewPath("spec", "podTemplate", "options", "podConfig", "spawner", "default")
|
|
errs = append(errs, field.Invalid(defaultPodConfigPath, defaultPodConfig, fmt.Sprintf("default podConfig %q not found", defaultPodConfig)))
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// validateImageConfigValue validates an imageConfig value
|
|
func validateImageConfigValue(imageConfigValue *kubefloworgv1beta1.ImageConfigValue, imageConfigValuePath *field.Path) []*field.Error {
|
|
var errs []*field.Error
|
|
|
|
// validate the ports
|
|
seenPorts := make(map[int32]bool)
|
|
for _, port := range imageConfigValue.Spec.Ports {
|
|
portId := port.Id
|
|
portNumber := port.Port
|
|
if _, exists := seenPorts[portNumber]; exists {
|
|
portPath := imageConfigValuePath.Child("spec", "ports").Key(portId).Child("port")
|
|
errs = append(errs, field.Invalid(portPath, portNumber, fmt.Sprintf("port %d is defined more than once", portNumber)))
|
|
}
|
|
seenPorts[portNumber] = true
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// validateImageConfigRedirects validates redirects in the imageConfig values
|
|
func validateImageConfigRedirects(imageConfigIdMap map[string]kubefloworgv1beta1.ImageConfigValue, imageConfigRedirectMap map[string]string) []*field.Error {
|
|
var errs []*field.Error
|
|
|
|
// validate imageConfig redirects
|
|
checkedNodes := make(map[string]bool)
|
|
for id, redirectTo := range imageConfigRedirectMap {
|
|
// check if there is a cycle involving the current node
|
|
if cycle := helper.DetectGraphCycle(id, checkedNodes, imageConfigRedirectMap); cycle != nil {
|
|
redirectToPath := field.NewPath("spec", "podTemplate", "options", "imageConfig", "values").Key(id).Child("redirect", "to")
|
|
errs = append(errs, field.Invalid(redirectToPath, redirectTo, fmt.Sprintf("imageConfig redirect cycle detected: %v", cycle)))
|
|
break // stop checking redirects if a cycle is detected
|
|
}
|
|
|
|
// ensure the target of the redirect exists
|
|
if _, exists := imageConfigIdMap[redirectTo]; !exists {
|
|
redirectToPath := field.NewPath("spec", "podTemplate", "options", "imageConfig", "values").Key(id).Child("redirect", "to")
|
|
errs = append(errs, field.Invalid(redirectToPath, redirectTo, fmt.Sprintf("target imageConfig %q does not exist", redirectTo)))
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// validatePodConfigRedirects validates redirects in the podConfig values
|
|
func validatePodConfigRedirects(podConfigIdMap map[string]kubefloworgv1beta1.PodConfigValue, podConfigRedirectMap map[string]string) []*field.Error {
|
|
var errs []*field.Error
|
|
|
|
// validate podConfig redirects
|
|
checkedNodes := make(map[string]bool)
|
|
for id, redirectTo := range podConfigRedirectMap {
|
|
// check if there is a cycle involving the current node
|
|
if cycle := helper.DetectGraphCycle(id, checkedNodes, podConfigRedirectMap); cycle != nil {
|
|
redirectToPath := field.NewPath("spec", "podTemplate", "options", "podConfig", "values").Key(id).Child("redirect", "to")
|
|
errs = append(errs, field.Invalid(redirectToPath, redirectTo, fmt.Sprintf("podConfig redirect cycle detected: %v", cycle)))
|
|
break // stop checking redirects if a cycle is detected
|
|
}
|
|
|
|
// ensure the target of the redirect exists
|
|
if _, exists := podConfigIdMap[redirectTo]; !exists {
|
|
redirectToPath := field.NewPath("spec", "podTemplate", "options", "podConfig", "values").Key(id).Child("redirect", "to")
|
|
errs = append(errs, field.Invalid(redirectToPath, redirectTo, fmt.Sprintf("target podConfig %q does not exist", redirectTo)))
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|