pkg/cmd/server/api/validation/master.go
53c7aa34
 package validation
 
 import (
57894264
 	"fmt"
53c7aa34
 	"net"
 	"net/url"
81b520f2
 	"reflect"
a8dd903f
 	"regexp"
53c7aa34
 	"strings"
83e7080d
 	"time"
53c7aa34
 
7aabe6b9
 	apiserveroptions "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
 	controlleroptions "k8s.io/kubernetes/cmd/kube-controller-manager/app/options"
83c702b4
 	kvalidation "k8s.io/kubernetes/pkg/api/validation"
43ba781b
 	"k8s.io/kubernetes/pkg/serviceaccount"
 	knet "k8s.io/kubernetes/pkg/util/net"
3dd75654
 	"k8s.io/kubernetes/pkg/util/sets"
987aca9b
 	kuval "k8s.io/kubernetes/pkg/util/validation"
398ef03e
 	"k8s.io/kubernetes/pkg/util/validation/field"
53c7aa34
 
 	"github.com/openshift/origin/pkg/cmd/server/api"
878d37a8
 	"github.com/openshift/origin/pkg/cmd/server/bootstrappolicy"
ec5a5ac6
 	"github.com/openshift/origin/pkg/security/mcs"
 	"github.com/openshift/origin/pkg/security/uid"
abd33eda
 	"github.com/openshift/origin/pkg/util/labelselector"
53c7aa34
 )
 
e00edaae
 // TODO: this should just be two return arrays, no need to be clever
26c990fe
 type ValidationResults struct {
398ef03e
 	Warnings field.ErrorList
 	Errors   field.ErrorList
26c990fe
 }
 
 func (r *ValidationResults) Append(additionalResults ValidationResults) {
 	r.AddErrors(additionalResults.Errors...)
 	r.AddWarnings(additionalResults.Warnings...)
 }
 
398ef03e
 func (r *ValidationResults) AddErrors(errors ...*field.Error) {
26c990fe
 	if len(errors) == 0 {
 		return
 	}
 	r.Errors = append(r.Errors, errors...)
 }
 
398ef03e
 func (r *ValidationResults) AddWarnings(warnings ...*field.Error) {
26c990fe
 	if len(warnings) == 0 {
 		return
 	}
 	r.Warnings = append(r.Warnings, warnings...)
 }
 
