mirror of https://github.com/k3s-io/k3s
279 lines
14 KiB
Go
279 lines
14 KiB
Go
/*
|
|
Copyright 2016 The Kubernetes Authors.
|
|
|
|
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 validation
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/robfig/cron"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
apimachineryvalidation "k8s.io/apimachinery/pkg/util/validation"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
"k8s.io/kubernetes/pkg/apis/batch"
|
|
api "k8s.io/kubernetes/pkg/apis/core"
|
|
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
|
)
|
|
|
|
// ValidateGeneratedSelector validates that the generated selector on a controller object match the controller object
|
|
// metadata, and the labels on the pod template are as generated.
|
|
//
|
|
// TODO: generalize for other controller objects that will follow the same pattern, such as ReplicaSet and DaemonSet, and
|
|
// move to new location. Replace batch.Job with an interface.
|
|
func ValidateGeneratedSelector(obj *batch.Job) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
if obj.Spec.ManualSelector != nil && *obj.Spec.ManualSelector {
|
|
return allErrs
|
|
}
|
|
|
|
if obj.Spec.Selector == nil {
|
|
return allErrs // This case should already have been checked in caller. No need for more errors.
|
|
}
|
|
|
|
// If somehow uid was unset then we would get "controller-uid=" as the selector
|
|
// which is bad.
|
|
if obj.ObjectMeta.UID == "" {
|
|
allErrs = append(allErrs, field.Required(field.NewPath("metadata").Child("uid"), ""))
|
|
}
|
|
|
|
// If selector generation was requested, then expected labels must be
|
|
// present on pod template, and must match job's uid and name. The
|
|
// generated (not-manual) selectors/labels ensure no overlap with other
|
|
// controllers. The manual mode allows orphaning, adoption,
|
|
// backward-compatibility, and experimentation with new
|
|
// labeling/selection schemes. Automatic selector generation should
|
|
// have placed certain labels on the pod, but this could have failed if
|
|
// the user added coflicting labels. Validate that the expected
|
|
// generated ones are there.
|
|
|
|
allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), "controller-uid", string(obj.UID))...)
|
|
allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), "job-name", string(obj.Name))...)
|
|
expectedLabels := make(map[string]string)
|
|
expectedLabels["controller-uid"] = string(obj.UID)
|
|
expectedLabels["job-name"] = string(obj.Name)
|
|
// Whether manually or automatically generated, the selector of the job must match the pods it will produce.
|
|
if selector, err := metav1.LabelSelectorAsSelector(obj.Spec.Selector); err == nil {
|
|
if !selector.Matches(labels.Set(expectedLabels)) {
|
|
allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("selector"), obj.Spec.Selector, "`selector` not auto-generated"))
|
|
}
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJob validates a Job and returns an ErrorList with any errors.
|
|
func ValidateJob(job *batch.Job, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
// Jobs and rcs have the same name validation
|
|
allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidateGeneratedSelector(job)...)
|
|
allErrs = append(allErrs, ValidateJobSpec(&job.Spec, field.NewPath("spec"), opts)...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobSpec validates a JobSpec and returns an ErrorList with any errors.
|
|
func ValidateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
allErrs := validateJobSpec(spec, fldPath, opts)
|
|
|
|
if spec.Selector == nil {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
|
|
} else {
|
|
allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...)
|
|
}
|
|
|
|
// Whether manually or automatically generated, the selector of the job must match the pods it will produce.
|
|
if selector, err := metav1.LabelSelectorAsSelector(spec.Selector); err == nil {
|
|
labels := labels.Set(spec.Template.Labels)
|
|
if !selector.Matches(labels) {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "`selector` does not match template `labels`"))
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
|
|
if spec.Parallelism != nil {
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.Parallelism), fldPath.Child("parallelism"))...)
|
|
}
|
|
if spec.Completions != nil {
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.Completions), fldPath.Child("completions"))...)
|
|
}
|
|
if spec.ActiveDeadlineSeconds != nil {
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.ActiveDeadlineSeconds), fldPath.Child("activeDeadlineSeconds"))...)
|
|
}
|
|
if spec.BackoffLimit != nil {
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.BackoffLimit), fldPath.Child("backoffLimit"))...)
|
|
}
|
|
if spec.TTLSecondsAfterFinished != nil {
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.TTLSecondsAfterFinished), fldPath.Child("ttlSecondsAfterFinished"))...)
|
|
}
|
|
|
|
allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"), opts)...)
|
|
|
|
// spec.Template.Spec.RestartPolicy can be defaulted as RestartPolicyAlways
|
|
// by SetDefaults_PodSpec function when the user does not explicitly specify a value for it,
|
|
// so we check both empty and RestartPolicyAlways cases here
|
|
if spec.Template.Spec.RestartPolicy == api.RestartPolicyAlways || spec.Template.Spec.RestartPolicy == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("template", "spec", "restartPolicy"),
|
|
fmt.Sprintf("valid values: %q, %q", api.RestartPolicyOnFailure, api.RestartPolicyNever)))
|
|
} else if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && spec.Template.Spec.RestartPolicy != api.RestartPolicyNever {
|
|
allErrs = append(allErrs, field.NotSupported(fldPath.Child("template", "spec", "restartPolicy"),
|
|
spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyOnFailure), string(api.RestartPolicyNever)}))
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobStatus validates a JobStatus and returns an ErrorList with any errors.
|
|
func ValidateJobStatus(status *batch.JobStatus, fldPath *field.Path) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Active), fldPath.Child("active"))...)
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Succeeded), fldPath.Child("succeeded"))...)
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Failed), fldPath.Child("failed"))...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobUpdate validates an update to a Job and returns an ErrorList with any errors.
|
|
func ValidateJobUpdate(job, oldJob *batch.Job, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidateJobSpecUpdate(job.Spec, oldJob.Spec, field.NewPath("spec"), opts)...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobUpdateStatus validates an update to the status of a Job and returns an ErrorList with any errors.
|
|
func ValidateJobUpdateStatus(job, oldJob *batch.Job) field.ErrorList {
|
|
allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidateJobStatusUpdate(job.Status, oldJob.Status)...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobSpecUpdate validates an update to a JobSpec and returns an ErrorList with any errors.
|
|
func ValidateJobSpecUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
allErrs = append(allErrs, ValidateJobSpec(&spec, fldPath, opts)...)
|
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath.Child("completions"))...)
|
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Selector, oldSpec.Selector, fldPath.Child("selector"))...)
|
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Template, oldSpec.Template, fldPath.Child("template"))...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobStatusUpdate validates an update to a JobStatus and returns an ErrorList with any errors.
|
|
func ValidateJobStatusUpdate(status, oldStatus batch.JobStatus) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
allErrs = append(allErrs, ValidateJobStatus(&status, field.NewPath("status"))...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateCronJob validates a CronJob and returns an ErrorList with any errors.
|
|
func ValidateCronJob(cronJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
// CronJobs and rcs have the same name validation
|
|
allErrs := apivalidation.ValidateObjectMeta(&cronJob.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidateCronJobSpec(&cronJob.Spec, field.NewPath("spec"), opts)...)
|
|
if len(cronJob.ObjectMeta.Name) > apimachineryvalidation.DNS1035LabelMaxLength-11 {
|
|
// The cronjob controller appends a 11-character suffix to the cronjob (`-$TIMESTAMP`) when
|
|
// creating a job. The job name length limit is 63 characters.
|
|
// Therefore cronjob names must have length <= 63-11=52. If we don't validate this here,
|
|
// then job creation will fail later.
|
|
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), cronJob.ObjectMeta.Name, "must be no more than 52 characters"))
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateCronJobUpdate validates an update to a CronJob and returns an ErrorList with any errors.
|
|
func ValidateCronJobUpdate(job, oldJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidateCronJobSpec(&job.Spec, field.NewPath("spec"), opts)...)
|
|
// skip the 52-character name validation limit on update validation
|
|
// to allow old cronjobs with names > 52 chars to be updated/deleted
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateCronJobSpec validates a CronJobSpec and returns an ErrorList with any errors.
|
|
func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
|
|
if len(spec.Schedule) == 0 {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("schedule"), ""))
|
|
} else {
|
|
allErrs = append(allErrs, validateScheduleFormat(spec.Schedule, fldPath.Child("schedule"))...)
|
|
}
|
|
if spec.StartingDeadlineSeconds != nil {
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.StartingDeadlineSeconds), fldPath.Child("startingDeadlineSeconds"))...)
|
|
}
|
|
allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...)
|
|
allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"), opts)...)
|
|
|
|
if spec.SuccessfulJobsHistoryLimit != nil {
|
|
// zero is a valid SuccessfulJobsHistoryLimit
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.SuccessfulJobsHistoryLimit), fldPath.Child("successfulJobsHistoryLimit"))...)
|
|
}
|
|
if spec.FailedJobsHistoryLimit != nil {
|
|
// zero is a valid SuccessfulJobsHistoryLimit
|
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.FailedJobsHistoryLimit), fldPath.Child("failedJobsHistoryLimit"))...)
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
func validateConcurrencyPolicy(concurrencyPolicy *batch.ConcurrencyPolicy, fldPath *field.Path) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
switch *concurrencyPolicy {
|
|
case batch.AllowConcurrent, batch.ForbidConcurrent, batch.ReplaceConcurrent:
|
|
break
|
|
case "":
|
|
allErrs = append(allErrs, field.Required(fldPath, ""))
|
|
default:
|
|
validValues := []string{string(batch.AllowConcurrent), string(batch.ForbidConcurrent), string(batch.ReplaceConcurrent)}
|
|
allErrs = append(allErrs, field.NotSupported(fldPath, *concurrencyPolicy, validValues))
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
func validateScheduleFormat(schedule string, fldPath *field.Path) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
if _, err := cron.ParseStandard(schedule); err != nil {
|
|
allErrs = append(allErrs, field.Invalid(fldPath, schedule, err.Error()))
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobTemplate validates a JobTemplate and returns an ErrorList with any errors.
|
|
func ValidateJobTemplate(job *batch.JobTemplate, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
// this method should be identical to ValidateJob
|
|
allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidateJobTemplateSpec(&job.Template, field.NewPath("template"), opts)...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateJobTemplateSpec validates a JobTemplateSpec and returns an ErrorList with any errors.
|
|
func ValidateJobTemplateSpec(spec *batch.JobTemplateSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
|
|
allErrs := validateJobSpec(&spec.Spec, fldPath.Child("spec"), opts)
|
|
|
|
// jobtemplate will always have the selector automatically generated
|
|
if spec.Spec.Selector != nil {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("spec", "selector"), spec.Spec.Selector, "`selector` will be auto-generated"))
|
|
}
|
|
if spec.Spec.ManualSelector != nil && *spec.Spec.ManualSelector {
|
|
allErrs = append(allErrs, field.NotSupported(fldPath.Child("spec", "manualSelector"), spec.Spec.ManualSelector, []string{"nil", "false"}))
|
|
}
|
|
return allErrs
|
|
}
|