package validation
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"strings"
"unicode"
"unicode/utf8"
"github.com/spf13/pflag"
kvalidation "k8s.io/kubernetes/pkg/api/validation"
"k8s.io/kubernetes/pkg/util/sets"
utilvalidation "k8s.io/kubernetes/pkg/util/validation"
"k8s.io/kubernetes/pkg/util/validation/field"
"github.com/openshift/origin/pkg/cmd/server/api"
cmdutil "github.com/openshift/origin/pkg/cmd/util"
cmdflags "github.com/openshift/origin/pkg/cmd/util/flags"
)
func ValidateStringSource(s api.StringSource, fieldPath *field.Path) ValidationResults {
validationResults := ValidationResults{}
methods := 0
if len(s.Value) > 0 {
methods++
}
if len(s.File) > 0 {
methods++
fileErrors := ValidateFile(s.File, fieldPath.Child("file"))
validationResults.AddErrors(fileErrors...)
// If the file was otherwise ok, and its value will be used verbatim, warn about trailing whitespace
if len(fileErrors) == 0 && len(s.KeyFile) == 0 {
if data, err := ioutil.ReadFile(s.File); err != nil {
validationResults.AddErrors(field.Invalid(fieldPath.Child("file"), s.File, fmt.Sprintf("could not read file: %v", err)))
} else if len(data) > 0 {
r, _ := utf8.DecodeLastRune(data)
if unicode.IsSpace(r) {
validationResults.AddWarnings(field.Invalid(fieldPath.Child("file"), s.File, "contains trailing whitespace which will be included in the value"))
}
}
}
}
if len(s.Env) > 0 {
methods++
}
if methods > 1 {
validationResults.AddErrors(field.Invalid(fieldPath, "", "only one of value, file, and env can be specified"))
}
if len(s.KeyFile) > 0 {
validationResults.AddErrors(ValidateFile(s.KeyFile, fieldPath.Child("keyFile"))...)
}
return validationResults
}
func ValidateHostPort(value string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(value) == 0 {
allErrs = append(allErrs, field.Required(fldPath, ""))
} else if _, _, err := net.SplitHostPort(value); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, value, "must be a host:port"))
}
return allErrs
}
func ValidateCertInfo(certInfo api.CertInfo, required bool, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if required || len(certInfo.CertFile) > 0 || len(certInfo.KeyFile) > 0 {
if len(certInfo.CertFile) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("certFile"), ""))
}
if len(certInfo.KeyFile) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("keyFile"), ""))
}
}
if len(certInfo.CertFile) > 0 {
allErrs = append(allErrs, ValidateFile(certInfo.CertFile, fldPath.Child("certFile"))...)
}
if len(certInfo.KeyFile) > 0 {
allErrs = append(allErrs, ValidateFile(certInfo.KeyFile, fldPath.Child("keyFile"))...)
}
// validate certfile/keyfile load/parse?
return allErrs
}
func ValidateServingInfo(info api.ServingInfo, fldPath *field.Path) ValidationResults {
validationResults := ValidationResults{}
validationResults.AddErrors(ValidateHostPort(info.BindAddress, fldPath.Child("bindAddress"))...)
validationResults.AddErrors(ValidateCertInfo(info.ServerCert, false, fldPath)...)
if len(info.NamedCertificates) > 0 && len(info.ServerCert.CertFile) == 0 {
validationResults.AddErrors(field.Invalid(fldPath.Child("namedCertificates"), "", "a default certificate and key is required in certFile/keyFile in order to use namedCertificates"))
}
validationResults.Append(ValidateNamedCertificates(fldPath.Child("namedCertificates"), info.NamedCertificates))
switch info.BindNetwork {
case "tcp", "tcp4", "tcp6":
default:
validationResults.AddErrors(field.Invalid(fldPath.Child("bindNetwork"), info.BindNetwork, "must be 'tcp', 'tcp4', or 'tcp6'"))
}
if len(info.ServerCert.CertFile) > 0 {
if len(info.ClientCA) > 0 {
validationResults.AddErrors(ValidateFile(info.ClientCA, fldPath.Child("clientCA"))...)
}
} else {
if len(info.ClientCA) > 0 {
validationResults.AddErrors(field.Invalid(fldPath.Child("clientCA"), info.ClientCA, "cannot specify a clientCA without a certFile"))
}
}
return validationResults
}
func ValidateNamedCertificates(fldPath *field.Path, namedCertificates []api.NamedCertificate) ValidationResults {
validationResults := ValidationResults{}
takenNames := sets.NewString()
for i, namedCertificate := range namedCertificates {
idxPath := fldPath.Index(i)
certDNSNames := []string{}
if len(namedCertificate.CertFile) == 0 {
validationResults.AddErrors(field.Required(idxPath.Child("certInfo"), ""))
} else if certInfoErrors := ValidateCertInfo(namedCertificate.CertInfo, false, idxPath); len(certInfoErrors) > 0 {
validationResults.AddErrors(certInfoErrors...)
} else if cert, err := tls.LoadX509KeyPair(namedCertificate.CertFile, namedCertificate.KeyFile); err != nil {
validationResults.AddErrors(field.Invalid(idxPath.Child("certInfo"), namedCertificate.CertInfo, fmt.Sprintf("error loading certificate/key: %v", err)))
} else {
leaf, _ := x509.ParseCertificate(cert.Certificate[0])
certDNSNames = append(certDNSNames, leaf.Subject.CommonName)
certDNSNames = append(certDNSNames, leaf.DNSNames...)
}
if len(namedCertificate.Names) == 0 {
validationResults.AddErrors(field.Required(idxPath.Child("names"), ""))
}
for j, name := range namedCertificate.Names {
jdxPath := idxPath.Child("names").Index(j)
if len(name) == 0 {
validationResults.AddErrors(field.Required(jdxPath, ""))
continue
}
if takenNames.Has(name) {
validationResults.AddErrors(field.Invalid(jdxPath, name, "this name is already used in another named certificate"))
continue
}
// validate names as domain names or *.*.foo.com domain names
validDNSName := true
for _, s := range strings.Split(name, ".") {
if s != "*" && !utilvalidation.IsDNS1123Label(s) {
validDNSName = false
}
}
if !validDNSName {
validationResults.AddErrors(field.Invalid(jdxPath, name, "must be a valid DNS name"))
continue
}
takenNames.Insert(name)
// validate certificate has common name or subject alt names that match
if len(certDNSNames) > 0 {
foundMatch := false
for _, dnsName := range certDNSNames {
if cmdutil.HostnameMatches(dnsName, name) {
foundMatch = true
break
}
// if the cert has a wildcard dnsName, and we've configured a non-wildcard name, see if our specified name will match against the dnsName.
if strings.HasPrefix(dnsName, "*.") && !strings.HasPrefix(name, "*.") && cmdutil.HostnameMatches(name, dnsName) {
foundMatch = true
break
}
}
if !foundMatch {
validationResults.AddWarnings(field.Invalid(jdxPath, name, "the specified certificate does not have a CommonName or DNS subjectAltName that matches this name"))
}
}
}
}
return validationResults
}
func ValidateHTTPServingInfo(info api.HTTPServingInfo, fldPath *field.Path) ValidationResults {
validationResults := ValidationResults{}
validationResults.Append(ValidateServingInfo(info.ServingInfo, fldPath))
if info.MaxRequestsInFlight < 0 {
validationResults.AddErrors(field.Invalid(fldPath.Child("maxRequestsInFlight"), info.MaxRequestsInFlight, "must be zero (no limit) or greater"))
}
if info.RequestTimeoutSeconds < -1 {
validationResults.AddErrors(field.Invalid(fldPath.Child("requestTimeoutSeconds"), info.RequestTimeoutSeconds, "must be -1 (no timeout), 0 (default timeout), or greater"))
}
return validationResults
}
func ValidateDisabledFeatures(disabledFeatures []string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
for i, feature := range disabledFeatures {
if _, isKnown := api.NormalizeOpenShiftFeature(feature); !isKnown {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), disabledFeatures[i], fmt.Sprintf("not one of valid features: %s", strings.Join(api.KnownOpenShiftFeatures, ", "))))
}
}
return allErrs
}
func ValidateKubeConfig(path string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, ValidateFile(path, fldPath)...)
// TODO: load and parse
return allErrs
}
func ValidateRemoteConnectionInfo(remoteConnectionInfo api.RemoteConnectionInfo, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(remoteConnectionInfo.URL) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("url"), ""))
} else {
_, urlErrs := ValidateURL(remoteConnectionInfo.URL, fldPath.Child("url"))
allErrs = append(allErrs, urlErrs...)
}
if len(remoteConnectionInfo.CA) > 0 {
allErrs = append(allErrs, ValidateFile(remoteConnectionInfo.CA, fldPath.Child("ca"))...)
}
allErrs = append(allErrs, ValidateCertInfo(remoteConnectionInfo.ClientCert, false, fldPath)...)
return allErrs
}
func ValidatePodManifestConfig(podManifestConfig *api.PodManifestConfig, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
// the Path can be a file or a directory
allErrs = append(allErrs, ValidateFile(podManifestConfig.Path, fldPath.Child("path"))...)
if podManifestConfig.FileCheckIntervalSeconds < 1 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("fileCheckIntervalSeconds"), podManifestConfig.FileCheckIntervalSeconds, "interval has to be positive"))
}
return allErrs
}
func ValidateSpecifiedIP(ipString string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
ip := net.ParseIP(ipString)
if ip == nil {
allErrs = append(allErrs, field.Invalid(fldPath, ipString, "must be a valid IP"))
} else if ip.IsUnspecified() {
allErrs = append(allErrs, field.Invalid(fldPath, ipString, "cannot be an unspecified IP"))
}
return allErrs
}
func ValidateSecureURL(urlString string, fldPath *field.Path) (*url.URL, field.ErrorList) {
url, urlErrs := ValidateURL(urlString, fldPath)
if len(urlErrs) == 0 && url.Scheme != "https" {
urlErrs = append(urlErrs, field.Invalid(fldPath, urlString, "must use https scheme"))
}
return url, urlErrs
}
func ValidateURL(urlString string, fldPath *field.Path) (*url.URL, field.ErrorList) {
allErrs := field.ErrorList{}
urlObj, err := url.Parse(urlString)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, urlString, "must be a valid URL"))
return nil, allErrs
}
if len(urlObj.Scheme) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath, urlString, "must contain a scheme (e.g. https://)"))
}
if len(urlObj.Host) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath, urlString, "must contain a host"))
}
return urlObj, allErrs
}
func ValidateNamespace(namespace string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(namespace) == 0 {
allErrs = append(allErrs, field.Required(fldPath, ""))
} else if ok, _ := kvalidation.ValidateNamespaceName(namespace, false); !ok {
allErrs = append(allErrs, field.Invalid(fldPath, namespace, "must be a valid namespace"))
}
return allErrs
}
func ValidateFile(path string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(path) == 0 {
allErrs = append(allErrs, field.Required(fldPath, ""))
} else if _, err := os.Stat(path); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, path, "could not read file"))
}
return allErrs
}
func ValidateDir(path string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(path) == 0 {
allErrs = append(allErrs, field.Required(fldPath, ""))
} else {
fileInfo, err := os.Stat(path)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, path, "could not read info"))
} else if !fileInfo.IsDir() {
allErrs = append(allErrs, field.Invalid(fldPath, path, "not a directory"))
}
}
return allErrs
}
func ValidateExtendedArguments(config api.ExtendedArguments, flagFunc func(*pflag.FlagSet), fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
// check extended arguments for errors
for _, err := range cmdflags.Resolve(config, flagFunc) {
switch t := err.(type) {
case *field.Error:
allErrs = append(allErrs, t)
default:
allErrs = append(allErrs, field.Invalid(fldPath.Child("????"), config, err.Error()))
}
}
return allErrs
}