398ef03e
 func ValidateMasterConfig(config *api.MasterConfig, fldPath *field.Path) ValidationResults {
26c990fe
 	validationResults := ValidationResults{}
53c7aa34
 
398ef03e
 	if _, urlErrs := ValidateURL(config.MasterPublicURL, fldPath.Child("masterPublicURL")); len(urlErrs) > 0 {
26c990fe
 		validationResults.AddErrors(urlErrs...)
f53feeed
 	}
 
69491ff4
 	switch {
 	case config.ControllerLeaseTTL > 300,
 		config.ControllerLeaseTTL < -1,
 		config.ControllerLeaseTTL > 0 && config.ControllerLeaseTTL < 10:
398ef03e
 		validationResults.AddErrors(field.Invalid(fldPath.Child("controllerLeaseTTL"), config.ControllerLeaseTTL, "TTL must be -1 (disabled), 0 (default), or between 10 and 300 seconds"))
69491ff4
 	}
 
398ef03e
 	validationResults.AddErrors(ValidateDisabledFeatures(config.DisabledFeatures, fldPath.Child("disabledFeatures"))...)
642797f7
 
53c7aa34
 	if config.AssetConfig != nil {
398ef03e
 		assetConfigPath := fldPath.Child("assetConfig")
 		validationResults.Append(ValidateAssetConfig(config.AssetConfig, assetConfigPath))
53c7aa34
 		colocated := config.AssetConfig.ServingInfo.BindAddress == config.ServingInfo.BindAddress
 		if colocated {
 			publicURL, _ := url.Parse(config.AssetConfig.PublicURL)
 			if publicURL.Path == "/" {
398ef03e
 				validationResults.AddErrors(field.Invalid(assetConfigPath.Child("publicURL"), config.AssetConfig.PublicURL, "path can not be / when colocated with master API"))
53c7aa34
 			}
3d3db986
 
 			// Warn if they have customized the asset certificates in ways that will be ignored
 			if !reflect.DeepEqual(config.AssetConfig.ServingInfo.ServerCert, config.ServingInfo.ServerCert) ||
 				!reflect.DeepEqual(config.AssetConfig.ServingInfo.NamedCertificates, config.ServingInfo.NamedCertificates) {
398ef03e
 				validationResults.AddWarnings(field.Invalid(assetConfigPath.Child("servingInfo"), "<not displayed>", "changes to assetConfig certificate configuration are not used when colocated with master API"))
81b520f2
 			}
53c7aa34
 		}
 
 		if config.OAuthConfig != nil {
 			if config.OAuthConfig.AssetPublicURL != config.AssetConfig.PublicURL {
26c990fe
 				validationResults.AddErrors(
398ef03e
 					field.Invalid(assetConfigPath.Child("publicURL"), config.AssetConfig.PublicURL, "must match oauthConfig.assetPublicURL"),
 					field.Invalid(fldPath.Child("oauthConfig", "assetPublicURL"), config.OAuthConfig.AssetPublicURL, "must match assetConfig.publicURL"),
53c7aa34
 				)
 			}
 		}
 
 		// TODO warn when the CORS list does not include the assetConfig.publicURL host:port
 		// only warn cause they could handle CORS headers themselves in a proxy
 	}
 
 	if config.DNSConfig != nil {
398ef03e
 		dnsConfigPath := fldPath.Child("dnsConfig")
 		validationResults.AddErrors(ValidateHostPort(config.DNSConfig.BindAddress, dnsConfigPath.Child("bindAddress"))...)
429c4b0b
 		switch config.DNSConfig.BindNetwork {
 		case "tcp", "tcp4", "tcp6":
 		default:
398ef03e
 			validationResults.AddErrors(field.Invalid(dnsConfigPath.Child("bindNetwork"), config.DNSConfig.BindNetwork, "must be 'tcp', 'tcp4', or 'tcp6'"))
429c4b0b
 		}
53c7aa34
 	}
 
 	if config.EtcdConfig != nil {
398ef03e
 		etcdConfigErrs := ValidateEtcdConfig(config.EtcdConfig, fldPath.Child("etcdConfig"))
81b520f2
 		validationResults.Append(etcdConfigErrs)
53c7aa34
 
81b520f2
 		if len(etcdConfigErrs.Errors) == 0 {
53c7aa34
 			// Validate the etcdClientInfo with the internal etcdConfig
398ef03e
 			validationResults.AddErrors(ValidateEtcdConnectionInfo(config.EtcdClientInfo, config.EtcdConfig, fldPath.Child("etcdClientInfo"))...)
53c7aa34
 		} else {
 			// Validate the etcdClientInfo by itself
398ef03e
 			validationResults.AddErrors(ValidateEtcdConnectionInfo(config.EtcdClientInfo, nil, fldPath.Child("etcdClientInfo"))...)
53c7aa34
 		}
 	} else {
 		// Validate the etcdClientInfo by itself
398ef03e
 		validationResults.AddErrors(ValidateEtcdConnectionInfo(config.EtcdClientInfo, nil, fldPath.Child("etcdClientInfo"))...)
53c7aa34
 	}
398ef03e
 	validationResults.AddErrors(ValidateEtcdStorageConfig(config.EtcdStorageConfig, fldPath.Child("etcdStorageConfig"))...)
53c7aa34
 
398ef03e
 	validationResults.AddErrors(ValidateImageConfig(config.ImageConfig, fldPath.Child("imageConfig"))...)
53c7aa34
 
0e9494df
 	validationResults.AddErrors(ValidateImagePolicyConfig(config.ImagePolicyConfig, fldPath.Child("imagePolicyConfig"))...)
 
398ef03e
 	validationResults.AddErrors(ValidateKubeletConnectionInfo(config.KubeletClientInfo, fldPath.Child("kubeletClientInfo"))...)
53c7aa34
 
878d37a8
 	builtInKubernetes := config.KubernetesMasterConfig != nil
53c7aa34
 	if config.KubernetesMasterConfig != nil {
398ef03e
 		validationResults.Append(ValidateKubernetesMasterConfig(config.KubernetesMasterConfig, fldPath.Child("kubernetesMasterConfig")))
53c7aa34
 	}
da1980d3
 	if (config.KubernetesMasterConfig == nil) && (len(config.MasterClients.ExternalKubernetesKubeConfig) == 0) {
398ef03e
 		validationResults.AddErrors(field.Invalid(fldPath.Child("kubernetesMasterConfig"), config.KubernetesMasterConfig, "either kubernetesMasterConfig or masterClients.externalKubernetesKubeConfig must have a value"))
da1980d3
 	}
 	if (config.KubernetesMasterConfig != nil) && (len(config.MasterClients.ExternalKubernetesKubeConfig) != 0) {
398ef03e
 		validationResults.AddErrors(field.Invalid(fldPath.Child("kubernetesMasterConfig"), config.KubernetesMasterConfig, "kubernetesMasterConfig and masterClients.externalKubernetesKubeConfig are mutually exclusive"))
da1980d3
 	}
53c7aa34
 
543dd90e
 	if len(config.NetworkConfig.ServiceNetworkCIDR) > 0 {
 		if _, _, err := net.ParseCIDR(strings.TrimSpace(config.NetworkConfig.ServiceNetworkCIDR)); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("networkConfig", "serviceNetworkCIDR"), config.NetworkConfig.ServiceNetworkCIDR, "must be a valid CIDR notation IP range (e.g. 172.30.0.0/16)"))
543dd90e
 		} else if config.KubernetesMasterConfig != nil && len(config.KubernetesMasterConfig.ServicesSubnet) > 0 && config.KubernetesMasterConfig.ServicesSubnet != config.NetworkConfig.ServiceNetworkCIDR {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("networkConfig", "serviceNetworkCIDR"), config.NetworkConfig.ServiceNetworkCIDR, fmt.Sprintf("must match kubernetesMasterConfig.servicesSubnet value of %q", config.KubernetesMasterConfig.ServicesSubnet)))
543dd90e
 		}
 	}
290ade01
 	if len(config.NetworkConfig.ExternalIPNetworkCIDRs) > 0 {
 		for i, s := range config.NetworkConfig.ExternalIPNetworkCIDRs {
 			if strings.HasPrefix(s, "!") {
 				s = s[1:]
 			}
 			if _, _, err := net.ParseCIDR(s); err != nil {
 				validationResults.AddErrors(field.Invalid(fldPath.Child("networkConfig", "externalIPNetworkCIDRs").Index(i), config.NetworkConfig.ExternalIPNetworkCIDRs[i], "must be a valid CIDR notation IP range (e.g. 172.30.0.0/16) with an optional leading !"))
 			}
 		}
 	}
