notebooks/workspaces/controller/internal/webhook/workspacekind_webhook.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
}