mirror of https://github.com/knative/client.git
545 lines
17 KiB
Go
545 lines
17 KiB
Go
/*
|
|
Copyright 2019 The Knative 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 serving
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"knative.dev/pkg/apis"
|
|
"knative.dev/pkg/profiling"
|
|
"knative.dev/serving/pkg/apis/networking"
|
|
)
|
|
|
|
const (
|
|
minUserID = 0
|
|
maxUserID = math.MaxInt32
|
|
)
|
|
|
|
var (
|
|
reservedPaths = sets.NewString(
|
|
"/",
|
|
"/dev",
|
|
"/dev/log", // Should be a domain socket
|
|
"/tmp",
|
|
"/var",
|
|
"/var/log",
|
|
)
|
|
|
|
reservedContainerNames = sets.NewString(
|
|
"queue-proxy",
|
|
)
|
|
|
|
reservedEnvVars = sets.NewString(
|
|
"PORT",
|
|
"K_SERVICE",
|
|
"K_CONFIGURATION",
|
|
"K_REVISION",
|
|
)
|
|
|
|
// The port is named "user-port" on the deployment, but a user cannot set an arbitrary name on the port
|
|
// in Configuration. The name field is reserved for content-negotiation. Currently 'h2c' and 'http1' are
|
|
// allowed.
|
|
// https://github.com/knative/serving/blob/master/docs/runtime-contract.md#inbound-network-connectivity
|
|
validPortNames = sets.NewString(
|
|
"h2c",
|
|
"http1",
|
|
"",
|
|
)
|
|
)
|
|
|
|
func ValidateVolumes(vs []corev1.Volume) (sets.String, *apis.FieldError) {
|
|
volumes := sets.NewString()
|
|
var errs *apis.FieldError
|
|
for i, volume := range vs {
|
|
if volumes.Has(volume.Name) {
|
|
errs = errs.Also((&apis.FieldError{
|
|
Message: fmt.Sprintf("duplicate volume name %q", volume.Name),
|
|
Paths: []string{"name"},
|
|
}).ViaIndex(i))
|
|
}
|
|
errs = errs.Also(validateVolume(volume).ViaIndex(i))
|
|
volumes.Insert(volume.Name)
|
|
}
|
|
return volumes, errs
|
|
}
|
|
|
|
func validateVolume(volume corev1.Volume) *apis.FieldError {
|
|
errs := apis.CheckDisallowedFields(volume, *VolumeMask(&volume))
|
|
if volume.Name == "" {
|
|
errs = apis.ErrMissingField("name")
|
|
} else if len(validation.IsDNS1123Label(volume.Name)) != 0 {
|
|
errs = apis.ErrInvalidValue(volume.Name, "name")
|
|
}
|
|
|
|
vs := volume.VolumeSource
|
|
errs = errs.Also(apis.CheckDisallowedFields(vs, *VolumeSourceMask(&vs)))
|
|
specified := []string{}
|
|
if vs.Secret != nil {
|
|
specified = append(specified, "secret")
|
|
for i, item := range vs.Secret.Items {
|
|
errs = errs.Also(validateKeyToPath(item).ViaFieldIndex("items", i))
|
|
}
|
|
}
|
|
if vs.ConfigMap != nil {
|
|
specified = append(specified, "configMap")
|
|
for i, item := range vs.ConfigMap.Items {
|
|
errs = errs.Also(validateKeyToPath(item).ViaFieldIndex("items", i))
|
|
}
|
|
}
|
|
if vs.Projected != nil {
|
|
specified = append(specified, "projected")
|
|
for i, proj := range vs.Projected.Sources {
|
|
errs = errs.Also(validateProjectedVolumeSource(proj).ViaFieldIndex("projected", i))
|
|
}
|
|
}
|
|
if len(specified) == 0 {
|
|
errs = errs.Also(apis.ErrMissingOneOf("secret", "configMap", "projected"))
|
|
} else if len(specified) > 1 {
|
|
errs = errs.Also(apis.ErrMultipleOneOf(specified...))
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
func validateProjectedVolumeSource(vp corev1.VolumeProjection) *apis.FieldError {
|
|
errs := apis.CheckDisallowedFields(vp, *VolumeProjectionMask(&vp))
|
|
specified := []string{}
|
|
if vp.Secret != nil {
|
|
specified = append(specified, "secret")
|
|
errs = errs.Also(validateSecretProjection(vp.Secret).ViaField("secret"))
|
|
}
|
|
if vp.ConfigMap != nil {
|
|
specified = append(specified, "configMap")
|
|
errs = errs.Also(validateConfigMapProjection(vp.ConfigMap).ViaField("configMap"))
|
|
}
|
|
if len(specified) == 0 {
|
|
errs = errs.Also(apis.ErrMissingOneOf("secret", "configMap"))
|
|
} else if len(specified) > 1 {
|
|
errs = errs.Also(apis.ErrMultipleOneOf(specified...))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func validateConfigMapProjection(cmp *corev1.ConfigMapProjection) *apis.FieldError {
|
|
errs := apis.CheckDisallowedFields(*cmp, *ConfigMapProjectionMask(cmp))
|
|
errs = errs.Also(apis.CheckDisallowedFields(
|
|
cmp.LocalObjectReference, *LocalObjectReferenceMask(&cmp.LocalObjectReference)))
|
|
if cmp.Name == "" {
|
|
errs = errs.Also(apis.ErrMissingField("name"))
|
|
}
|
|
for i, item := range cmp.Items {
|
|
errs = errs.Also(validateKeyToPath(item).ViaFieldIndex("items", i))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func validateSecretProjection(sp *corev1.SecretProjection) *apis.FieldError {
|
|
errs := apis.CheckDisallowedFields(*sp, *SecretProjectionMask(sp))
|
|
errs = errs.Also(apis.CheckDisallowedFields(
|
|
sp.LocalObjectReference, *LocalObjectReferenceMask(&sp.LocalObjectReference)))
|
|
if sp.Name == "" {
|
|
errs = errs.Also(apis.ErrMissingField("name"))
|
|
}
|
|
for i, item := range sp.Items {
|
|
errs = errs.Also(validateKeyToPath(item).ViaFieldIndex("items", i))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func validateKeyToPath(k2p corev1.KeyToPath) *apis.FieldError {
|
|
errs := apis.CheckDisallowedFields(k2p, *KeyToPathMask(&k2p))
|
|
if k2p.Key == "" {
|
|
errs = errs.Also(apis.ErrMissingField("key"))
|
|
}
|
|
if k2p.Path == "" {
|
|
errs = errs.Also(apis.ErrMissingField("path"))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func validateEnvValueFrom(source *corev1.EnvVarSource) *apis.FieldError {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return apis.CheckDisallowedFields(*source, *EnvVarSourceMask(source))
|
|
}
|
|
|
|
func validateEnvVar(env corev1.EnvVar) *apis.FieldError {
|
|
errs := apis.CheckDisallowedFields(env, *EnvVarMask(&env))
|
|
|
|
if env.Name == "" {
|
|
errs = errs.Also(apis.ErrMissingField("name"))
|
|
} else if reservedEnvVars.Has(env.Name) {
|
|
errs = errs.Also(&apis.FieldError{
|
|
Message: fmt.Sprintf("%q is a reserved environment variable", env.Name),
|
|
Paths: []string{"name"},
|
|
})
|
|
}
|
|
|
|
return errs.Also(validateEnvValueFrom(env.ValueFrom).ViaField("valueFrom"))
|
|
}
|
|
|
|
func validateEnv(envVars []corev1.EnvVar) *apis.FieldError {
|
|
var errs *apis.FieldError
|
|
for i, env := range envVars {
|
|
errs = errs.Also(validateEnvVar(env).ViaIndex(i))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func validateEnvFrom(envFromList []corev1.EnvFromSource) *apis.FieldError {
|
|
var errs *apis.FieldError
|
|
for i, envFrom := range envFromList {
|
|
errs = errs.Also(apis.CheckDisallowedFields(envFrom, *EnvFromSourceMask(&envFrom)).ViaIndex(i))
|
|
|
|
cm := envFrom.ConfigMapRef
|
|
sm := envFrom.SecretRef
|
|
if sm != nil {
|
|
errs = errs.Also(apis.CheckDisallowedFields(*sm, *SecretEnvSourceMask(sm)).ViaIndex(i))
|
|
errs = errs.Also(apis.CheckDisallowedFields(
|
|
sm.LocalObjectReference, *LocalObjectReferenceMask(&sm.LocalObjectReference))).ViaIndex(i).ViaField("secretRef")
|
|
}
|
|
|
|
if cm != nil {
|
|
errs = errs.Also(apis.CheckDisallowedFields(*cm, *ConfigMapEnvSourceMask(cm)).ViaIndex(i))
|
|
errs = errs.Also(apis.CheckDisallowedFields(
|
|
cm.LocalObjectReference, *LocalObjectReferenceMask(&cm.LocalObjectReference))).ViaIndex(i).ViaField("configMapRef")
|
|
}
|
|
if cm != nil && sm != nil {
|
|
errs = errs.Also(apis.ErrMultipleOneOf("configMapRef", "secretRef"))
|
|
} else if cm == nil && sm == nil {
|
|
errs = errs.Also(apis.ErrMissingOneOf("configMapRef", "secretRef"))
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func ValidatePodSpec(ps corev1.PodSpec) *apis.FieldError {
|
|
// This is inlined, and so it makes for a less meaningful
|
|
// error message.
|
|
// if equality.Semantic.DeepEqual(ps, corev1.PodSpec{}) {
|
|
// return apis.ErrMissingField(apis.CurrentField)
|
|
// }
|
|
|
|
errs := apis.CheckDisallowedFields(ps, *PodSpecMask(&ps))
|
|
|
|
volumes, err := ValidateVolumes(ps.Volumes)
|
|
if err != nil {
|
|
errs = errs.Also(err.ViaField("volumes"))
|
|
}
|
|
|
|
switch len(ps.Containers) {
|
|
case 0:
|
|
errs = errs.Also(apis.ErrMissingField("containers"))
|
|
case 1:
|
|
errs = errs.Also(ValidateContainer(ps.Containers[0], volumes).
|
|
ViaFieldIndex("containers", 0))
|
|
default:
|
|
errs = errs.Also(apis.ErrMultipleOneOf("containers"))
|
|
}
|
|
if ps.ServiceAccountName != "" {
|
|
for range validation.IsDNS1123Subdomain(ps.ServiceAccountName) {
|
|
errs = errs.Also(apis.ErrInvalidValue("serviceAccountName", ps.ServiceAccountName))
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func ValidateContainer(container corev1.Container, volumes sets.String) *apis.FieldError {
|
|
if equality.Semantic.DeepEqual(container, corev1.Container{}) {
|
|
return apis.ErrMissingField(apis.CurrentField)
|
|
}
|
|
|
|
errs := apis.CheckDisallowedFields(container, *ContainerMask(&container))
|
|
|
|
if reservedContainerNames.Has(container.Name) {
|
|
errs = errs.Also(&apis.FieldError{
|
|
Message: fmt.Sprintf("%q is a reserved container name", container.Name),
|
|
Paths: []string{"name"},
|
|
})
|
|
}
|
|
|
|
// Env
|
|
errs = errs.Also(validateEnv(container.Env).ViaField("env"))
|
|
// EnvFrom
|
|
errs = errs.Also(validateEnvFrom(container.EnvFrom).ViaField("envFrom"))
|
|
// Image
|
|
if container.Image == "" {
|
|
errs = errs.Also(apis.ErrMissingField("image"))
|
|
} else if _, err := name.ParseReference(container.Image, name.WeakValidation); err != nil {
|
|
fe := &apis.FieldError{
|
|
Message: "Failed to parse image reference",
|
|
Paths: []string{"image"},
|
|
Details: fmt.Sprintf("image: %q, error: %v", container.Image, err),
|
|
}
|
|
errs = errs.Also(fe)
|
|
}
|
|
// Liveness Probes
|
|
errs = errs.Also(validateProbe(container.LivenessProbe).ViaField("livenessProbe"))
|
|
// Ports
|
|
errs = errs.Also(validateContainerPorts(container.Ports).ViaField("ports"))
|
|
// Readiness Probes
|
|
errs = errs.Also(validateReadinessProbe(container.ReadinessProbe).ViaField("readinessProbe"))
|
|
// Resources
|
|
errs = errs.Also(validateResources(&container.Resources).ViaField("resources"))
|
|
// SecurityContext
|
|
errs = errs.Also(validateSecurityContext(container.SecurityContext).ViaField("securityContext"))
|
|
// TerminationMessagePolicy
|
|
switch container.TerminationMessagePolicy {
|
|
case corev1.TerminationMessageReadFile, corev1.TerminationMessageFallbackToLogsOnError, "":
|
|
default:
|
|
errs = errs.Also(apis.ErrInvalidValue(container.TerminationMessagePolicy, "terminationMessagePolicy"))
|
|
}
|
|
// VolumeMounts
|
|
errs = errs.Also(validateVolumeMounts(container.VolumeMounts, volumes).ViaField("volumeMounts"))
|
|
|
|
return errs
|
|
}
|
|
|
|
func validateResources(resources *corev1.ResourceRequirements) *apis.FieldError {
|
|
if resources == nil {
|
|
return nil
|
|
}
|
|
return apis.CheckDisallowedFields(*resources, *ResourceRequirementsMask(resources))
|
|
}
|
|
|
|
func validateSecurityContext(sc *corev1.SecurityContext) *apis.FieldError {
|
|
if sc == nil {
|
|
return nil
|
|
}
|
|
errs := apis.CheckDisallowedFields(*sc, *SecurityContextMask(sc))
|
|
|
|
if sc.RunAsUser != nil {
|
|
uid := *sc.RunAsUser
|
|
if uid < minUserID || uid > maxUserID {
|
|
errs = errs.Also(apis.ErrOutOfBoundsValue(uid, minUserID, maxUserID, "runAsUser"))
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func validateVolumeMounts(mounts []corev1.VolumeMount, volumes sets.String) *apis.FieldError {
|
|
var errs *apis.FieldError
|
|
// Check that volume mounts match names in "volumes", that "volumes" has 100%
|
|
// coverage, and the field restrictions.
|
|
seenName := sets.NewString()
|
|
seenMountPath := sets.NewString()
|
|
for i, vm := range mounts {
|
|
errs = errs.Also(apis.CheckDisallowedFields(vm, *VolumeMountMask(&vm)).ViaIndex(i))
|
|
// This effectively checks that Name is non-empty because Volume name must be non-empty.
|
|
if !volumes.Has(vm.Name) {
|
|
errs = errs.Also((&apis.FieldError{
|
|
Message: "volumeMount has no matching volume",
|
|
Paths: []string{"name"},
|
|
}).ViaIndex(i))
|
|
}
|
|
seenName.Insert(vm.Name)
|
|
|
|
if vm.MountPath == "" {
|
|
errs = errs.Also(apis.ErrMissingField("mountPath").ViaIndex(i))
|
|
} else if reservedPaths.Has(filepath.Clean(vm.MountPath)) {
|
|
errs = errs.Also((&apis.FieldError{
|
|
Message: fmt.Sprintf("mountPath %q is a reserved path", filepath.Clean(vm.MountPath)),
|
|
Paths: []string{"mountPath"},
|
|
}).ViaIndex(i))
|
|
} else if !filepath.IsAbs(vm.MountPath) {
|
|
errs = errs.Also(apis.ErrInvalidValue(vm.MountPath, "mountPath").ViaIndex(i))
|
|
} else if seenMountPath.Has(filepath.Clean(vm.MountPath)) {
|
|
errs = errs.Also(apis.ErrInvalidValue(
|
|
fmt.Sprintf("%q must be unique", vm.MountPath), "mountPath").ViaIndex(i))
|
|
}
|
|
seenMountPath.Insert(filepath.Clean(vm.MountPath))
|
|
|
|
if !vm.ReadOnly {
|
|
errs = errs.Also(apis.ErrMissingField("readOnly").ViaIndex(i))
|
|
}
|
|
|
|
}
|
|
|
|
if missing := volumes.Difference(seenName); missing.Len() > 0 {
|
|
errs = errs.Also(&apis.FieldError{
|
|
Message: fmt.Sprintf("volumes not mounted: %v", missing.List()),
|
|
Paths: []string{apis.CurrentField},
|
|
})
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func validateContainerPorts(ports []corev1.ContainerPort) *apis.FieldError {
|
|
if len(ports) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var errs *apis.FieldError
|
|
|
|
// user can set container port which names "user-port" to define application's port.
|
|
// Queue-proxy will use it to send requests to application
|
|
// if user didn't set any port, it will set default port user-port=8080.
|
|
if len(ports) > 1 {
|
|
errs = errs.Also(&apis.FieldError{
|
|
Message: "More than one container port is set",
|
|
Paths: []string{apis.CurrentField},
|
|
Details: "Only a single port is allowed",
|
|
})
|
|
}
|
|
|
|
userPort := ports[0]
|
|
|
|
errs = errs.Also(apis.CheckDisallowedFields(userPort, *ContainerPortMask(&userPort)))
|
|
|
|
// Only allow empty (defaulting to "TCP") or explicit TCP for protocol
|
|
if userPort.Protocol != "" && userPort.Protocol != corev1.ProtocolTCP {
|
|
errs = errs.Also(apis.ErrInvalidValue(userPort.Protocol, "protocol"))
|
|
}
|
|
|
|
// Don't allow userPort to conflict with QueueProxy sidecar
|
|
if userPort.ContainerPort == networking.BackendHTTPPort ||
|
|
userPort.ContainerPort == networking.BackendHTTP2Port ||
|
|
userPort.ContainerPort == networking.QueueAdminPort ||
|
|
userPort.ContainerPort == networking.AutoscalingQueueMetricsPort ||
|
|
userPort.ContainerPort == networking.UserQueueMetricsPort ||
|
|
userPort.ContainerPort == profiling.ProfilingPort {
|
|
errs = errs.Also(apis.ErrInvalidValue(userPort.ContainerPort, "containerPort"))
|
|
}
|
|
|
|
if userPort.ContainerPort < 0 || userPort.ContainerPort > 65535 {
|
|
errs = errs.Also(apis.ErrOutOfBoundsValue(userPort.ContainerPort,
|
|
0, 65535, "containerPort"))
|
|
}
|
|
|
|
if !validPortNames.Has(userPort.Name) {
|
|
errs = errs.Also(&apis.FieldError{
|
|
Message: fmt.Sprintf("Port name %v is not allowed", ports[0].Name),
|
|
Paths: []string{apis.CurrentField},
|
|
Details: "Name must be empty, or one of: 'h2c', 'http1'",
|
|
})
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
func validateReadinessProbe(p *corev1.Probe) *apis.FieldError {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
|
|
errs := validateProbe(p)
|
|
|
|
if p.PeriodSeconds < 0 {
|
|
errs = errs.Also(apis.ErrOutOfBoundsValue(p.PeriodSeconds, 0, math.MaxInt32, "periodSeconds"))
|
|
}
|
|
|
|
if p.InitialDelaySeconds < 0 {
|
|
errs = errs.Also(apis.ErrOutOfBoundsValue(p.InitialDelaySeconds, 0, math.MaxInt32, "initialDelaySeconds"))
|
|
}
|
|
|
|
if p.SuccessThreshold < 1 {
|
|
errs = errs.Also(apis.ErrOutOfBoundsValue(p.SuccessThreshold, 1, math.MaxInt32, "successThreshold"))
|
|
}
|
|
|
|
// PeriodSeconds == 0 indicates Knative's special probe with aggressive retries
|
|
if p.PeriodSeconds == 0 {
|
|
if p.FailureThreshold != 0 {
|
|
errs = errs.Also(&apis.FieldError{
|
|
Message: "failureThreshold is disallowed when periodSeconds is zero",
|
|
Paths: []string{"failureThreshold"},
|
|
})
|
|
}
|
|
|
|
if p.TimeoutSeconds != 0 {
|
|
errs = errs.Also(&apis.FieldError{
|
|
Message: "timeoutSeconds is disallowed when periodSeconds is zero",
|
|
Paths: []string{"timeoutSeconds"},
|
|
})
|
|
}
|
|
} else {
|
|
if p.TimeoutSeconds < 1 {
|
|
errs = errs.Also(apis.ErrOutOfBoundsValue(p.TimeoutSeconds, 1, math.MaxInt32, "timeoutSeconds"))
|
|
}
|
|
|
|
if p.FailureThreshold < 1 {
|
|
errs = errs.Also(apis.ErrOutOfBoundsValue(p.FailureThreshold, 1, math.MaxInt32, "failureThreshold"))
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
func validateProbe(p *corev1.Probe) *apis.FieldError {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
errs := apis.CheckDisallowedFields(*p, *ProbeMask(p))
|
|
|
|
h := p.Handler
|
|
errs = errs.Also(apis.CheckDisallowedFields(h, *HandlerMask(&h)))
|
|
|
|
var handlers []string
|
|
|
|
if h.HTTPGet != nil {
|
|
handlers = append(handlers, "httpGet")
|
|
errs = errs.Also(apis.CheckDisallowedFields(*h.HTTPGet, *HTTPGetActionMask(h.HTTPGet))).ViaField("httpGet")
|
|
}
|
|
if h.TCPSocket != nil {
|
|
handlers = append(handlers, "tcpSocket")
|
|
errs = errs.Also(apis.CheckDisallowedFields(*h.TCPSocket, *TCPSocketActionMask(h.TCPSocket))).ViaField("tcpSocket")
|
|
}
|
|
if h.Exec != nil {
|
|
handlers = append(handlers, "exec")
|
|
errs = errs.Also(apis.CheckDisallowedFields(*h.Exec, *ExecActionMask(h.Exec))).ViaField("exec")
|
|
}
|
|
|
|
if len(handlers) == 0 {
|
|
errs = errs.Also(apis.ErrMissingOneOf("httpGet", "tcpSocket", "exec"))
|
|
} else if len(handlers) > 1 {
|
|
errs = errs.Also(apis.ErrMultipleOneOf(handlers...))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func ValidateNamespacedObjectReference(p *corev1.ObjectReference) *apis.FieldError {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
errs := apis.CheckDisallowedFields(*p, *NamespacedObjectReferenceMask(p))
|
|
|
|
if p.APIVersion == "" {
|
|
errs = errs.Also(apis.ErrMissingField("apiVersion"))
|
|
} else if verrs := validation.IsQualifiedName(p.APIVersion); len(verrs) != 0 {
|
|
errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "apiVersion"))
|
|
}
|
|
if p.Kind == "" {
|
|
errs = errs.Also(apis.ErrMissingField("kind"))
|
|
} else if verrs := validation.IsCIdentifier(p.Kind); len(verrs) != 0 {
|
|
errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "kind"))
|
|
}
|
|
if p.Name == "" {
|
|
errs = errs.Also(apis.ErrMissingField("name"))
|
|
} else if verrs := validation.IsDNS1123Label(p.Name); len(verrs) != 0 {
|
|
errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "name"))
|
|
}
|
|
return errs
|
|
}
|