346982a0
 
 	validationResults.AddErrors(ValidateIngressIPNetworkCIDR(config, fldPath.Child("networkConfig", "ingressIPNetworkCIDR").Index(0))...)
543dd90e
 
398ef03e
 	validationResults.AddErrors(ValidateKubeConfig(config.MasterClients.OpenShiftLoopbackKubeConfig, fldPath.Child("masterClients", "openShiftLoopbackKubeConfig"))...)
da1980d3
 
 	if len(config.MasterClients.ExternalKubernetesKubeConfig) > 0 {
398ef03e
 		validationResults.AddErrors(ValidateKubeConfig(config.MasterClients.ExternalKubernetesKubeConfig, fldPath.Child("masterClients", "externalKubernetesKubeConfig"))...)
da1980d3
 	}
53c7aa34
 
398ef03e
 	validationResults.AddErrors(ValidatePolicyConfig(config.PolicyConfig, fldPath.Child("policyConfig"))...)
53c7aa34
 	if config.OAuthConfig != nil {
398ef03e
 		validationResults.Append(ValidateOAuthConfig(config.OAuthConfig, fldPath.Child("oauthConfig")))
53c7aa34
 	}
 
398ef03e
 	validationResults.Append(ValidateServiceAccountConfig(config.ServiceAccountConfig, builtInKubernetes, fldPath.Child("serviceAccountConfig")))
878d37a8
 
398ef03e
 	validationResults.Append(ValidateHTTPServingInfo(config.ServingInfo, fldPath.Child("servingInfo")))
53c7aa34
 
398ef03e
 	validationResults.Append(ValidateProjectConfig(config.ProjectConfig, fldPath.Child("projectConfig")))
384f11fa
 
398ef03e
 	validationResults.AddErrors(ValidateRoutingConfig(config.RoutingConfig, fldPath.Child("routingConfig"))...)
321d1032
 
398ef03e
 	validationResults.Append(ValidateAPILevels(config.APILevels, api.KnownOpenShiftAPILevels, api.DeadOpenShiftAPILevels, fldPath.Child("apiLevels")))
26c990fe
 
00b6e181
 	if config.AdmissionConfig.PluginConfig != nil {
398ef03e
 		validationResults.AddErrors(ValidateAdmissionPluginConfig(config.AdmissionConfig.PluginConfig, fldPath.Child("admissionConfig", "pluginConfig"))...)
3a98b97f
 		validationResults.Append(ValidateAdmissionPluginConfigConflicts(config))
 	}
 	if len(config.AdmissionConfig.PluginOrderOverride) != 0 {
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("admissionConfig", "pluginOrderOverride"), config.AdmissionConfig.PluginOrderOverride, "specified admission ordering is being phased out.  Convert to DefaultAdmissionConfig in admissionConfig.pluginConfig."))
00b6e181
 	}
 
99df912d
 	validationResults.Append(ValidateControllerConfig(config.ControllerConfig, fldPath.Child("controllerConfig")))
 
59e59e37
 	validationResults.Append(ValidateAuditConfig(config.AuditConfig, fldPath.Child("auditConfig")))
 
 	return validationResults
 }
 
 func ValidateAuditConfig(config api.AuditConfig, fldPath *field.Path) ValidationResults {
 	validationResults := ValidationResults{}
 
 	if len(config.AuditFilePath) == 0 {
 		// for backwards compatibility reasons we can't error this out
 		validationResults.AddWarnings(field.Required(fldPath.Child("auditFilePath"), "audit can now be logged to a separate file"))
 	}
 	if config.MaximumFileRetentionDays < 0 {
 		validationResults.AddErrors(field.Invalid(fldPath.Child("maximumFileRetentionDays"), config.MaximumFileRetentionDays, "must be greater than or equal to 0"))
 	}
 	if config.MaximumRetainedFiles < 0 {
 		validationResults.AddErrors(field.Invalid(fldPath.Child("maximumRetainedFiles"), config.MaximumRetainedFiles, "must be greater than or equal to 0"))
 	}
 	if config.MaximumFileSizeMegabytes < 0 {
 		validationResults.AddErrors(field.Invalid(fldPath.Child("maximumFileSizeMegabytes"), config.MaximumFileSizeMegabytes, "must be greater than or equal to 0"))
 	}
 
99df912d
 	return validationResults
 }
 
 func ValidateControllerConfig(config api.ControllerConfig, fldPath *field.Path) ValidationResults {
 	validationResults := ValidationResults{}
 
 	if config.ServiceServingCert.Signer != nil {
 		validationResults.AddErrors(ValidateCertInfo(*config.ServiceServingCert.Signer, true, fldPath.Child("serviceServingCert.signer"))...)
 	}
 
26c990fe
 	return validationResults
 }
 
398ef03e
 func ValidateAPILevels(apiLevels []string, knownAPILevels, deadAPILevels []string, fldPath *field.Path) ValidationResults {
26c990fe
 	validationResults := ValidationResults{}
 
 	if len(apiLevels) == 0 {
43ba781b
 		validationResults.AddErrors(field.Required(fldPath, ""))
26c990fe
 	}
 
3dd75654
 	deadLevels := sets.NewString(deadAPILevels...)
 	knownLevels := sets.NewString(knownAPILevels...)
26c990fe
 	for i, apiLevel := range apiLevels {
398ef03e
 		idxPath := fldPath.Index(i)
26c990fe
 		if deadLevels.Has(apiLevel) {
398ef03e
 			validationResults.AddWarnings(field.Invalid(idxPath, apiLevel, "unsupported level"))
26c990fe
 		}
 		if !knownLevels.Has(apiLevel) {
398ef03e
 			validationResults.AddWarnings(field.Invalid(idxPath, apiLevel, "unknown level"))
26c990fe
 		}
 	}
 
 	return validationResults
53c7aa34
 }
 
398ef03e
 func ValidateEtcdStorageConfig(config api.EtcdStorageConfig, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
6a7364d3
 
1eb472bb
 	allErrs = append(allErrs, ValidateStorageVersionLevel(
 		config.KubernetesStorageVersion,
 		api.KnownKubernetesStorageVersionLevels,
 		api.DeadKubernetesStorageVersionLevels,
398ef03e
 		fldPath.Child("kubernetesStorageVersion"))...)
1eb472bb
 	allErrs = append(allErrs, ValidateStorageVersionLevel(
 		config.OpenShiftStorageVersion,
 		api.KnownOpenShiftStorageVersionLevels,
 		api.DeadOpenShiftStorageVersionLevels,
398ef03e
 		fldPath.Child("openShiftStorageVersion"))...)
6a7364d3
 
4580178f
 	if strings.ContainsRune(config.KubernetesStoragePrefix, '%') {
398ef03e
 		allErrs = append(allErrs, field.Invalid(fldPath.Child("kubernetesStoragePrefix"), config.KubernetesStoragePrefix, "the '%' character may not be used in etcd path prefixes"))
4580178f
 	}
 	if strings.ContainsRune(config.OpenShiftStoragePrefix, '%') {
398ef03e
 		allErrs = append(allErrs, field.Invalid(fldPath.Child("openShiftStoragePrefix"), config.OpenShiftStoragePrefix, "the '%' character may not be used in etcd path prefixes"))
4580178f
 	}
 
6a7364d3
 	return allErrs
 }
 
398ef03e
 func ValidateStorageVersionLevel(level string, knownAPILevels, deadAPILevels []string, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
1eb472bb
 
 	if len(level) == 0 {
43ba781b
 		allErrs = append(allErrs, field.Required(fldPath, ""))
1eb472bb
 		return allErrs
 	}
 	supportedLevels := sets.NewString(knownAPILevels...)
 	supportedLevels.Delete(deadAPILevels...)
 	if !supportedLevels.Has(level) {
398ef03e
 		allErrs = append(allErrs, field.NotSupported(fldPath, level, supportedLevels.List()))
1eb472bb
 	}
 
 	return allErrs
 }
 
398ef03e
 func ValidateServiceAccountConfig(config api.ServiceAccountConfig, builtInKubernetes bool, fldPath *field.Path) ValidationResults {
89364aa9
 	validationResults := ValidationResults{}
878d37a8
 
3dd75654
 	managedNames := sets.NewString(config.ManagedNames...)
398ef03e
 	managedNamesPath := fldPath.Child("managedNames")
878d37a8
 	if !managedNames.Has(bootstrappolicy.BuilderServiceAccountName) {
398ef03e
 		validationResults.AddWarnings(field.Invalid(managedNamesPath, "", fmt.Sprintf("missing %q, which will require manual creation in each namespace before builds can run", bootstrappolicy.BuilderServiceAccountName)))
89364aa9
 	}
 	if !managedNames.Has(bootstrappolicy.DeployerServiceAccountName) {
398ef03e
 		validationResults.AddWarnings(field.Invalid(managedNamesPath, "", fmt.Sprintf("missing %q, which will require manual creation in each namespace before deployments can run", bootstrappolicy.DeployerServiceAccountName)))
878d37a8
 	}
 	if builtInKubernetes && !managedNames.Has(bootstrappolicy.DefaultServiceAccountName) {
398ef03e
 		validationResults.AddWarnings(field.Invalid(managedNamesPath, "", fmt.Sprintf("missing %q, which will prevent creation of pods that do not specify a valid service account", bootstrappolicy.DefaultServiceAccountName)))
878d37a8
 	}
 
 	for i, name := range config.ManagedNames {
8201efbe
 		if reasons := kvalidation.ValidateServiceAccountName(name, false); len(reasons) != 0 {
 			validationResults.AddErrors(field.Invalid(managedNamesPath.Index(i), name, strings.Join(reasons, ", ")))
878d37a8
 		}
 	}
 
 	if len(config.PrivateKeyFile) > 0 {
398ef03e
 		privateKeyFilePath := fldPath.Child("privateKeyFile")
 		if fileErrs := ValidateFile(config.PrivateKeyFile, privateKeyFilePath); len(fileErrs) > 0 {
89364aa9
 			validationResults.AddErrors(fileErrs...)
878d37a8
 		} else if privateKey, err := serviceaccount.ReadPrivateKey(config.PrivateKeyFile); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(privateKeyFilePath, config.PrivateKeyFile, err.Error()))
878d37a8
 		} else if err := privateKey.Validate(); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(privateKeyFilePath, config.PrivateKeyFile, err.Error()))
878d37a8
 		}
 	} else if builtInKubernetes {
398ef03e
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("privateKeyFile"), "", "no service account tokens will be generated, which could prevent builds and deployments from working"))
878d37a8
 	}
 
 	if len(config.PublicKeyFiles) == 0 {
398ef03e
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("publicKeyFiles"), "", "no service account tokens will be accepted by the API, which will prevent builds and deployments from working"))
878d37a8
 	}
 	for i, publicKeyFile := range config.PublicKeyFiles {
398ef03e
 		idxPath := fldPath.Child("publicKeyFiles").Index(i)
 		if fileErrs := ValidateFile(publicKeyFile, idxPath); len(fileErrs) > 0 {
89364aa9
 			validationResults.AddErrors(fileErrs...)
878d37a8
 		} else if _, err := serviceaccount.ReadPublicKey(publicKeyFile); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(idxPath, publicKeyFile, err.Error()))
878d37a8
 		}
 	}
 
5b4ee6a4
 	if len(config.MasterCA) > 0 {
398ef03e
 		validationResults.AddErrors(ValidateFile(config.MasterCA, fldPath.Child("masterCA"))...)
5b4ee6a4
 	} else if builtInKubernetes {
398ef03e
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("masterCA"), "", "master CA information will not be automatically injected into pods, which will prevent verification of the API server from inside a pod"))
5b4ee6a4
 	}
 
89364aa9
 	return validationResults
878d37a8
 }
 
398ef03e
 func ValidateAssetConfig(config *api.AssetConfig, fldPath *field.Path) ValidationResults {
81b520f2
 	validationResults := ValidationResults{}
53c7aa34
 
398ef03e
 	validationResults.Append(ValidateHTTPServingInfo(config.ServingInfo, fldPath.Child("servingInfo")))
53c7aa34
 
0fd35257
 	if len(config.LogoutURL) > 0 {
398ef03e
 		_, urlErrs := ValidateURL(config.LogoutURL, fldPath.Child("logoutURL"))
0fd35257
 		if len(urlErrs) > 0 {
81b520f2
 			validationResults.AddErrors(urlErrs...)
0fd35257
 		}
 	}
 
398ef03e
 	urlObj, urlErrs := ValidateURL(config.PublicURL, fldPath.Child("publicURL"))
53c7aa34
 	if len(urlErrs) > 0 {
81b520f2
 		validationResults.AddErrors(urlErrs...)
53c7aa34
 	}
 	if urlObj != nil {
 		if !strings.HasSuffix(urlObj.Path, "/") {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("publicURL"), config.PublicURL, "must have a trailing slash in path"))
53c7aa34
 		}
 	}
 
398ef03e
 	if _, urlErrs := ValidateURL(config.MasterPublicURL, fldPath.Child("masterPublicURL")); len(urlErrs) > 0 {
81b520f2
 		validationResults.AddErrors(urlErrs...)
f53feeed
 	}
 
8fbf136d
 	if len(config.LoggingPublicURL) > 0 {
398ef03e
 		if _, loggingURLErrs := ValidateSecureURL(config.LoggingPublicURL, fldPath.Child("loggingPublicURL")); len(loggingURLErrs) > 0 {
81b520f2
 			validationResults.AddErrors(loggingURLErrs...)
8fbf136d
 		}
363e9a33
 	} else {
398ef03e
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("loggingPublicURL"), "", "required to view aggregated container logs in the console"))
8fbf136d
 	}
 
 	if len(config.MetricsPublicURL) > 0 {
398ef03e
 		if _, metricsURLErrs := ValidateSecureURL(config.MetricsPublicURL, fldPath.Child("metricsPublicURL")); len(metricsURLErrs) > 0 {
81b520f2
 			validationResults.AddErrors(metricsURLErrs...)
8fbf136d
 		}
363e9a33
 	} else {
398ef03e
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("metricsPublicURL"), "", "required to view cluster metrics in the console"))
8fbf136d
 	}
 
a8dd903f
 	for i, scriptFile := range config.ExtensionScripts {
398ef03e
 		validationResults.AddErrors(ValidateFile(scriptFile, fldPath.Child("extensionScripts").Index(i))...)
a8dd903f
 	}
 
 	for i, stylesheetFile := range config.ExtensionStylesheets {
398ef03e
 		validationResults.AddErrors(ValidateFile(stylesheetFile, fldPath.Child("extensionStylesheets").Index(i))...)
a8dd903f
 	}
 
 	nameTaken := map[string]bool{}
 	for i, extConfig := range config.Extensions {
398ef03e
 		idxPath := fldPath.Child("extensions").Index(i)
 		extConfigErrors := ValidateAssetExtensionsConfig(extConfig, idxPath)
81b520f2
 		validationResults.AddErrors(extConfigErrors...)
a8dd903f
 		if nameTaken[extConfig.Name] {
398ef03e
 			dupError := field.Invalid(idxPath.Child("name"), extConfig.Name, "duplicate extension name")
81b520f2
 			validationResults.AddErrors(dupError)
a8dd903f
 		} else {
 			nameTaken[extConfig.Name] = true
 		}
 	}
 
81b520f2
 	return validationResults
a8dd903f
 }
 
 var extNameExp = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
 
398ef03e
 func ValidateAssetExtensionsConfig(extConfig api.AssetExtensionsConfig, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
a8dd903f
 
398ef03e
 	allErrs = append(allErrs, ValidateDir(extConfig.SourceDirectory, fldPath.Child("sourceDirectory"))...)
a8dd903f
 
 	if len(extConfig.Name) == 0 {
43ba781b
 		allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
a8dd903f
 	} else if !extNameExp.MatchString(extConfig.Name) {
398ef03e
 		allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), extConfig.Name, fmt.Sprintf("does not match %v", extNameExp)))
a8dd903f
 	}
 
53c7aa34
 	return allErrs
 }
 
398ef03e
 func ValidateImageConfig(config api.ImageConfig, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
53c7aa34
 
 	if len(config.Format) == 0 {
43ba781b
 		allErrs = append(allErrs, field.Required(fldPath.Child("format"), ""))
53c7aa34
 	}
 
 	return allErrs
 }
 
0e9494df
 func ValidateImagePolicyConfig(config api.ImagePolicyConfig, fldPath *field.Path) field.ErrorList {
 	errs := field.ErrorList{}
 
 	if config.MaxImagesBulkImportedPerRepository == 0 || config.MaxImagesBulkImportedPerRepository < -1 {
 		errs = append(errs, field.Invalid(fldPath.Child("maxImagesBulkImportedPerRepository"), config.MaxImagesBulkImportedPerRepository, "must be a positive integer or -1"))
 	}
 	if config.ScheduledImageImportMinimumIntervalSeconds <= 0 {
 		errs = append(errs, field.Invalid(fldPath.Child("scheduledImageImportMinimumIntervalSeconds"), config.ScheduledImageImportMinimumIntervalSeconds, "must be a positive integer"))
 	}
 	if config.MaxScheduledImageImportsPerMinute == 0 || config.MaxScheduledImageImportsPerMinute < -1 {
 		errs = append(errs, field.Invalid(fldPath.Child("maxScheduledImageImportsPerMinute"), config.MaxScheduledImageImportsPerMinute, "must be a positive integer or -1"))
 	}
 	return errs
 }
 
398ef03e
 func ValidateKubeletConnectionInfo(config api.KubeletConnectionInfo, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
0e9494df
 
53c7aa34
 	if config.Port == 0 {
43ba781b
 		allErrs = append(allErrs, field.Required(fldPath.Child("port"), ""))
53c7aa34
 	}
 
 	if len(config.CA) > 0 {
398ef03e
 		allErrs = append(allErrs, ValidateFile(config.CA, fldPath.Child("ca"))...)
53c7aa34
 	}
398ef03e
 	allErrs = append(allErrs, ValidateCertInfo(config.ClientCert, false, fldPath)...)
53c7aa34
 
 	return allErrs
 }
 
398ef03e
 func ValidateKubernetesMasterConfig(config *api.KubernetesMasterConfig, fldPath *field.Path) ValidationResults {
26c990fe
 	validationResults := ValidationResults{}
53c7aa34
 
 	if len(config.MasterIP) > 0 {
398ef03e
 		validationResults.AddErrors(ValidateSpecifiedIP(config.MasterIP, fldPath.Child("masterIP"))...)
53c7aa34
 	}
 
5cbeeacd
 	if config.MasterCount == 0 || config.MasterCount < -1 {
398ef03e
 		validationResults.AddErrors(field.Invalid(fldPath.Child("masterCount"), config.MasterCount, "must be a positive integer or -1"))
52723316
 	}
 
398ef03e
 	validationResults.AddErrors(ValidateCertInfo(config.ProxyClientInfo, false, fldPath.Child("proxyClientInfo"))...)
e4a83a7a
 	if len(config.ProxyClientInfo.CertFile) == 0 && len(config.ProxyClientInfo.KeyFile) == 0 {
398ef03e
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("proxyClientInfo"), "", "if no client certificate is specified, TLS pods and services cannot validate requests came from the proxy"))
e4a83a7a
 	}
 
53c7aa34
 	if len(config.ServicesSubnet) > 0 {
 		if _, _, err := net.ParseCIDR(strings.TrimSpace(config.ServicesSubnet)); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("servicesSubnet"), config.ServicesSubnet, "must be a valid CIDR notation IP range (e.g. 172.30.0.0/16)"))
53c7aa34
 		}
 	}
 
e00edaae
 	if len(config.ServicesNodePortRange) > 0 {
43ba781b
 		if _, err := knet.ParsePortRange(strings.TrimSpace(config.ServicesNodePortRange)); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("servicesNodePortRange"), config.ServicesNodePortRange, "must be a valid port range (e.g. 30000-32000)"))
e00edaae
 		}
 	}
 
53c7aa34
 	if len(config.SchedulerConfigFile) > 0 {
398ef03e
 		validationResults.AddErrors(ValidateFile(config.SchedulerConfigFile, fldPath.Child("schedulerConfigFile"))...)
53c7aa34
 	}
 
57894264
 	for i, nodeName := range config.StaticNodeNames {
 		if len(nodeName) == 0 {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("staticNodeName").Index(i), nodeName, "may not be empty"))
55ac6a57
 		} else {
398ef03e
 			validationResults.AddWarnings(field.Invalid(fldPath.Child("staticNodeName").Index(i), nodeName, "static nodes are not supported"))
57894264
 		}
 	}
 
83e7080d
 	if len(config.PodEvictionTimeout) > 0 {
 		if _, err := time.ParseDuration(config.PodEvictionTimeout); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("podEvictionTimeout"), config.PodEvictionTimeout, "must be a valid time duration string (e.g. '300ms' or '2m30s'). Valid time units are 'ns', 'us', 'ms', 's', 'm', 'h'"))
83e7080d
 		}
 	}
 
3ceb818d
 	for group, versions := range config.DisabledAPIGroupVersions {
398ef03e
 		keyPath := fldPath.Child("disabledAPIGroupVersions").Key(group)
3ceb818d
 		if !api.KnownKubeAPIGroups.Has(group) {
398ef03e
 			validationResults.AddWarnings(field.NotSupported(keyPath, group, api.KnownKubeAPIGroups.List()))
3ceb818d
 			continue
 		}
 
 		allowedVersions := sets.NewString(api.KubeAPIGroupsToAllowedVersions[group]...)
 		for i, version := range versions {
 			if version == "*" {
 				continue
 			}
 
 			if !allowedVersions.Has(version) {
398ef03e
 				validationResults.AddWarnings(field.NotSupported(keyPath.Index(i), version, allowedVersions.List()))
3ceb818d
 			}
 		}
 	}
26c990fe
 
00b6e181
 	if config.AdmissionConfig.PluginConfig != nil {
398ef03e
 		validationResults.AddErrors(ValidateAdmissionPluginConfig(config.AdmissionConfig.PluginConfig, fldPath.Child("admissionConfig", "pluginConfig"))...)
00b6e181
 	}
3a98b97f
 	if len(config.AdmissionConfig.PluginOrderOverride) != 0 {
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("admissionConfig", "pluginOrderOverride"), config.AdmissionConfig.PluginOrderOverride, "specified admission ordering is being phased out.  Convert to DefaultAdmissionConfig in admissionConfig.pluginConfig."))
 	}
00b6e181
 
3a98b97f
 	validationResults.Append(ValidateAPIServerExtendedArguments(config.APIServerArguments, fldPath.Child("apiServerArguments")))
398ef03e
 	validationResults.AddErrors(ValidateControllerExtendedArguments(config.ControllerArguments, fldPath.Child("controllerArguments"))...)
0b833843
 
26c990fe
 	return validationResults
53c7aa34
 }
 
398ef03e
 func ValidatePolicyConfig(config api.PolicyConfig, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
53c7aa34
 
398ef03e
 	allErrs = append(allErrs, ValidateFile(config.BootstrapPolicyFile, fldPath.Child("bootstrapPolicyFile"))...)
 	allErrs = append(allErrs, ValidateNamespace(config.OpenShiftSharedResourcesNamespace, fldPath.Child("openShiftSharedResourcesNamespace"))...)
 	allErrs = append(allErrs, ValidateNamespace(config.OpenShiftInfrastructureNamespace, fldPath.Child("openShiftInfrastructureNamespace"))...)
53c7aa34
 
049217ce
 	for i, matchingRule := range config.UserAgentMatchingConfig.DeniedClients {
 		_, err := regexp.Compile(matchingRule.Regex)
 		if err != nil {
 			allErrs = append(allErrs, field.Invalid(fldPath.Child("userAgentMatchingConfig", "deniedClients").Index(i), matchingRule.Regex, err.Error()))
 		}
 	}
 	for i, matchingRule := range config.UserAgentMatchingConfig.RequiredClients {
 		_, err := regexp.Compile(matchingRule.Regex)
 		if err != nil {
 			allErrs = append(allErrs, field.Invalid(fldPath.Child("userAgentMatchingConfig", "requiredClients").Index(i), matchingRule.Regex, err.Error()))
 		}
 	}
 
53c7aa34
 	return allErrs
 }
384f11fa
 
398ef03e
 func ValidateProjectConfig(config api.ProjectConfig, fldPath *field.Path) ValidationResults {
b035b8e9
 	validationResults := ValidationResults{}
384f11fa
 
970f2c0f
 	if _, _, err := api.ParseNamespaceAndName(config.ProjectRequestTemplate); err != nil {
398ef03e
 		validationResults.AddErrors(field.Invalid(fldPath.Child("projectRequestTemplate"), config.ProjectRequestTemplate, "must be in the form: namespace/templateName"))
2baf326a
 	}
 
97f43232
 	if len(config.DefaultNodeSelector) > 0 {
 		_, err := labelselector.Parse(config.DefaultNodeSelector)
 		if err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(fldPath.Child("defaultNodeSelector"), config.DefaultNodeSelector, "must be a valid label selector"))
97f43232
 		}
 	}
 
ec5a5ac6
 	if alloc := config.SecurityAllocator; alloc != nil {
398ef03e
 		securityAllocatorPath := fldPath.Child("securityAllocator")
ec5a5ac6
 		if _, err := uid.ParseRange(alloc.UIDAllocatorRange); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(securityAllocatorPath.Child("uidAllocatorRange"), alloc.UIDAllocatorRange, err.Error()))
ec5a5ac6
 		}
 		if _, err := mcs.ParseRange(alloc.MCSAllocatorRange); err != nil {
398ef03e
 			validationResults.AddErrors(field.Invalid(securityAllocatorPath.Child("mcsAllocatorRange"), alloc.MCSAllocatorRange, err.Error()))
ec5a5ac6
 		}
 		if alloc.MCSLabelsPerProject <= 0 {
398ef03e
 			validationResults.AddErrors(field.Invalid(securityAllocatorPath.Child("mcsLabelsPerProject"), alloc.MCSLabelsPerProject, "must be a positive integer"))
ec5a5ac6
 		}
b035b8e9
 
 	} else {
398ef03e
 		validationResults.AddWarnings(field.Invalid(fldPath.Child("securityAllocator"), "null", "allocation of UIDs and MCS labels to a project must be done manually"))
b035b8e9
 
ec5a5ac6
 	}
b035b8e9
 
 	return validationResults
384f11fa
 }
321d1032
 
398ef03e
 func ValidateRoutingConfig(config api.RoutingConfig, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
321d1032
 
 	if len(config.Subdomain) == 0 {
43ba781b
 		allErrs = append(allErrs, field.Required(fldPath.Child("subdomain"), ""))
8201efbe
 	} else if len(kuval.IsDNS1123Subdomain(config.Subdomain)) != 0 {
398ef03e
 		allErrs = append(allErrs, field.Invalid(fldPath.Child("subdomain"), config.Subdomain, "must be a valid subdomain"))
321d1032
 	}
 
 	return allErrs
 }
0b833843
 
3a98b97f
 func ValidateAPIServerExtendedArguments(config api.ExtendedArguments, fldPath *field.Path) ValidationResults {
 	validationResults := ValidationResults{}
 
 	validationResults.AddErrors(ValidateExtendedArguments(config, apiserveroptions.NewAPIServer().AddFlags, fldPath)...)
 
 	if len(config["admission-control"]) > 0 {
 		validationResults.AddWarnings(field.Invalid(fldPath.Key("admission-control"), config["admission-control"], "specified admission ordering is being phased out.  Convert to DefaultAdmissionConfig in admissionConfig.pluginConfig."))
 	}
 	if len(config["admission-control-config-file"]) > 0 {
 		validationResults.AddWarnings(field.Invalid(fldPath.Key("admission-control-config-file"), config["admission-control-config-file"], "specify a single admission control config file is being phased out.  Convert to admissionConfig.pluginConfig, one file per plugin."))
 	}
 
 	return validationResults
0b833843
 }
 
398ef03e
 func ValidateControllerExtendedArguments(config api.ExtendedArguments, fldPath *field.Path) field.ErrorList {
7aabe6b9
 	return ValidateExtendedArguments(config, controlleroptions.NewCMServer().AddFlags, fldPath)
0b833843
 }
00b6e181
 
398ef03e
 func ValidateAdmissionPluginConfig(pluginConfig map[string]api.AdmissionPluginConfig, fieldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
00b6e181
 	for name, config := range pluginConfig {
43ba781b
 		if len(config.Location) > 0 && config.Configuration != nil {
398ef03e
 			allErrs = append(allErrs, field.Invalid(fieldPath.Key(name), "", "cannot specify both location and embedded config"))
00b6e181
 		}
43ba781b
 		if len(config.Location) == 0 && config.Configuration == nil {
398ef03e
 			allErrs = append(allErrs, field.Invalid(fieldPath.Key(name), "", "must specify either a location or an embedded config"))
00b6e181
 		}
 	}
 	return allErrs
 
 }
3a98b97f
 
 func ValidateAdmissionPluginConfigConflicts(masterConfig *api.MasterConfig) ValidationResults {
 	validationResults := ValidationResults{}
 
 	if masterConfig.KubernetesMasterConfig != nil {
 		// check for collisions between openshift and kube plugin config
 		for pluginName, kubeConfig := range masterConfig.KubernetesMasterConfig.AdmissionConfig.PluginConfig {
 			if openshiftConfig, exists := masterConfig.AdmissionConfig.PluginConfig[pluginName]; exists && !reflect.DeepEqual(kubeConfig, openshiftConfig) {
 				validationResults.AddWarnings(field.Invalid(field.NewPath("kubernetesMasterConfig", "admissionConfig", "pluginConfig").Key(pluginName), masterConfig.AdmissionConfig.PluginConfig[pluginName], "conflicts with kubernetesMasterConfig.admissionConfig.pluginConfig.  Separate admission chains are being phased out.  Convert to admissionConfig.pluginConfig."))
 			}
 		}
 	}
 
 	return validationResults
 }
346982a0
 
ee401ccd
 func ValidateIngressIPNetworkCIDR(config *api.MasterConfig, fldPath *field.Path) (errors field.ErrorList) {
346982a0
 	cidr := config.NetworkConfig.IngressIPNetworkCIDR
 	if len(cidr) == 0 {
ee401ccd
 		return
346982a0
 	}
 
 	addError := func(errMessage string) {
 		errors = append(errors, field.Invalid(fldPath, cidr, errMessage))
 	}
 
ee401ccd
 	_, ipNet, err := net.ParseCIDR(cidr)
 	if err != nil {
f980b4c5
 		addError(fmt.Sprintf("must be a valid CIDR notation IP range (e.g. %s)", api.DefaultIngressIPNetworkCIDR))
ee401ccd
 		return
 	}
 
346982a0
 	// TODO Detect cloud provider when not using built-in kubernetes
 	kubeConfig := config.KubernetesMasterConfig
 	noCloudProvider := kubeConfig != nil && (len(kubeConfig.ControllerArguments["cloud-provider"]) == 0 || kubeConfig.ControllerArguments["cloud-provider"][0] == "")
 
 	if noCloudProvider {
ee401ccd
 		if api.CIDRsOverlap(cidr, config.NetworkConfig.ClusterNetworkCIDR) {
 			addError("conflicts with cluster network CIDR")
346982a0
 		}
ee401ccd
 		if api.CIDRsOverlap(cidr, config.NetworkConfig.ServiceNetworkCIDR) {
 			addError("conflicts with service network CIDR")
 		}
 	} else if !ipNet.IP.IsUnspecified() {
346982a0
 		addError("should not be provided when a cloud-provider is enabled")
 	}
 
ee401ccd
 	return
346982a0
 }