| // Copyright Istio 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 ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "net" |
| "net/http" |
| "net/url" |
| "path" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| import ( |
| udpaa "github.com/cncf/xds/go/udpa/annotations" |
| "github.com/envoyproxy/go-control-plane/pkg/wellknown" |
| "github.com/hashicorp/go-multierror" |
| "github.com/lestrrat-go/jwx/jwk" |
| "google.golang.org/protobuf/proto" |
| "google.golang.org/protobuf/reflect/protoreflect" |
| "google.golang.org/protobuf/reflect/protoregistry" |
| "google.golang.org/protobuf/types/descriptorpb" |
| any "google.golang.org/protobuf/types/known/anypb" |
| "google.golang.org/protobuf/types/known/durationpb" |
| "istio.io/api/annotation" |
| extensions "istio.io/api/extensions/v1alpha1" |
| meshconfig "istio.io/api/mesh/v1alpha1" |
| networking "istio.io/api/networking/v1alpha3" |
| networkingv1beta1 "istio.io/api/networking/v1beta1" |
| security_beta "istio.io/api/security/v1beta1" |
| telemetry "istio.io/api/telemetry/v1alpha1" |
| type_beta "istio.io/api/type/v1beta1" |
| "istio.io/pkg/log" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/features" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/util/constant" |
| "github.com/apache/dubbo-go-pixiu/pkg/config" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/constants" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/gateway" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/host" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/labels" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/protocol" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/security" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/visibility" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/xds" |
| "github.com/apache/dubbo-go-pixiu/pkg/kube/apimirror" |
| "github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal" |
| "github.com/apache/dubbo-go-pixiu/pkg/util/sets" |
| ) |
| |
| // Constants for duration fields |
| const ( |
| // nolint: revive |
| connectTimeoutMax = time.Second * 30 |
| // nolint: revive |
| connectTimeoutMin = time.Millisecond |
| |
| drainTimeMax = time.Hour |
| parentShutdownTimeMax = time.Hour |
| |
| // UnixAddressPrefix is the prefix used to indicate an address is for a Unix Domain socket. It is used in |
| // ServiceEntry.Endpoint.Address message. |
| UnixAddressPrefix = "unix://" |
| |
| matchExact = "exact:" |
| matchPrefix = "prefix:" |
| ) |
| |
| const ( |
| regionIndex int = iota |
| zoneIndex |
| subZoneIndex |
| ) |
| |
| var ( |
| // envoy supported retry on header values |
| supportedRetryOnPolicies = map[string]bool{ |
| // 'x-envoy-retry-on' supported policies: |
| // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter.html#x-envoy-retry-on |
| "5xx": true, |
| "gateway-error": true, |
| "reset": true, |
| "connect-failure": true, |
| "retriable-4xx": true, |
| "refused-stream": true, |
| "retriable-status-codes": true, |
| "retriable-headers": true, |
| "envoy-ratelimited": true, |
| |
| // 'x-envoy-retry-grpc-on' supported policies: |
| // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#x-envoy-retry-grpc-on |
| "cancelled": true, |
| "deadline-exceeded": true, |
| "internal": true, |
| "resource-exhausted": true, |
| "unavailable": true, |
| } |
| |
| // golang supported methods: https://golang.org/src/net/http/method.go |
| supportedMethods = map[string]bool{ |
| http.MethodGet: true, |
| http.MethodHead: true, |
| http.MethodPost: true, |
| http.MethodPut: true, |
| http.MethodPatch: true, |
| http.MethodDelete: true, |
| http.MethodConnect: true, |
| http.MethodOptions: true, |
| http.MethodTrace: true, |
| } |
| |
| scope = log.RegisterScope("validation", "CRD validation debugging", 0) |
| |
| // EmptyValidate is a Validate that does nothing and returns no error. |
| EmptyValidate = registerValidateFunc("EmptyValidate", |
| func(config.Config) (Warning, error) { |
| return nil, nil |
| }) |
| |
| validateFuncs = make(map[string]ValidateFunc) |
| ) |
| |
| type Warning error |
| |
| // Validation holds errors and warnings. They can be joined with additional errors by called appendValidation |
| type Validation struct { |
| Err error |
| Warning Warning |
| } |
| |
| type AnalysisAwareError struct { |
| Type string |
| Msg string |
| Parameters []interface{} |
| } |
| |
| // OverlappingMatchValidationForHTTPRoute holds necessary information from virtualservice |
| // to do such overlapping match validation |
| type OverlappingMatchValidationForHTTPRoute struct { |
| RouteStr string |
| MatchStr string |
| Prefix string |
| MatchPort uint32 |
| MatchMethod string |
| MatchAuthority string |
| MatchHeaders map[string]string |
| MatchQueryParams map[string]string |
| MatchNonHeaders map[string]string |
| } |
| |
| var _ error = Validation{} |
| |
| // WrapError turns an error into a Validation |
| func WrapError(e error) Validation { |
| return Validation{Err: e} |
| } |
| |
| // WrapWarning turns an error into a Validation as a warning |
| func WrapWarning(e error) Validation { |
| return Validation{Warning: e} |
| } |
| |
| // Warningf formats according to a format specifier and returns the string as a |
| // value that satisfies error. Like Errorf, but for warnings. |
| func Warningf(format string, a ...interface{}) Validation { |
| return WrapWarning(fmt.Errorf(format, a...)) |
| } |
| |
| func (v Validation) Unwrap() (Warning, error) { |
| return v.Warning, v.Err |
| } |
| |
| func (v Validation) Error() string { |
| if v.Err == nil { |
| return "" |
| } |
| return v.Err.Error() |
| } |
| |
| // ValidateFunc defines a validation func for an API proto. |
| type ValidateFunc func(config config.Config) (Warning, error) |
| |
| // IsValidateFunc indicates whether there is a validation function with the given name. |
| func IsValidateFunc(name string) bool { |
| return GetValidateFunc(name) != nil |
| } |
| |
| // GetValidateFunc returns the validation function with the given name, or null if it does not exist. |
| func GetValidateFunc(name string) ValidateFunc { |
| return validateFuncs[name] |
| } |
| |
| func registerValidateFunc(name string, f ValidateFunc) ValidateFunc { |
| // Wrap the original validate function with an extra validate function for the annotation "istio.io/dry-run". |
| validate := validateAnnotationDryRun(f) |
| validateFuncs[name] = validate |
| return validate |
| } |
| |
| func validateAnnotationDryRun(f ValidateFunc) ValidateFunc { |
| return func(config config.Config) (Warning, error) { |
| _, isAuthz := config.Spec.(*security_beta.AuthorizationPolicy) |
| // Only the AuthorizationPolicy supports the annotation "istio.io/dry-run". |
| if err := checkDryRunAnnotation(config, isAuthz); err != nil { |
| return nil, err |
| } |
| return f(config) |
| } |
| } |
| |
| func checkDryRunAnnotation(cfg config.Config, allowed bool) error { |
| if val, found := cfg.Annotations[annotation.IoIstioDryRun.Name]; found { |
| if !allowed { |
| return fmt.Errorf("%s/%s has unsupported annotation %s, please remove the annotation", cfg.Namespace, cfg.Name, annotation.IoIstioDryRun.Name) |
| } |
| if spec, ok := cfg.Spec.(*security_beta.AuthorizationPolicy); ok { |
| switch spec.Action { |
| case security_beta.AuthorizationPolicy_ALLOW, security_beta.AuthorizationPolicy_DENY: |
| if _, err := strconv.ParseBool(val); err != nil { |
| return fmt.Errorf("%s/%s has annotation %s with invalid value (%s): %v", cfg.Namespace, cfg.Name, annotation.IoIstioDryRun.Name, val, err) |
| } |
| default: |
| return fmt.Errorf("the annotation %s currently only supports action ALLOW/DENY, found action %v in %s/%s", |
| annotation.IoIstioDryRun.Name, spec.Action, cfg.Namespace, cfg.Name) |
| } |
| } |
| } |
| return nil |
| } |
| |
| // ValidatePort checks that the network port is in range |
| func ValidatePort(port int) error { |
| if 1 <= port && port <= 65535 { |
| return nil |
| } |
| return fmt.Errorf("port number %d must be in the range 1..65535", port) |
| } |
| |
| // ValidateFQDN checks a fully-qualified domain name |
| func ValidateFQDN(fqdn string) error { |
| if err := checkDNS1123Preconditions(fqdn); err != nil { |
| return err |
| } |
| return validateDNS1123Labels(fqdn) |
| } |
| |
| // ValidateWildcardDomain checks that a domain is a valid FQDN, but also allows wildcard prefixes. |
| func ValidateWildcardDomain(domain string) error { |
| if err := checkDNS1123Preconditions(domain); err != nil { |
| return err |
| } |
| // We only allow wildcards in the first label; split off the first label (parts[0]) from the rest of the host (parts[1]) |
| parts := strings.SplitN(domain, ".", 2) |
| if !labels.IsWildcardDNS1123Label(parts[0]) { |
| return fmt.Errorf("domain name %q invalid (label %q invalid)", domain, parts[0]) |
| } else if len(parts) > 1 { |
| return validateDNS1123Labels(parts[1]) |
| } |
| return nil |
| } |
| |
| // encapsulates DNS 1123 checks common to both wildcarded hosts and FQDNs |
| func checkDNS1123Preconditions(name string) error { |
| if len(name) > 255 { |
| return fmt.Errorf("domain name %q too long (max 255)", name) |
| } |
| if len(name) == 0 { |
| return fmt.Errorf("empty domain name not allowed") |
| } |
| return nil |
| } |
| |
| func validateDNS1123Labels(domain string) error { |
| parts := strings.Split(domain, ".") |
| topLevelDomain := parts[len(parts)-1] |
| if _, err := strconv.Atoi(topLevelDomain); err == nil { |
| return fmt.Errorf("domain name %q invalid (top level domain %q cannot be all-numeric)", domain, topLevelDomain) |
| } |
| for i, label := range parts { |
| // Allow the last part to be empty, for unambiguous names like `istio.io.` |
| if i == len(parts)-1 && label == "" { |
| return nil |
| } |
| if !labels.IsDNS1123Label(label) { |
| return fmt.Errorf("domain name %q invalid (label %q invalid)", domain, label) |
| } |
| } |
| return nil |
| } |
| |
| // validate the trust domain format |
| func ValidateTrustDomain(domain string) error { |
| if len(domain) == 0 { |
| return fmt.Errorf("empty domain name not allowed") |
| } |
| parts := strings.Split(domain, ".") |
| for i, label := range parts { |
| // Allow the last part to be empty, for unambiguous names like `istio.io.` |
| if i == len(parts)-1 && label == "" { |
| return nil |
| } |
| if !labels.IsDNS1123Label(label) { |
| return fmt.Errorf("trust domain name %q invalid", domain) |
| } |
| } |
| return nil |
| } |
| |
| // ValidateHTTPHeaderName validates a header name |
| func ValidateHTTPHeaderName(name string) error { |
| if name == "" { |
| return fmt.Errorf("header name cannot be empty") |
| } |
| return nil |
| } |
| |
| // ValidateHTTPHeaderWithAuthorityOperationName validates a header name when used to add/set in request. |
| func ValidateHTTPHeaderWithAuthorityOperationName(name string) error { |
| if name == "" { |
| return fmt.Errorf("header name cannot be empty") |
| } |
| // Authority header is validated later |
| if isInternalHeader(name) && !isAuthorityHeader(name) { |
| return fmt.Errorf(`invalid header %q: header cannot have ":" prefix`, name) |
| } |
| return nil |
| } |
| |
| // ValidateHTTPHeaderWithHostOperationName validates a header name when used to destination specific add/set in request. |
| // TODO(https://github.com/envoyproxy/envoy/issues/16775) merge with ValidateHTTPHeaderWithAuthorityOperationName |
| func ValidateHTTPHeaderWithHostOperationName(name string) error { |
| if name == "" { |
| return fmt.Errorf("header name cannot be empty") |
| } |
| // Authority header is validated later |
| if isInternalHeader(name) && !strings.EqualFold(name, "host") { |
| return fmt.Errorf(`invalid header %q: header cannot have ":" prefix`, name) |
| } |
| return nil |
| } |
| |
| // ValidateHTTPHeaderOperationName validates a header name when used to remove from request or modify response. |
| func ValidateHTTPHeaderOperationName(name string) error { |
| if name == "" { |
| return fmt.Errorf("header name cannot be empty") |
| } |
| if strings.EqualFold(name, "host") { |
| return fmt.Errorf(`invalid header %q: cannot set Host header`, name) |
| } |
| if isInternalHeader(name) { |
| return fmt.Errorf(`invalid header %q: header cannot have ":" prefix`, name) |
| } |
| return nil |
| } |
| |
| // ValidateHTTPHeaderValue validates a header value for Envoy |
| // Valid: "foo", "%HOSTNAME%", "100%%", "prefix %HOSTNAME% suffix" |
| // Invalid: "abc%123" |
| // We don't try to check that what is inside the %% is one of Envoy recognized values, we just prevent invalid config. |
| // See: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers.html#custom-request-response-headers |
| func ValidateHTTPHeaderValue(value string) error { |
| if strings.Count(value, "%")%2 != 0 { |
| return errors.New("single % not allowed. Escape by doubling to %% or encase Envoy variable name in pair of %") |
| } |
| return nil |
| } |
| |
| // ValidatePercent checks that percent is in range |
| func ValidatePercent(val int32) error { |
| if val < 0 || val > 100 { |
| return fmt.Errorf("percentage %v is not in range 0..100", val) |
| } |
| return nil |
| } |
| |
| // validatePercentage checks if the specified fractional percentage is valid. |
| func validatePercentage(percentage *networking.Percent) error { |
| if percentage != nil { |
| if percentage.Value < 0.0 || percentage.Value > 100.0 || (percentage.Value > 0.0 && percentage.Value < 0.0001) { |
| return fmt.Errorf("percentage %v is neither 0.0, nor in range [0.0001, 100.0]", percentage.Value) |
| } |
| } |
| return nil |
| } |
| |
| // ValidateIPSubnet checks that a string is in "CIDR notation" or "Dot-decimal notation" |
| func ValidateIPSubnet(subnet string) error { |
| // We expect a string in "CIDR notation" or "Dot-decimal notation" |
| // E.g., a.b.c.d/xx form or just a.b.c.d or 2001:1::1/64 |
| if strings.Count(subnet, "/") == 1 { |
| // We expect a string in "CIDR notation", i.e. a.b.c.d/xx or 2001:1::1/64 form |
| ip, _, err := net.ParseCIDR(subnet) |
| if err != nil { |
| return fmt.Errorf("%v is not a valid CIDR block", subnet) |
| } |
| if ip.To4() == nil && ip.To16() == nil { |
| return fmt.Errorf("%v is not a valid IPv4 or IPv6 address", subnet) |
| } |
| return nil |
| } |
| return ValidateIPAddress(subnet) |
| } |
| |
| // ValidateIPAddress validates that a string in "CIDR notation" or "Dot-decimal notation" |
| func ValidateIPAddress(addr string) error { |
| ip := net.ParseIP(addr) |
| if ip == nil { |
| return fmt.Errorf("%v is not a valid IP", addr) |
| } |
| |
| return nil |
| } |
| |
| // ValidateUnixAddress validates that the string is a valid unix domain socket path. |
| func ValidateUnixAddress(addr string) error { |
| if len(addr) == 0 { |
| return errors.New("unix address must not be empty") |
| } |
| |
| // Allow unix abstract domain sockets whose names start with @ |
| if strings.HasPrefix(addr, "@") { |
| return nil |
| } |
| |
| // Note that we use path, not path/filepath even though a domain socket path is a file path. We don't want the |
| // Pilot output to depend on which OS Pilot is run on, so we always use Unix-style forward slashes. |
| if !path.IsAbs(addr) || strings.HasSuffix(addr, "/") { |
| return fmt.Errorf("%s is not an absolute path to a file", addr) |
| } |
| return nil |
| } |
| |
| // ValidateGateway checks gateway specifications |
| var ValidateGateway = registerValidateFunc("ValidateGateway", |
| func(cfg config.Config) (Warning, error) { |
| name := cfg.Name |
| v := Validation{} |
| // Gateway name must conform to the DNS label format (no dots) |
| if !labels.IsDNS1123Label(name) { |
| v = appendValidation(v, fmt.Errorf("invalid gateway name: %q", name)) |
| } |
| value, ok := cfg.Spec.(*networking.Gateway) |
| if !ok { |
| v = appendValidation(v, fmt.Errorf("cannot cast to gateway: %#v", cfg.Spec)) |
| return v.Unwrap() |
| } |
| |
| if len(value.Servers) == 0 { |
| v = appendValidation(v, fmt.Errorf("gateway must have at least one server")) |
| } else { |
| for _, server := range value.Servers { |
| v = appendValidation(v, validateServer(server)) |
| } |
| } |
| |
| // Ensure unique port names |
| portNames := make(map[string]bool) |
| |
| for _, s := range value.Servers { |
| if s == nil { |
| v = appendValidation(v, fmt.Errorf("server may not be nil")) |
| continue |
| } |
| if s.Port != nil { |
| if portNames[s.Port.Name] { |
| v = appendValidation(v, fmt.Errorf("port names in servers must be unique: duplicate name %s", s.Port.Name)) |
| } |
| portNames[s.Port.Name] = true |
| if !protocol.Parse(s.Port.Protocol).IsHTTP() && s.GetTls().GetHttpsRedirect() { |
| v = appendValidation(v, WrapWarning(fmt.Errorf("tls.httpsRedirect should only be used with http servers"))) |
| } |
| } |
| } |
| |
| return v.Unwrap() |
| }) |
| |
| func validateServer(server *networking.Server) (v Validation) { |
| if server == nil { |
| return WrapError(fmt.Errorf("cannot have nil server")) |
| } |
| if len(server.Hosts) == 0 { |
| v = appendValidation(v, fmt.Errorf("server config must contain at least one host")) |
| } else { |
| for _, hostname := range server.Hosts { |
| v = appendValidation(v, validateNamespaceSlashWildcardHostname(hostname, true)) |
| } |
| } |
| portErr := validateServerPort(server.Port) |
| if portErr != nil { |
| v = appendValidation(v, portErr) |
| } |
| v = appendValidation(v, validateServerBind(server.Port, server.Bind)) |
| v = appendValidation(v, validateTLSOptions(server.Tls)) |
| |
| // If port is HTTPS or TLS, make sure that server has TLS options |
| if portErr == nil { |
| p := protocol.Parse(server.Port.Protocol) |
| if p.IsTLS() && server.Tls == nil { |
| v = appendValidation(v, fmt.Errorf("server must have TLS settings for HTTPS/TLS protocols")) |
| } else if !p.IsTLS() && server.Tls != nil { |
| // only tls redirect is allowed if this is a HTTP server |
| if p.IsHTTP() { |
| if !gateway.IsPassThroughServer(server) || |
| server.Tls.CaCertificates != "" || server.Tls.PrivateKey != "" || server.Tls.ServerCertificate != "" { |
| v = appendValidation(v, fmt.Errorf("server cannot have TLS settings for plain text HTTP ports")) |
| } |
| } else { |
| v = appendValidation(v, fmt.Errorf("server cannot have TLS settings for non HTTPS/TLS ports")) |
| } |
| } |
| } |
| return v |
| } |
| |
| func validateServerPort(port *networking.Port) (errs error) { |
| if port == nil { |
| return appendErrors(errs, fmt.Errorf("port is required")) |
| } |
| if protocol.Parse(port.Protocol) == protocol.Unsupported { |
| errs = appendErrors(errs, fmt.Errorf("invalid protocol %q, supported protocols are HTTP, HTTP2, GRPC, GRPC-WEB, MONGO, REDIS, MYSQL, TCP", port.Protocol)) |
| } |
| if port.Number > 0 { |
| errs = appendErrors(errs, ValidatePort(int(port.Number))) |
| } |
| |
| if port.Name == "" { |
| errs = appendErrors(errs, fmt.Errorf("port name must be set: %v", port)) |
| } |
| return |
| } |
| |
| func validateServerBind(port *networking.Port, bind string) (errs error) { |
| if strings.HasPrefix(bind, UnixAddressPrefix) { |
| errs = appendErrors(errs, ValidateUnixAddress(strings.TrimPrefix(bind, UnixAddressPrefix))) |
| if port != nil && port.Number != 0 { |
| errs = appendErrors(errs, fmt.Errorf("port number must be 0 for unix domain socket: %v", port)) |
| } |
| } else if len(bind) != 0 { |
| errs = appendErrors(errs, ValidateIPAddress(bind)) |
| } |
| return |
| } |
| |
| func validateTLSOptions(tls *networking.ServerTLSSettings) (v Validation) { |
| if tls == nil { |
| // no tls config at all is valid |
| return |
| } |
| |
| invalidCiphers := sets.New() |
| validCiphers := sets.New() |
| duplicateCiphers := sets.New() |
| for _, cs := range tls.CipherSuites { |
| if !security.IsValidCipherSuite(cs) { |
| invalidCiphers.Insert(cs) |
| } else { |
| if !validCiphers.Contains(cs) { |
| validCiphers.Insert(cs) |
| } else { |
| duplicateCiphers.Insert(cs) |
| } |
| } |
| } |
| |
| if len(invalidCiphers) > 0 { |
| v = appendWarningf(v, "ignoring invalid cipher suites: %v", invalidCiphers.SortedList()) |
| } |
| |
| if len(duplicateCiphers) > 0 { |
| v = appendWarningf(v, "ignoring duplicate cipher suites: %v", duplicateCiphers.SortedList()) |
| } |
| |
| if tls.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL { |
| // ISTIO_MUTUAL TLS mode uses either SDS or default certificate mount paths |
| // therefore, we should fail validation if other TLS fields are set |
| if tls.ServerCertificate != "" { |
| v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated server certificate")) |
| } |
| if tls.PrivateKey != "" { |
| v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated private key")) |
| } |
| if tls.CaCertificates != "" { |
| v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated CA bundle")) |
| } |
| if tls.CredentialName != "" { |
| if features.EnableLegacyIstioMutualCredentialName { |
| // Legacy mode enabled, just warn |
| v = appendWarningf(v, "ISTIO_MUTUAL TLS cannot have associated credentialName") |
| } else { |
| v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated credentialName")) |
| } |
| } |
| return |
| } |
| |
| if tls.Mode == networking.ServerTLSSettings_PASSTHROUGH || tls.Mode == networking.ServerTLSSettings_AUTO_PASSTHROUGH { |
| if tls.ServerCertificate != "" || tls.PrivateKey != "" || tls.CaCertificates != "" || tls.CredentialName != "" { |
| // Warn for backwards compatibility |
| v = appendWarningf(v, "%v mode does not use certificates, they will be ignored", tls.Mode) |
| } |
| } |
| |
| if (tls.Mode == networking.ServerTLSSettings_SIMPLE || tls.Mode == networking.ServerTLSSettings_MUTUAL) && tls.CredentialName != "" { |
| // If tls mode is SIMPLE or MUTUAL, and CredentialName is specified, credentials are fetched |
| // remotely. ServerCertificate and CaCertificates fields are not required. |
| return |
| } |
| if tls.Mode == networking.ServerTLSSettings_SIMPLE { |
| if tls.ServerCertificate == "" { |
| v = appendValidation(v, fmt.Errorf("SIMPLE TLS requires a server certificate")) |
| } |
| if tls.PrivateKey == "" { |
| v = appendValidation(v, fmt.Errorf("SIMPLE TLS requires a private key")) |
| } |
| } else if tls.Mode == networking.ServerTLSSettings_MUTUAL { |
| if tls.ServerCertificate == "" { |
| v = appendValidation(v, fmt.Errorf("MUTUAL TLS requires a server certificate")) |
| } |
| if tls.PrivateKey == "" { |
| v = appendValidation(v, fmt.Errorf("MUTUAL TLS requires a private key")) |
| } |
| if tls.CaCertificates == "" { |
| v = appendValidation(v, fmt.Errorf("MUTUAL TLS requires a client CA bundle")) |
| } |
| } |
| return |
| } |
| |
| // ValidateDestinationRule checks proxy policies |
| var ValidateDestinationRule = registerValidateFunc("ValidateDestinationRule", |
| func(cfg config.Config) (Warning, error) { |
| rule, ok := cfg.Spec.(*networking.DestinationRule) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to destination rule") |
| } |
| v := Validation{} |
| if features.EnableDestinationRuleInheritance { |
| if rule.Host == "" { |
| if rule.GetWorkloadSelector() != nil { |
| v = appendValidation(v, |
| fmt.Errorf("mesh/namespace destination rule cannot have workloadSelector configured")) |
| } |
| if len(rule.Subsets) != 0 { |
| v = appendValidation(v, |
| fmt.Errorf("mesh/namespace destination rule cannot have subsets")) |
| } |
| if len(rule.ExportTo) != 0 { |
| v = appendValidation(v, |
| fmt.Errorf("mesh/namespace destination rule cannot have exportTo configured")) |
| } |
| if rule.TrafficPolicy != nil && len(rule.TrafficPolicy.PortLevelSettings) != 0 { |
| v = appendValidation(v, |
| fmt.Errorf("mesh/namespace destination rule cannot have portLevelSettings configured")) |
| } |
| } else { |
| v = appendValidation(v, ValidateWildcardDomain(rule.Host)) |
| } |
| } else { |
| v = appendValidation(v, ValidateWildcardDomain(rule.Host)) |
| } |
| |
| v = appendValidation(v, validateTrafficPolicy(rule.TrafficPolicy)) |
| |
| for _, subset := range rule.Subsets { |
| if subset == nil { |
| v = appendValidation(v, errors.New("subset may not be null")) |
| continue |
| } |
| v = appendValidation(v, validateSubset(subset)) |
| } |
| v = appendValidation(v, |
| validateExportTo(cfg.Namespace, rule.ExportTo, false, rule.GetWorkloadSelector() != nil)) |
| |
| v = appendValidation(v, validateWorkloadSelector(rule.GetWorkloadSelector())) |
| |
| return v.Unwrap() |
| }) |
| |
| func validateExportTo(namespace string, exportTo []string, isServiceEntry bool, isDestinationRuleWithSelector bool) (errs error) { |
| if len(exportTo) > 0 { |
| // Make sure there are no duplicates |
| exportToSet := sets.New() |
| for _, e := range exportTo { |
| key := e |
| if visibility.Instance(e) == visibility.Private { |
| // substitute this with the current namespace so that we |
| // can check for duplicates like ., namespace |
| key = namespace |
| } |
| if exportToSet.Contains(key) { |
| if key != e { |
| errs = appendErrors(errs, fmt.Errorf("duplicate entries in exportTo: . and current namespace %s", namespace)) |
| } else { |
| errs = appendErrors(errs, fmt.Errorf("duplicate entries in exportTo for entry %s", e)) |
| } |
| } else { |
| // if this is a serviceEntry, allow ~ in exportTo as it can be used to create |
| // a service that is not even visible within the local namespace to anyone other |
| // than the proxies of that service. |
| if isServiceEntry && visibility.Instance(e) == visibility.None { |
| exportToSet.Insert(key) |
| } else { |
| if err := visibility.Instance(key).Validate(); err != nil { |
| errs = appendErrors(errs, err) |
| } else { |
| exportToSet.Insert(key) |
| } |
| } |
| } |
| } |
| |
| // Make sure workloadSelector based destination rule does not use exportTo other than current namespace |
| if isDestinationRuleWithSelector && !exportToSet.IsEmpty() { |
| if exportToSet.Contains(namespace) { |
| if len(exportToSet) > 1 { |
| errs = appendErrors(errs, fmt.Errorf("destination rule with workload selector cannot have multiple entries in exportTo")) |
| } |
| } else { |
| errs = appendErrors(errs, fmt.Errorf("destination rule with workload selector cannot have exportTo beyond current namespace")) |
| } |
| } |
| |
| // Make sure we have only one of . or * |
| if exportToSet.Contains(string(visibility.Public)) { |
| // make sure that there are no other entries in the exportTo |
| // i.e. no point in saying ns1,ns2,*. Might as well say * |
| if len(exportTo) > 1 { |
| errs = appendErrors(errs, fmt.Errorf("cannot have both public (*) and non-public exportTo values for a resource")) |
| } |
| } |
| |
| // if this is a service entry, then we need to disallow * and ~ together. Or ~ and other namespaces |
| if exportToSet.Contains(string(visibility.None)) { |
| if len(exportTo) > 1 { |
| errs = appendErrors(errs, fmt.Errorf("cannot export service entry to no one (~) and someone")) |
| } |
| } |
| } |
| |
| return |
| } |
| |
| func validateAlphaWorkloadSelector(selector *networking.WorkloadSelector) error { |
| var errs error |
| if selector != nil { |
| for k, v := range selector.Labels { |
| if k == "" { |
| errs = appendErrors(errs, |
| fmt.Errorf("empty key is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) |
| } |
| if strings.Contains(k, "*") || strings.Contains(v, "*") { |
| errs = appendErrors(errs, |
| fmt.Errorf("wildcard is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) |
| } |
| } |
| } |
| |
| return errs |
| } |
| |
| // ValidateEnvoyFilter checks envoy filter config supplied by user |
| var ValidateEnvoyFilter = registerValidateFunc("ValidateEnvoyFilter", |
| func(cfg config.Config) (Warning, error) { |
| errs := Validation{} |
| rule, ok := cfg.Spec.(*networking.EnvoyFilter) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to Envoy filter") |
| } |
| |
| if err := validateAlphaWorkloadSelector(rule.WorkloadSelector); err != nil { |
| return nil, err |
| } |
| |
| for _, cp := range rule.ConfigPatches { |
| if cp == nil { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: null config patch")) // nolint: stylecheck |
| continue |
| } |
| if cp.ApplyTo == networking.EnvoyFilter_INVALID { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing applyTo")) // nolint: stylecheck |
| continue |
| } |
| if cp.Patch == nil { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing patch")) // nolint: stylecheck |
| continue |
| } |
| if cp.Patch.Operation == networking.EnvoyFilter_Patch_INVALID { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing patch operation")) // nolint: stylecheck |
| continue |
| } |
| if cp.Patch.Operation != networking.EnvoyFilter_Patch_REMOVE && cp.Patch.Value == nil { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing patch value for non-remove operation")) // nolint: stylecheck |
| continue |
| } |
| |
| // ensure that the supplied regex for proxy version compiles |
| if cp.Match != nil && cp.Match.Proxy != nil && cp.Match.Proxy.ProxyVersion != "" { |
| if _, err := regexp.Compile(cp.Match.Proxy.ProxyVersion); err != nil { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: invalid regex for proxy version, [%v]", err)) // nolint: stylecheck |
| continue |
| } |
| } |
| // ensure that applyTo, match and patch all line up |
| switch cp.ApplyTo { |
| case networking.EnvoyFilter_LISTENER, |
| networking.EnvoyFilter_FILTER_CHAIN, |
| networking.EnvoyFilter_NETWORK_FILTER, |
| networking.EnvoyFilter_HTTP_FILTER: |
| if cp.Match != nil && cp.Match.ObjectTypes != nil { |
| if cp.Match.GetListener() == nil { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: applyTo for listener class objects cannot have non listener match")) // nolint: stylecheck |
| continue |
| } |
| listenerMatch := cp.Match.GetListener() |
| if listenerMatch.FilterChain != nil { |
| if listenerMatch.FilterChain.Filter != nil { |
| if cp.ApplyTo == networking.EnvoyFilter_LISTENER || cp.ApplyTo == networking.EnvoyFilter_FILTER_CHAIN { |
| // This would be an error but is a warning for backwards compatibility |
| errs = appendValidation(errs, WrapWarning( |
| fmt.Errorf("Envoy filter: filter match has no effect when used with %v", cp.ApplyTo))) // nolint: stylecheck |
| } |
| // filter names are required if network filter matches are being made |
| if listenerMatch.FilterChain.Filter.Name == "" { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: filter match has no name to match on")) // nolint: stylecheck |
| continue |
| } else if listenerMatch.FilterChain.Filter.SubFilter != nil { |
| // sub filter match is supported only for applyTo HTTP_FILTER |
| if cp.ApplyTo != networking.EnvoyFilter_HTTP_FILTER { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: subfilter match can be used with applyTo HTTP_FILTER only")) // nolint: stylecheck |
| continue |
| } |
| // sub filter match requires the network filter to match to envoy http connection manager |
| if listenerMatch.FilterChain.Filter.Name != wellknown.HTTPConnectionManager && |
| listenerMatch.FilterChain.Filter.Name != "envoy.http_connection_manager" { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: subfilter match requires filter match with %s", // nolint: stylecheck |
| wellknown.HTTPConnectionManager)) |
| continue |
| } |
| if listenerMatch.FilterChain.Filter.SubFilter.Name == "" { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: subfilter match has no name to match on")) // nolint: stylecheck |
| continue |
| } |
| } |
| |
| errs = appendValidation(errs, validateListenerMatchName(listenerMatch.FilterChain.Filter.GetName())) |
| errs = appendValidation(errs, validateListenerMatchName(listenerMatch.FilterChain.Filter.GetSubFilter().GetName())) |
| } |
| } |
| } |
| case networking.EnvoyFilter_ROUTE_CONFIGURATION, networking.EnvoyFilter_VIRTUAL_HOST, networking.EnvoyFilter_HTTP_ROUTE: |
| if cp.Match != nil && cp.Match.ObjectTypes != nil { |
| if cp.Match.GetRouteConfiguration() == nil { |
| errs = appendValidation(errs, |
| fmt.Errorf("Envoy filter: applyTo for http route class objects cannot have non route configuration match")) // nolint: stylecheck |
| } |
| } |
| |
| case networking.EnvoyFilter_CLUSTER: |
| if cp.Match != nil && cp.Match.ObjectTypes != nil { |
| if cp.Match.GetCluster() == nil { |
| errs = appendValidation(errs, fmt.Errorf("Envoy filter: applyTo for cluster class objects cannot have non cluster match")) // nolint: stylecheck |
| } |
| } |
| } |
| // ensure that the struct is valid |
| if _, err := xds.BuildXDSObjectFromStruct(cp.ApplyTo, cp.Patch.Value, false); err != nil { |
| errs = appendValidation(errs, err) |
| } else { |
| // Run with strict validation, and emit warnings. This helps capture cases like unknown fields |
| // We do not want to reject in case the proto is valid but our libraries are outdated |
| obj, err := xds.BuildXDSObjectFromStruct(cp.ApplyTo, cp.Patch.Value, true) |
| if err != nil { |
| errs = appendValidation(errs, WrapWarning(err)) |
| } |
| |
| // Append any deprecation notices |
| if obj != nil { |
| errs = appendValidation(errs, validateDeprecatedFilterTypes(obj)) |
| errs = appendValidation(errs, validateMissingTypedConfigFilterTypes(obj)) |
| } |
| } |
| } |
| |
| return errs.Unwrap() |
| }) |
| |
| func validateListenerMatchName(name string) error { |
| if newName, f := xds.ReverseDeprecatedFilterNames[name]; f { |
| return WrapWarning(fmt.Errorf("using deprecated filter name %q; use %q instead", name, newName)) |
| } |
| return nil |
| } |
| |
| func recurseDeprecatedTypes(message protoreflect.Message) ([]string, error) { |
| var topError error |
| var deprecatedTypes []string |
| if message == nil { |
| return nil, nil |
| } |
| message.Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool { |
| m, isMessage := value.Interface().(protoreflect.Message) |
| if isMessage { |
| anyMessage, isAny := m.Interface().(*any.Any) |
| if isAny { |
| mt, err := protoregistry.GlobalTypes.FindMessageByURL(anyMessage.TypeUrl) |
| if err != nil { |
| topError = err |
| return false |
| } |
| var fileOpts proto.Message = mt.Descriptor().ParentFile().Options().(*descriptorpb.FileOptions) |
| if proto.HasExtension(fileOpts, udpaa.E_FileStatus) { |
| ext := proto.GetExtension(fileOpts, udpaa.E_FileStatus) |
| udpaext, ok := ext.(*udpaa.StatusAnnotation) |
| if !ok { |
| topError = fmt.Errorf("extension was of wrong type: %T", ext) |
| return false |
| } |
| if udpaext.PackageVersionStatus == udpaa.PackageVersionStatus_FROZEN { |
| deprecatedTypes = append(deprecatedTypes, anyMessage.TypeUrl) |
| } |
| } |
| } |
| newTypes, err := recurseDeprecatedTypes(m) |
| if err != nil { |
| topError = err |
| return false |
| } |
| deprecatedTypes = append(deprecatedTypes, newTypes...) |
| } |
| return true |
| }) |
| return deprecatedTypes, topError |
| } |
| |
| // recurseMissingTypedConfig checks that configured filters do not rely on `name` and elide `typed_config`. |
| // This is temporarily enabled in Envoy by the envoy.reloadable_features.no_extension_lookup_by_name flag, but in the future will be removed. |
| func recurseMissingTypedConfig(message protoreflect.Message) []string { |
| var deprecatedTypes []string |
| if message == nil { |
| return nil |
| } |
| // First, iterate over the fields to find the 'name' field to help with reporting errors. |
| var name string |
| for i := 0; i < message.Type().Descriptor().Fields().Len(); i++ { |
| field := message.Type().Descriptor().Fields().Get(i) |
| if field.JSONName() == "name" { |
| name = fmt.Sprintf("%v", message.Get(field).Interface()) |
| } |
| } |
| |
| // Now go through fields again |
| for i := 0; i < message.Type().Descriptor().Fields().Len(); i++ { |
| field := message.Type().Descriptor().Fields().Get(i) |
| set := message.Has(field) |
| // If it has a typedConfig field, it must be set. |
| // Note: it is possible there is some API that has typedConfig but has a non-deprecated alternative, |
| // but I couldn't find any. Worst case, this is a warning, not an error, so a false positive is not so bad. |
| if field.JSONName() == "typedConfig" && !set { |
| deprecatedTypes = append(deprecatedTypes, name) |
| } |
| if set { |
| // If the field was set and is a message, recurse into it to check children |
| m, isMessage := message.Get(field).Interface().(protoreflect.Message) |
| if isMessage { |
| deprecatedTypes = append(deprecatedTypes, recurseMissingTypedConfig(m)...) |
| } |
| } |
| } |
| return deprecatedTypes |
| } |
| |
| func validateDeprecatedFilterTypes(obj proto.Message) error { |
| deprecated, err := recurseDeprecatedTypes(obj.ProtoReflect()) |
| if err != nil { |
| return fmt.Errorf("failed to find deprecated types: %v", err) |
| } |
| if len(deprecated) > 0 { |
| return WrapWarning(fmt.Errorf("using deprecated type_url(s); %v", strings.Join(deprecated, ", "))) |
| } |
| return nil |
| } |
| |
| func validateMissingTypedConfigFilterTypes(obj proto.Message) error { |
| missing := recurseMissingTypedConfig(obj.ProtoReflect()) |
| if len(missing) > 0 { |
| return WrapWarning(fmt.Errorf("using deprecated types by name without typed_config; %v", strings.Join(missing, ", "))) |
| } |
| return nil |
| } |
| |
| // validates that hostname in ns/<hostname> is a valid hostname according to |
| // API specs |
| func validateSidecarOrGatewayHostnamePart(hostname string, isGateway bool) (errs error) { |
| // short name hosts are not allowed |
| if hostname != "*" && !strings.Contains(hostname, ".") { |
| errs = appendErrors(errs, fmt.Errorf("short names (non FQDN) are not allowed")) |
| } |
| |
| if err := ValidateWildcardDomain(hostname); err != nil { |
| if !isGateway { |
| errs = appendErrors(errs, err) |
| } |
| |
| // Gateway allows IP as the host string, as well |
| ipAddr := net.ParseIP(hostname) |
| if ipAddr == nil { |
| errs = appendErrors(errs, err) |
| } |
| } |
| return |
| } |
| |
| func validateNamespaceSlashWildcardHostname(hostname string, isGateway bool) (errs error) { |
| parts := strings.SplitN(hostname, "/", 2) |
| if len(parts) != 2 { |
| if isGateway { |
| // Old style host in the gateway |
| return validateSidecarOrGatewayHostnamePart(hostname, true) |
| } |
| errs = appendErrors(errs, fmt.Errorf("host must be of form namespace/dnsName")) |
| return |
| } |
| |
| if len(parts[0]) == 0 || len(parts[1]) == 0 { |
| errs = appendErrors(errs, fmt.Errorf("config namespace and dnsName in host entry cannot be empty")) |
| } |
| |
| if !isGateway { |
| // namespace can be * or . or ~ or a valid DNS label in sidecars |
| if parts[0] != "*" && parts[0] != "." && parts[0] != "~" { |
| if !labels.IsDNS1123Label(parts[0]) { |
| errs = appendErrors(errs, fmt.Errorf("invalid namespace value %q in sidecar", parts[0])) |
| } |
| } |
| } else { |
| // namespace can be * or . or a valid DNS label in gateways |
| if parts[0] != "*" && parts[0] != "." { |
| if !labels.IsDNS1123Label(parts[0]) { |
| errs = appendErrors(errs, fmt.Errorf("invalid namespace value %q in gateway", parts[0])) |
| } |
| } |
| } |
| errs = appendErrors(errs, validateSidecarOrGatewayHostnamePart(parts[1], isGateway)) |
| return |
| } |
| |
| // ValidateSidecar checks sidecar config supplied by user |
| var ValidateSidecar = registerValidateFunc("ValidateSidecar", |
| func(cfg config.Config) (Warning, error) { |
| errs := Validation{} |
| rule, ok := cfg.Spec.(*networking.Sidecar) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to Sidecar") |
| } |
| |
| if err := validateAlphaWorkloadSelector(rule.WorkloadSelector); err != nil { |
| return nil, err |
| } |
| |
| if len(rule.Egress) == 0 && len(rule.Ingress) == 0 && rule.OutboundTrafficPolicy == nil { |
| return nil, fmt.Errorf("sidecar: empty configuration provided") |
| } |
| |
| portMap := make(map[uint32]struct{}) |
| for _, i := range rule.Ingress { |
| if i == nil { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: ingress may not be null")) |
| continue |
| } |
| if i.Port == nil { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: port is required for ingress listeners")) |
| continue |
| } |
| |
| bind := i.GetBind() |
| errs = appendValidation(errs, validateSidecarIngressPortAndBind(i.Port, bind)) |
| |
| if _, found := portMap[i.Port.Number]; found { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: ports on IP bound listeners must be unique")) |
| } |
| portMap[i.Port.Number] = struct{}{} |
| |
| if len(i.DefaultEndpoint) != 0 { |
| if strings.HasPrefix(i.DefaultEndpoint, UnixAddressPrefix) { |
| errs = appendValidation(errs, ValidateUnixAddress(strings.TrimPrefix(i.DefaultEndpoint, UnixAddressPrefix))) |
| } else { |
| // format should be 127.0.0.1:port or :port |
| parts := strings.Split(i.DefaultEndpoint, ":") |
| if len(parts) < 2 { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: defaultEndpoint must be of form 127.0.0.1:<port>, 0.0.0.0:<port>, unix://filepath, or unset")) |
| } else { |
| if len(parts[0]) > 0 && parts[0] != "127.0.0.1" && parts[0] != "0.0.0.0" { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: defaultEndpoint must be of form 127.0.0.1:<port>, 0.0.0.0:<port>, unix://filepath, or unset")) |
| } |
| |
| port, err := strconv.Atoi(parts[1]) |
| if err != nil { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: defaultEndpoint port (%s) is not a number: %v", parts[1], err)) |
| } else { |
| errs = appendValidation(errs, ValidatePort(port)) |
| } |
| } |
| } |
| } |
| |
| if i.Tls != nil { |
| if len(i.Tls.SubjectAltNames) > 0 { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: subjectAltNames is not supported in ingress tls")) |
| } |
| if i.Tls.HttpsRedirect { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: httpsRedirect is not supported")) |
| } |
| if i.Tls.CredentialName != "" { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: credentialName is not currently supported")) |
| } |
| if i.Tls.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL || i.Tls.Mode == networking.ServerTLSSettings_AUTO_PASSTHROUGH { |
| errs = appendValidation(errs, fmt.Errorf("configuration is invalid: cannot set mode to %s in sidecar ingress tls", i.Tls.Mode.String())) |
| } |
| protocol := protocol.Parse(i.Port.Protocol) |
| if !protocol.IsTLS() { |
| errs = appendValidation(errs, fmt.Errorf("server cannot have TLS settings for non HTTPS/TLS ports")) |
| } |
| errs = appendValidation(errs, validateTLSOptions(i.Tls)) |
| } |
| } |
| |
| portMap = make(map[uint32]struct{}) |
| udsMap := make(map[string]struct{}) |
| catchAllEgressListenerFound := false |
| for index, egress := range rule.Egress { |
| if egress == nil { |
| errs = appendValidation(errs, errors.New("egress listener may not be null")) |
| continue |
| } |
| // there can be only one catch all egress listener with empty port, and it should be the last listener. |
| if egress.Port == nil { |
| if !catchAllEgressListenerFound { |
| if index == len(rule.Egress)-1 { |
| catchAllEgressListenerFound = true |
| } else { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: the egress listener with empty port should be the last listener in the list")) |
| } |
| } else { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: egress can have only one listener with empty port")) |
| continue |
| } |
| } else { |
| bind := egress.GetBind() |
| captureMode := egress.GetCaptureMode() |
| errs = appendValidation(errs, validateSidecarEgressPortBindAndCaptureMode(egress.Port, bind, captureMode)) |
| |
| if egress.Port.Number == 0 { |
| if _, found := udsMap[bind]; found { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: unix domain socket values for listeners must be unique")) |
| } |
| udsMap[bind] = struct{}{} |
| } else { |
| if _, found := portMap[egress.Port.Number]; found { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: ports on IP bound listeners must be unique")) |
| } |
| portMap[egress.Port.Number] = struct{}{} |
| } |
| } |
| |
| // validate that the hosts field is a slash separated value |
| // of form ns1/host, or */host, or */*, or ns1/*, or ns1/*.example.com |
| if len(egress.Hosts) == 0 { |
| errs = appendValidation(errs, fmt.Errorf("sidecar: egress listener must contain at least one host")) |
| } else { |
| nssSvcs := map[string]map[string]bool{} |
| for _, hostname := range egress.Hosts { |
| parts := strings.SplitN(hostname, "/", 2) |
| if len(parts) == 2 { |
| ns := parts[0] |
| svc := parts[1] |
| if ns == "." { |
| ns = cfg.Namespace |
| } |
| if _, ok := nssSvcs[ns]; !ok { |
| nssSvcs[ns] = map[string]bool{} |
| } |
| |
| // test/a |
| // test/a |
| // test/* |
| if svc != "*" { |
| if _, ok := nssSvcs[ns][svc]; ok || nssSvcs[ns]["*"] { |
| // already exists |
| // TODO: prevent this invalid setting, maybe in 1.12+ |
| errs = appendValidation(errs, WrapWarning(fmt.Errorf("duplicated egress host: %s", hostname))) |
| } |
| } else { |
| if len(nssSvcs[ns]) != 0 { |
| errs = appendValidation(errs, WrapWarning(fmt.Errorf("duplicated egress host: %s", hostname))) |
| } |
| } |
| nssSvcs[ns][svc] = true |
| } |
| errs = appendValidation(errs, validateNamespaceSlashWildcardHostname(hostname, false)) |
| } |
| // */* |
| // test/a |
| if nssSvcs["*"]["*"] && len(nssSvcs) != 1 { |
| errs = appendValidation(errs, WrapWarning(fmt.Errorf("`*/*` host select all resources, no other hosts can be added"))) |
| } |
| } |
| } |
| |
| errs = appendValidation(errs, validateSidecarOutboundTrafficPolicy(rule.OutboundTrafficPolicy)) |
| |
| return errs.Unwrap() |
| }) |
| |
| func validateSidecarOutboundTrafficPolicy(tp *networking.OutboundTrafficPolicy) (errs error) { |
| if tp == nil { |
| return |
| } |
| mode := tp.GetMode() |
| if tp.EgressProxy != nil { |
| if mode != networking.OutboundTrafficPolicy_ALLOW_ANY { |
| errs = appendErrors(errs, fmt.Errorf("sidecar: egress_proxy must be set only with ALLOW_ANY outbound_traffic_policy mode")) |
| return |
| } |
| |
| errs = appendErrors(errs, ValidateFQDN(tp.EgressProxy.GetHost())) |
| |
| if tp.EgressProxy.Port == nil { |
| errs = appendErrors(errs, fmt.Errorf("sidecar: egress_proxy port must be non-nil")) |
| return |
| } |
| errs = appendErrors(errs, validateDestination(tp.EgressProxy)) |
| } |
| return |
| } |
| |
| func validateSidecarEgressPortBindAndCaptureMode(port *networking.Port, bind string, |
| captureMode networking.CaptureMode) (errs error) { |
| // Port name is optional. Validate if exists. |
| if len(port.Name) > 0 { |
| errs = appendErrors(errs, ValidatePortName(port.Name)) |
| } |
| |
| // Handle Unix domain sockets |
| if port.Number == 0 { |
| // require bind to be a unix domain socket |
| errs = appendErrors(errs, |
| ValidateProtocol(port.Protocol)) |
| |
| if !strings.HasPrefix(bind, UnixAddressPrefix) { |
| errs = appendErrors(errs, fmt.Errorf("sidecar: ports with 0 value must have a unix domain socket bind address")) |
| } else { |
| errs = appendErrors(errs, ValidateUnixAddress(strings.TrimPrefix(bind, UnixAddressPrefix))) |
| } |
| |
| if captureMode != networking.CaptureMode_DEFAULT && captureMode != networking.CaptureMode_NONE { |
| errs = appendErrors(errs, fmt.Errorf("sidecar: captureMode must be DEFAULT/NONE for unix domain socket listeners")) |
| } |
| } else { |
| errs = appendErrors(errs, |
| ValidateProtocol(port.Protocol), |
| ValidatePort(int(port.Number))) |
| |
| if len(bind) != 0 { |
| errs = appendErrors(errs, ValidateIPAddress(bind)) |
| } |
| } |
| |
| return |
| } |
| |
| func validateSidecarIngressPortAndBind(port *networking.Port, bind string) (errs error) { |
| // Port name is optional. Validate if exists. |
| if len(port.Name) > 0 { |
| errs = appendErrors(errs, ValidatePortName(port.Name)) |
| } |
| |
| errs = appendErrors(errs, |
| ValidateProtocol(port.Protocol), |
| ValidatePort(int(port.Number))) |
| |
| if len(bind) != 0 { |
| errs = appendErrors(errs, ValidateIPAddress(bind)) |
| } |
| |
| return |
| } |
| |
| func validateTrafficPolicy(policy *networking.TrafficPolicy) Validation { |
| if policy == nil { |
| return Validation{} |
| } |
| if policy.OutlierDetection == nil && policy.ConnectionPool == nil && |
| policy.LoadBalancer == nil && policy.Tls == nil && policy.PortLevelSettings == nil { |
| return WrapError(fmt.Errorf("traffic policy must have at least one field")) |
| } |
| |
| return appendValidation(validateOutlierDetection(policy.OutlierDetection), |
| validateConnectionPool(policy.ConnectionPool), |
| validateLoadBalancer(policy.LoadBalancer), |
| validateTLS(policy.Tls), |
| validatePortTrafficPolicies(policy.PortLevelSettings)) |
| } |
| |
| func validateOutlierDetection(outlier *networking.OutlierDetection) (errs Validation) { |
| if outlier == nil { |
| return |
| } |
| |
| if outlier.BaseEjectionTime != nil { |
| errs = appendValidation(errs, ValidateDuration(outlier.BaseEjectionTime)) |
| } |
| // nolint: staticcheck |
| if outlier.ConsecutiveErrors != 0 { |
| warn := "outlier detection consecutive errors is deprecated, use consecutiveGatewayErrors or consecutive5xxErrors instead" |
| scope.Warnf(warn) |
| errs = appendValidation(errs, WrapWarning(errors.New(warn))) |
| } |
| if !outlier.SplitExternalLocalOriginErrors && outlier.ConsecutiveLocalOriginFailures.GetValue() > 0 { |
| err := "outlier detection consecutive local origin failures is specified, but split external local origin errors is set to false" |
| errs = appendValidation(errs, errors.New(err)) |
| } |
| if outlier.Interval != nil { |
| errs = appendValidation(errs, ValidateDuration(outlier.Interval)) |
| } |
| errs = appendValidation(errs, ValidatePercent(outlier.MaxEjectionPercent), ValidatePercent(outlier.MinHealthPercent)) |
| |
| return |
| } |
| |
| func validateConnectionPool(settings *networking.ConnectionPoolSettings) (errs error) { |
| if settings == nil { |
| return |
| } |
| if settings.Http == nil && settings.Tcp == nil { |
| return fmt.Errorf("connection pool must have at least one field") |
| } |
| |
| if httpSettings := settings.Http; httpSettings != nil { |
| if httpSettings.Http1MaxPendingRequests < 0 { |
| errs = appendErrors(errs, fmt.Errorf("http1 max pending requests must be non-negative")) |
| } |
| if httpSettings.Http2MaxRequests < 0 { |
| errs = appendErrors(errs, fmt.Errorf("http2 max requests must be non-negative")) |
| } |
| if httpSettings.MaxRequestsPerConnection < 0 { |
| errs = appendErrors(errs, fmt.Errorf("max requests per connection must be non-negative")) |
| } |
| if httpSettings.MaxRetries < 0 { |
| errs = appendErrors(errs, fmt.Errorf("max retries must be non-negative")) |
| } |
| if httpSettings.IdleTimeout != nil { |
| errs = appendErrors(errs, ValidateDuration(httpSettings.IdleTimeout)) |
| } |
| if httpSettings.H2UpgradePolicy == networking.ConnectionPoolSettings_HTTPSettings_UPGRADE && httpSettings.UseClientProtocol { |
| errs = appendErrors(errs, fmt.Errorf("use client protocol must not be true when H2UpgradePolicy is UPGRADE")) |
| } |
| } |
| |
| if tcp := settings.Tcp; tcp != nil { |
| if tcp.MaxConnections < 0 { |
| errs = appendErrors(errs, fmt.Errorf("max connections must be non-negative")) |
| } |
| if tcp.ConnectTimeout != nil { |
| errs = appendErrors(errs, ValidateDuration(tcp.ConnectTimeout)) |
| } |
| } |
| |
| return |
| } |
| |
| func validateLoadBalancer(settings *networking.LoadBalancerSettings) (errs error) { |
| if settings == nil { |
| return |
| } |
| |
| // simple load balancing is always valid |
| |
| consistentHash := settings.GetConsistentHash() |
| if consistentHash != nil { |
| httpCookie := consistentHash.GetHttpCookie() |
| if httpCookie != nil { |
| if httpCookie.Name == "" { |
| errs = appendErrors(errs, fmt.Errorf("name required for HttpCookie")) |
| } |
| if httpCookie.Ttl == nil { |
| errs = appendErrors(errs, fmt.Errorf("ttl required for HttpCookie")) |
| } |
| } |
| } |
| if err := validateLocalityLbSetting(settings.LocalityLbSetting); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| return |
| } |
| |
| func validateTLS(settings *networking.ClientTLSSettings) (errs error) { |
| if settings == nil { |
| return |
| } |
| |
| if (settings.Mode == networking.ClientTLSSettings_SIMPLE || settings.Mode == networking.ClientTLSSettings_MUTUAL) && |
| settings.CredentialName != "" { |
| if settings.ClientCertificate != "" || settings.CaCertificates != "" || settings.PrivateKey != "" { |
| errs = appendErrors(errs, |
| fmt.Errorf("cannot specify client certificates or CA certificate If credentialName is set")) |
| } |
| |
| // If tls mode is SIMPLE or MUTUAL, and CredentialName is specified, credentials are fetched |
| // remotely. ServerCertificate and CaCertificates fields are not required. |
| return |
| } |
| |
| if settings.Mode == networking.ClientTLSSettings_MUTUAL { |
| if settings.ClientCertificate == "" { |
| errs = appendErrors(errs, fmt.Errorf("client certificate required for mutual tls")) |
| } |
| if settings.PrivateKey == "" { |
| errs = appendErrors(errs, fmt.Errorf("private key required for mutual tls")) |
| } |
| } |
| |
| return |
| } |
| |
| func validateSubset(subset *networking.Subset) error { |
| return appendErrors(validateSubsetName(subset.Name), |
| labels.Instance(subset.Labels).Validate(), |
| validateTrafficPolicy(subset.TrafficPolicy)) |
| } |
| |
| func validatePortTrafficPolicies(pls []*networking.TrafficPolicy_PortTrafficPolicy) (errs error) { |
| for _, t := range pls { |
| if t == nil { |
| errs = appendErrors(errs, fmt.Errorf("traffic policy may not be null")) |
| continue |
| } |
| if t.Port == nil { |
| errs = appendErrors(errs, fmt.Errorf("portTrafficPolicy must have valid port")) |
| } |
| if t.OutlierDetection == nil && t.ConnectionPool == nil && |
| t.LoadBalancer == nil && t.Tls == nil { |
| errs = appendErrors(errs, fmt.Errorf("port traffic policy must have at least one field")) |
| } else { |
| errs = appendErrors(errs, validateOutlierDetection(t.OutlierDetection), |
| validateConnectionPool(t.ConnectionPool), |
| validateLoadBalancer(t.LoadBalancer), |
| validateTLS(t.Tls)) |
| } |
| } |
| return |
| } |
| |
| // ValidateProxyAddress checks that a network address is well-formed |
| func ValidateProxyAddress(hostAddr string) error { |
| hostname, p, err := net.SplitHostPort(hostAddr) |
| if err != nil { |
| return fmt.Errorf("unable to split %q: %v", hostAddr, err) |
| } |
| port, err := strconv.Atoi(p) |
| if err != nil { |
| return fmt.Errorf("port (%s) is not a number: %v", p, err) |
| } |
| if err = ValidatePort(port); err != nil { |
| return err |
| } |
| if err = ValidateFQDN(hostname); err != nil { |
| ip := net.ParseIP(hostname) |
| if ip == nil { |
| return fmt.Errorf("%q is not a valid hostname or an IP address", hostname) |
| } |
| } |
| |
| return nil |
| } |
| |
| // ValidateDuration checks that a proto duration is well-formed |
| func ValidateDuration(pd *durationpb.Duration) error { |
| dur := pd.AsDuration() |
| if dur < time.Millisecond { |
| return errors.New("duration must be greater than 1ms") |
| } |
| if dur%time.Millisecond != 0 { |
| return errors.New("only durations to ms precision are supported") |
| } |
| return nil |
| } |
| |
| // ValidateDurationRange verifies range is in specified duration |
| func ValidateDurationRange(dur, min, max time.Duration) error { |
| if dur > max || dur < min { |
| return fmt.Errorf("time %v must be >%v and <%v", dur.String(), min.String(), max.String()) |
| } |
| |
| return nil |
| } |
| |
| // ValidateParentAndDrain checks that parent and drain durations are valid |
| func ValidateParentAndDrain(drainTime, parentShutdown *durationpb.Duration) (errs error) { |
| if err := ValidateDuration(drainTime); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid drain duration:")) |
| } |
| if err := ValidateDuration(parentShutdown); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid parent shutdown duration:")) |
| } |
| if errs != nil { |
| return |
| } |
| |
| drainDuration := drainTime.AsDuration() |
| parentShutdownDuration := parentShutdown.AsDuration() |
| |
| if drainDuration%time.Second != 0 { |
| errs = multierror.Append(errs, |
| errors.New("drain time only supports durations to seconds precision")) |
| } |
| if parentShutdownDuration%time.Second != 0 { |
| errs = multierror.Append(errs, |
| errors.New("parent shutdown time only supports durations to seconds precision")) |
| } |
| if parentShutdownDuration <= drainDuration { |
| errs = multierror.Append(errs, |
| fmt.Errorf("parent shutdown time %v must be greater than drain time %v", |
| parentShutdownDuration.String(), drainDuration.String())) |
| } |
| |
| if drainDuration > drainTimeMax { |
| errs = multierror.Append(errs, |
| fmt.Errorf("drain time %v must be <%v", drainDuration.String(), drainTimeMax.String())) |
| } |
| |
| if parentShutdownDuration > parentShutdownTimeMax { |
| errs = multierror.Append(errs, |
| fmt.Errorf("parent shutdown time %v must be <%v", |
| parentShutdownDuration.String(), parentShutdownTimeMax.String())) |
| } |
| |
| return |
| } |
| |
| // ValidateLightstepCollector validates the configuration for sending envoy spans to LightStep |
| func ValidateLightstepCollector(ls *meshconfig.Tracing_Lightstep) error { |
| var errs error |
| if ls.GetAddress() == "" { |
| errs = multierror.Append(errs, errors.New("address is required")) |
| } |
| if err := ValidateProxyAddress(ls.GetAddress()); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid lightstep address:")) |
| } |
| if ls.GetAccessToken() == "" { |
| errs = multierror.Append(errs, errors.New("access token is required")) |
| } |
| return errs |
| } |
| |
| // validateCustomTags validates that tracing CustomTags map does not contain any nil items |
| func validateCustomTags(tags map[string]*meshconfig.Tracing_CustomTag) error { |
| for tagName, tagVal := range tags { |
| if tagVal == nil { |
| return fmt.Errorf("encountered nil value for custom tag: %s", tagName) |
| } |
| } |
| return nil |
| } |
| |
| // ValidateZipkinCollector validates the configuration for sending envoy spans to Zipkin |
| func ValidateZipkinCollector(z *meshconfig.Tracing_Zipkin) error { |
| return ValidateProxyAddress(strings.Replace(z.GetAddress(), "$(HOST_IP)", "127.0.0.1", 1)) |
| } |
| |
| // ValidateDatadogCollector validates the configuration for sending envoy spans to Datadog |
| func ValidateDatadogCollector(d *meshconfig.Tracing_Datadog) error { |
| // If the address contains $(HOST_IP), replace it with a valid IP before validation. |
| return ValidateProxyAddress(strings.Replace(d.GetAddress(), "$(HOST_IP)", "127.0.0.1", 1)) |
| } |
| |
| // ValidateConnectTimeout validates the envoy connection timeout |
| func ValidateConnectTimeout(timeout *durationpb.Duration) error { |
| if err := ValidateDuration(timeout); err != nil { |
| return err |
| } |
| |
| err := ValidateDurationRange(timeout.AsDuration(), connectTimeoutMin, connectTimeoutMax) |
| return err |
| } |
| |
| // ValidateProtocolDetectionTimeout validates the envoy protocol detection timeout |
| func ValidateProtocolDetectionTimeout(timeout *durationpb.Duration) error { |
| dur := timeout.AsDuration() |
| // 0s is a valid value if trying to disable protocol detection timeout |
| if dur == time.Second*0 { |
| return nil |
| } |
| if dur%time.Millisecond != 0 { |
| return errors.New("only durations to ms precision are supported") |
| } |
| |
| return nil |
| } |
| |
| // ValidateMaxServerConnectionAge validate negative duration |
| func ValidateMaxServerConnectionAge(in time.Duration) error { |
| if err := IsNegativeDuration(in); err != nil { |
| return fmt.Errorf("%v: --keepaliveMaxServerConnectionAge only accepts positive duration eg: 30m", err) |
| } |
| return nil |
| } |
| |
| // IsNegativeDuration check if the duration is negative |
| func IsNegativeDuration(in time.Duration) error { |
| if in < 0 { |
| return fmt.Errorf("invalid duration: %s", in.String()) |
| } |
| return nil |
| } |
| |
| // ValidateMeshConfig checks that the mesh config is well-formed |
| func ValidateMeshConfig(mesh *meshconfig.MeshConfig) (errs error) { |
| if err := ValidatePort(int(mesh.ProxyListenPort)); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid proxy listen port:")) |
| } |
| |
| if err := ValidateConnectTimeout(mesh.ConnectTimeout); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid connect timeout:")) |
| } |
| |
| if err := ValidateProtocolDetectionTimeout(mesh.ProtocolDetectionTimeout); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid protocol detection timeout:")) |
| } |
| |
| if mesh.DefaultConfig == nil { |
| errs = multierror.Append(errs, errors.New("missing default config")) |
| } else if err := ValidateMeshConfigProxyConfig(mesh.DefaultConfig); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| |
| if err := validateLocalityLbSetting(mesh.LocalityLbSetting); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| |
| if err := validateServiceSettings(mesh); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| |
| if err := validateTrustDomainConfig(mesh); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| |
| if err := validateExtensionProvider(mesh); err != nil { |
| scope.Warnf("found invalid extension provider (can be ignored if the given extension provider is not used): %v", err) |
| } |
| |
| return |
| } |
| |
| func validateTrustDomainConfig(config *meshconfig.MeshConfig) (errs error) { |
| if err := ValidateTrustDomain(config.TrustDomain); err != nil { |
| errs = multierror.Append(errs, fmt.Errorf("trustDomain: %v", err)) |
| } |
| for i, tda := range config.TrustDomainAliases { |
| if err := ValidateTrustDomain(tda); err != nil { |
| errs = multierror.Append(errs, fmt.Errorf("trustDomainAliases[%d], domain `%s` : %v", i, tda, err)) |
| } |
| } |
| return |
| } |
| |
| func validateServiceSettings(config *meshconfig.MeshConfig) (errs error) { |
| for sIndex, s := range config.ServiceSettings { |
| for _, h := range s.Hosts { |
| if err := ValidateWildcardDomain(h); err != nil { |
| errs = multierror.Append(errs, fmt.Errorf("serviceSettings[%d], host `%s`: %v", sIndex, h, err)) |
| } |
| } |
| } |
| return |
| } |
| |
| func validatePrivateKeyProvider(pkpConf *meshconfig.PrivateKeyProvider) error { |
| var errs error |
| if pkpConf.GetProvider() == nil { |
| errs = multierror.Append(errs, errors.New("private key provider confguration is required")) |
| } |
| |
| switch pkpConf.GetProvider().(type) { |
| case *meshconfig.PrivateKeyProvider_Cryptomb: |
| cryptomb := pkpConf.GetCryptomb() |
| if cryptomb == nil { |
| errs = multierror.Append(errs, errors.New("cryptomb confguration is required")) |
| } else { |
| pollDelay := cryptomb.GetPollDelay() |
| if pollDelay == nil { |
| errs = multierror.Append(errs, errors.New("pollDelay is required")) |
| } else if pollDelay.GetSeconds() == 0 && pollDelay.GetNanos() == 0 { |
| errs = multierror.Append(errs, errors.New("pollDelay must be non zero")) |
| } |
| } |
| default: |
| errs = multierror.Append(errs, errors.New("unknown private key provider")) |
| } |
| |
| return errs |
| } |
| |
| // ValidateMeshConfigProxyConfig checks that the mesh config is well-formed |
| func ValidateMeshConfigProxyConfig(config *meshconfig.ProxyConfig) (errs error) { |
| if config.ConfigPath == "" { |
| errs = multierror.Append(errs, errors.New("config path must be set")) |
| } |
| |
| if config.BinaryPath == "" { |
| errs = multierror.Append(errs, errors.New("binary path must be set")) |
| } |
| |
| clusterName := config.GetClusterName() |
| switch naming := clusterName.(type) { |
| case *meshconfig.ProxyConfig_ServiceCluster: |
| if naming.ServiceCluster == "" { |
| errs = multierror.Append(errs, errors.New("service cluster must be specified")) |
| } |
| case *meshconfig.ProxyConfig_TracingServiceName_: // intentionally left empty for now |
| default: |
| errs = multierror.Append(errs, errors.New("oneof service cluster or tracing service name must be specified")) |
| } |
| |
| if err := ValidateParentAndDrain(config.DrainDuration, config.ParentShutdownDuration); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid parent and drain time combination")) |
| } |
| |
| // discovery address is mandatory since mutual TLS relies on CDS. |
| // strictly speaking, proxies can operate without RDS/CDS and with hot restarts |
| // but that requires additional test validation |
| if config.DiscoveryAddress == "" { |
| errs = multierror.Append(errs, errors.New("discovery address must be set to the proxy discovery service")) |
| } else if err := ValidateProxyAddress(config.DiscoveryAddress); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid discovery address:")) |
| } |
| |
| if tracer := config.GetTracing().GetLightstep(); tracer != nil { |
| if err := ValidateLightstepCollector(tracer); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid lightstep config:")) |
| } |
| } |
| |
| if tracer := config.GetTracing().GetZipkin(); tracer != nil { |
| if err := ValidateZipkinCollector(tracer); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid zipkin config:")) |
| } |
| } |
| |
| if tracer := config.GetTracing().GetDatadog(); tracer != nil { |
| if err := ValidateDatadogCollector(tracer); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid datadog config:")) |
| } |
| } |
| |
| if tracer := config.GetTracing().GetTlsSettings(); tracer != nil { |
| if err := validateTLS(tracer); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid tracing TLS config:")) |
| } |
| } |
| |
| if tracerCustomTags := config.GetTracing().GetCustomTags(); tracerCustomTags != nil { |
| if err := validateCustomTags(tracerCustomTags); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid tracing custom tags:")) |
| } |
| } |
| |
| if config.StatsdUdpAddress != "" { |
| if err := ValidateProxyAddress(config.StatsdUdpAddress); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid statsd udp address %q:", config.StatsdUdpAddress))) |
| } |
| } |
| |
| // nolint: staticcheck |
| if config.EnvoyMetricsServiceAddress != "" { |
| if err := ValidateProxyAddress(config.EnvoyMetricsServiceAddress); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy metrics service address %q:", config.EnvoyMetricsServiceAddress))) |
| } else { |
| scope.Warnf("EnvoyMetricsServiceAddress is deprecated, use EnvoyMetricsService instead.") // nolint: stylecheck |
| } |
| } |
| |
| if config.EnvoyMetricsService != nil && config.EnvoyMetricsService.Address != "" { |
| if err := ValidateProxyAddress(config.EnvoyMetricsService.Address); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy metrics service address %q:", config.EnvoyMetricsService.Address))) |
| } |
| } |
| |
| if config.EnvoyAccessLogService != nil && config.EnvoyAccessLogService.Address != "" { |
| if err := ValidateProxyAddress(config.EnvoyAccessLogService.Address); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy access log service address %q:", config.EnvoyAccessLogService.Address))) |
| } |
| } |
| |
| if err := ValidatePort(int(config.ProxyAdminPort)); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid proxy admin port:")) |
| } |
| |
| if err := ValidateControlPlaneAuthPolicy(config.ControlPlaneAuthPolicy); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid authentication policy:")) |
| } |
| |
| if err := ValidatePort(int(config.StatusPort)); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid status port:")) |
| } |
| |
| if pkpConf := config.GetPrivateKeyProvider(); pkpConf != nil { |
| if err := validatePrivateKeyProvider(pkpConf); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, "invalid private key provider confguration:")) |
| } |
| } |
| |
| return |
| } |
| |
| func ValidateControlPlaneAuthPolicy(policy meshconfig.AuthenticationPolicy) error { |
| if policy == meshconfig.AuthenticationPolicy_NONE || policy == meshconfig.AuthenticationPolicy_MUTUAL_TLS { |
| return nil |
| } |
| return fmt.Errorf("unrecognized control plane auth policy %q", policy) |
| } |
| |
| func validateWorkloadSelector(selector *type_beta.WorkloadSelector) error { |
| var errs error |
| if selector != nil { |
| for k, v := range selector.MatchLabels { |
| if k == "" { |
| errs = appendErrors(errs, |
| fmt.Errorf("empty key is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) |
| } |
| if strings.Contains(k, "*") || strings.Contains(v, "*") { |
| errs = appendErrors(errs, |
| fmt.Errorf("wildcard is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) |
| } |
| } |
| } |
| |
| return errs |
| } |
| |
| // ValidateAuthorizationPolicy checks that AuthorizationPolicy is well-formed. |
| var ValidateAuthorizationPolicy = registerValidateFunc("ValidateAuthorizationPolicy", |
| func(cfg config.Config) (Warning, error) { |
| in, ok := cfg.Spec.(*security_beta.AuthorizationPolicy) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to AuthorizationPolicy") |
| } |
| |
| var errs error |
| if err := validateWorkloadSelector(in.Selector); err != nil { |
| errs = appendErrors(errs, err) |
| } |
| |
| if in.Action == security_beta.AuthorizationPolicy_CUSTOM { |
| if in.Rules == nil { |
| errs = appendErrors(errs, fmt.Errorf("CUSTOM action without `rules` is meaningless as it will never be triggered, "+ |
| "add an empty rule `{}` if you want it be triggered for every request")) |
| } else { |
| if in.GetProvider() == nil || in.GetProvider().GetName() == "" { |
| errs = appendErrors(errs, fmt.Errorf("`provider.name` must not be empty")) |
| } |
| } |
| // TODO(yangminzhu): Add support for more matching rules. |
| for _, rule := range in.GetRules() { |
| check := func(invalid bool, name string) error { |
| if invalid { |
| return fmt.Errorf("%s is currently not supported with CUSTOM action", name) |
| } |
| return nil |
| } |
| for _, from := range rule.GetFrom() { |
| if src := from.GetSource(); src != nil { |
| errs = appendErrors(errs, check(len(src.Namespaces) != 0, "From.Namespaces")) |
| errs = appendErrors(errs, check(len(src.NotNamespaces) != 0, "From.NotNamespaces")) |
| errs = appendErrors(errs, check(len(src.Principals) != 0, "From.Principals")) |
| errs = appendErrors(errs, check(len(src.NotPrincipals) != 0, "From.NotPrincipals")) |
| errs = appendErrors(errs, check(len(src.RequestPrincipals) != 0, "From.RequestPrincipals")) |
| errs = appendErrors(errs, check(len(src.NotRequestPrincipals) != 0, "From.NotRequestPrincipals")) |
| } |
| } |
| for _, when := range rule.GetWhen() { |
| errs = appendErrors(errs, check(when.Key == "source.namespace", when.Key)) |
| errs = appendErrors(errs, check(when.Key == "source.principal", when.Key)) |
| errs = appendErrors(errs, check(strings.HasPrefix(when.Key, "request.auth."), when.Key)) |
| } |
| } |
| } |
| if in.GetProvider() != nil && in.Action != security_beta.AuthorizationPolicy_CUSTOM { |
| errs = appendErrors(errs, fmt.Errorf("`provider` must not be with non CUSTOM action, found %s", in.Action)) |
| } |
| |
| if in.Action == security_beta.AuthorizationPolicy_DENY && in.Rules == nil { |
| errs = appendErrors(errs, fmt.Errorf("DENY action without `rules` is meaningless as it will never be triggered, "+ |
| "add an empty rule `{}` if you want it be triggered for every request")) |
| } |
| |
| for i, rule := range in.GetRules() { |
| if rule == nil { |
| errs = appendErrors(errs, fmt.Errorf("`rule` must not be nil, found at rule %d", i)) |
| continue |
| } |
| if rule.From != nil && len(rule.From) == 0 { |
| errs = appendErrors(errs, fmt.Errorf("`from` must not be empty, found at rule %d", i)) |
| } |
| for _, from := range rule.From { |
| if from == nil { |
| errs = appendErrors(errs, fmt.Errorf("`from` must not be nil, found at rule %d", i)) |
| continue |
| } |
| if from.Source == nil { |
| errs = appendErrors(errs, fmt.Errorf("`from.source` must not be nil, found at rule %d", i)) |
| } else { |
| src := from.Source |
| if len(src.Principals) == 0 && len(src.RequestPrincipals) == 0 && len(src.Namespaces) == 0 && len(src.IpBlocks) == 0 && |
| len(src.RemoteIpBlocks) == 0 && len(src.NotPrincipals) == 0 && len(src.NotRequestPrincipals) == 0 && len(src.NotNamespaces) == 0 && |
| len(src.NotIpBlocks) == 0 && len(src.NotRemoteIpBlocks) == 0 { |
| errs = appendErrors(errs, fmt.Errorf("`from.source` must not be empty, found at rule %d", i)) |
| } |
| errs = appendErrors(errs, security.ValidateIPs(from.Source.GetIpBlocks())) |
| errs = appendErrors(errs, security.ValidateIPs(from.Source.GetNotIpBlocks())) |
| errs = appendErrors(errs, security.ValidateIPs(from.Source.GetRemoteIpBlocks())) |
| errs = appendErrors(errs, security.ValidateIPs(from.Source.GetNotRemoteIpBlocks())) |
| errs = appendErrors(errs, security.CheckEmptyValues("Principals", src.Principals)) |
| errs = appendErrors(errs, security.CheckEmptyValues("RequestPrincipals", src.RequestPrincipals)) |
| errs = appendErrors(errs, security.CheckEmptyValues("Namespaces", src.Namespaces)) |
| errs = appendErrors(errs, security.CheckEmptyValues("IpBlocks", src.IpBlocks)) |
| errs = appendErrors(errs, security.CheckEmptyValues("RemoteIpBlocks", src.RemoteIpBlocks)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotPrincipals", src.NotPrincipals)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotRequestPrincipals", src.NotRequestPrincipals)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotNamespaces", src.NotNamespaces)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotIpBlocks", src.NotIpBlocks)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotRemoteIpBlocks", src.NotRemoteIpBlocks)) |
| } |
| } |
| if rule.To != nil && len(rule.To) == 0 { |
| errs = appendErrors(errs, fmt.Errorf("`to` must not be empty, found at rule %d", i)) |
| } |
| for _, to := range rule.To { |
| if to == nil { |
| errs = appendErrors(errs, fmt.Errorf("`to` must not be nil, found at rule %d", i)) |
| continue |
| } |
| if to.Operation == nil { |
| errs = appendErrors(errs, fmt.Errorf("`to.operation` must not be nil, found at rule %d", i)) |
| } else { |
| op := to.Operation |
| if len(op.Ports) == 0 && len(op.Methods) == 0 && len(op.Paths) == 0 && len(op.Hosts) == 0 && |
| len(op.NotPorts) == 0 && len(op.NotMethods) == 0 && len(op.NotPaths) == 0 && len(op.NotHosts) == 0 { |
| errs = appendErrors(errs, fmt.Errorf("`to.operation` must not be empty, found at rule %d", i)) |
| } |
| errs = appendErrors(errs, security.ValidatePorts(to.Operation.GetPorts())) |
| errs = appendErrors(errs, security.ValidatePorts(to.Operation.GetNotPorts())) |
| errs = appendErrors(errs, security.CheckEmptyValues("Ports", op.Ports)) |
| errs = appendErrors(errs, security.CheckEmptyValues("Methods", op.Methods)) |
| errs = appendErrors(errs, security.CheckEmptyValues("Paths", op.Paths)) |
| errs = appendErrors(errs, security.CheckEmptyValues("Hosts", op.Hosts)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotPorts", op.NotPorts)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotMethods", op.NotMethods)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotPaths", op.NotPaths)) |
| errs = appendErrors(errs, security.CheckEmptyValues("NotHosts", op.NotHosts)) |
| } |
| } |
| for _, condition := range rule.GetWhen() { |
| key := condition.GetKey() |
| if key == "" { |
| errs = appendErrors(errs, fmt.Errorf("`key` must not be empty")) |
| } else { |
| if len(condition.GetValues()) == 0 && len(condition.GetNotValues()) == 0 { |
| errs = appendErrors(errs, fmt.Errorf("at least one of `values` or `notValues` must be set for key %s", |
| key)) |
| } else { |
| if err := security.ValidateAttribute(key, condition.GetValues()); err != nil { |
| errs = appendErrors(errs, fmt.Errorf("invalid `value` for `key` %s: %v", key, err)) |
| } |
| if err := security.ValidateAttribute(key, condition.GetNotValues()); err != nil { |
| errs = appendErrors(errs, fmt.Errorf("invalid `notValue` for `key` %s: %v", key, err)) |
| } |
| } |
| } |
| } |
| } |
| return nil, multierror.Prefix(errs, fmt.Sprintf("invalid policy %s.%s:", cfg.Name, cfg.Namespace)) |
| }) |
| |
| // ValidateRequestAuthentication checks that request authentication spec is well-formed. |
| var ValidateRequestAuthentication = registerValidateFunc("ValidateRequestAuthentication", |
| func(cfg config.Config) (Warning, error) { |
| in, ok := cfg.Spec.(*security_beta.RequestAuthentication) |
| if !ok { |
| return nil, errors.New("cannot cast to RequestAuthentication") |
| } |
| |
| var errs error |
| errs = appendErrors(errs, validateWorkloadSelector(in.Selector)) |
| |
| for _, rule := range in.JwtRules { |
| errs = appendErrors(errs, validateJwtRule(rule)) |
| } |
| return nil, errs |
| }) |
| |
| func validateJwtRule(rule *security_beta.JWTRule) (errs error) { |
| if rule == nil { |
| return nil |
| } |
| if len(rule.Issuer) == 0 { |
| errs = multierror.Append(errs, errors.New("issuer must be set")) |
| } |
| for _, audience := range rule.Audiences { |
| if len(audience) == 0 { |
| errs = multierror.Append(errs, errors.New("audience must be non-empty string")) |
| } |
| } |
| |
| if len(rule.JwksUri) != 0 { |
| if _, err := security.ParseJwksURI(rule.JwksUri); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| } |
| |
| if rule.Jwks != "" { |
| _, err := jwk.Parse([]byte(rule.Jwks)) |
| if err != nil { |
| errs = multierror.Append(errs, fmt.Errorf("jwks parse error: %v", err)) |
| } |
| } |
| |
| for _, location := range rule.FromHeaders { |
| if location == nil { |
| errs = multierror.Append(errs, errors.New("location header name must be non-null")) |
| continue |
| } |
| if len(location.Name) == 0 { |
| errs = multierror.Append(errs, errors.New("location header name must be non-empty string")) |
| } |
| } |
| |
| for _, location := range rule.FromParams { |
| if len(location) == 0 { |
| errs = multierror.Append(errs, errors.New("location query must be non-empty string")) |
| } |
| } |
| return |
| } |
| |
| // ValidatePeerAuthentication checks that peer authentication spec is well-formed. |
| var ValidatePeerAuthentication = registerValidateFunc("ValidatePeerAuthentication", |
| func(cfg config.Config) (Warning, error) { |
| in, ok := cfg.Spec.(*security_beta.PeerAuthentication) |
| if !ok { |
| return nil, errors.New("cannot cast to PeerAuthentication") |
| } |
| |
| var errs error |
| emptySelector := in.Selector == nil || len(in.Selector.MatchLabels) == 0 |
| |
| if emptySelector && len(in.PortLevelMtls) != 0 { |
| errs = appendErrors(errs, |
| fmt.Errorf("mesh/namespace peer authentication cannot have port level mTLS")) |
| } |
| |
| if in.PortLevelMtls != nil && len(in.PortLevelMtls) == 0 { |
| errs = appendErrors(errs, |
| fmt.Errorf("port level mTLS, if defined, must have at least one element")) |
| } |
| |
| for port := range in.PortLevelMtls { |
| if port == 0 { |
| errs = appendErrors(errs, fmt.Errorf("port cannot be 0")) |
| } |
| } |
| |
| errs = appendErrors(errs, validateWorkloadSelector(in.Selector)) |
| |
| return nil, errs |
| }) |
| |
| // ValidateVirtualService checks that a v1alpha3 route rule is well-formed. |
| var ValidateVirtualService = registerValidateFunc("ValidateVirtualService", |
| func(cfg config.Config) (Warning, error) { |
| virtualService, ok := cfg.Spec.(*networking.VirtualService) |
| if !ok { |
| return nil, errors.New("cannot cast to virtual service") |
| } |
| errs := Validation{} |
| if len(virtualService.Hosts) == 0 { |
| // This must be delegate - enforce delegate validations. |
| if len(virtualService.Gateways) != 0 { |
| // meaningless to specify gateways in delegate |
| errs = appendValidation(errs, fmt.Errorf("delegate virtual service must have no gateways specified")) |
| } |
| if len(virtualService.Tls) != 0 { |
| // meaningless to specify tls in delegate, we donot support tls delegate |
| errs = appendValidation(errs, fmt.Errorf("delegate virtual service must have no tls route specified")) |
| } |
| if len(virtualService.Tcp) != 0 { |
| // meaningless to specify tls in delegate, we donot support tcp delegate |
| errs = appendValidation(errs, fmt.Errorf("delegate virtual service must have no tcp route specified")) |
| } |
| } |
| |
| appliesToMesh := false |
| appliesToGateway := false |
| if len(virtualService.Gateways) == 0 { |
| appliesToMesh = true |
| } else { |
| errs = appendValidation(errs, validateGatewayNames(virtualService.Gateways)) |
| for _, gatewayName := range virtualService.Gateways { |
| if gatewayName == constants.IstioMeshGateway { |
| appliesToMesh = true |
| } else { |
| appliesToGateway = true |
| } |
| } |
| } |
| |
| if !appliesToGateway { |
| validateJWTClaimRoute := func(headers map[string]*networking.StringMatch) { |
| for key := range headers { |
| if strings.HasPrefix(key, constant.HeaderJWTClaim) { |
| msg := fmt.Sprintf("JWT claim based routing (key: %s) is only supported for gateway, found no gateways: %v", key, virtualService.Gateways) |
| errs = appendValidation(errs, errors.New(msg)) |
| } |
| } |
| } |
| for _, http := range virtualService.GetHttp() { |
| for _, m := range http.GetMatch() { |
| validateJWTClaimRoute(m.GetHeaders()) |
| validateJWTClaimRoute(m.GetWithoutHeaders()) |
| } |
| } |
| } |
| |
| allHostsValid := true |
| for _, virtualHost := range virtualService.Hosts { |
| if err := ValidateWildcardDomain(virtualHost); err != nil { |
| ipAddr := net.ParseIP(virtualHost) // Could also be an IP |
| if ipAddr == nil { |
| errs = appendValidation(errs, err) |
| allHostsValid = false |
| } |
| } else if appliesToMesh && virtualHost == "*" { |
| errs = appendValidation(errs, fmt.Errorf("wildcard host * is not allowed for virtual services bound to the mesh gateway")) |
| allHostsValid = false |
| } |
| } |
| |
| // Check for duplicate hosts |
| // Duplicates include literal duplicates as well as wildcard duplicates |
| // E.g., *.foo.com, and *.com are duplicates in the same virtual service |
| if allHostsValid { |
| for i := 0; i < len(virtualService.Hosts); i++ { |
| hostI := host.Name(virtualService.Hosts[i]) |
| for j := i + 1; j < len(virtualService.Hosts); j++ { |
| hostJ := host.Name(virtualService.Hosts[j]) |
| if hostI.Matches(hostJ) { |
| errs = appendValidation(errs, fmt.Errorf("duplicate hosts in virtual service: %s & %s", hostI, hostJ)) |
| } |
| } |
| } |
| } |
| |
| if len(virtualService.Http) == 0 && len(virtualService.Tcp) == 0 && len(virtualService.Tls) == 0 { |
| errs = appendValidation(errs, errors.New("http, tcp or tls must be provided in virtual service")) |
| } |
| for _, httpRoute := range virtualService.Http { |
| if httpRoute == nil { |
| errs = appendValidation(errs, errors.New("http route may not be null")) |
| continue |
| } |
| errs = appendValidation(errs, validateHTTPRoute(httpRoute, len(virtualService.Hosts) == 0)) |
| } |
| for _, tlsRoute := range virtualService.Tls { |
| errs = appendValidation(errs, validateTLSRoute(tlsRoute, virtualService)) |
| } |
| for _, tcpRoute := range virtualService.Tcp { |
| errs = appendValidation(errs, validateTCPRoute(tcpRoute)) |
| } |
| |
| errs = appendValidation(errs, validateExportTo(cfg.Namespace, virtualService.ExportTo, false, false)) |
| |
| warnUnused := func(ruleno, reason string) { |
| errs = appendValidation(errs, WrapWarning(&AnalysisAwareError{ |
| Type: "VirtualServiceUnreachableRule", |
| Msg: fmt.Sprintf("virtualService rule %v not used (%s)", ruleno, reason), |
| Parameters: []interface{}{ruleno, reason}, |
| })) |
| } |
| warnIneffective := func(ruleno, matchno, dupno string) { |
| errs = appendValidation(errs, WrapWarning(&AnalysisAwareError{ |
| Type: "VirtualServiceIneffectiveMatch", |
| Msg: fmt.Sprintf("virtualService rule %v match %v is not used (duplicate/overlapping match in rule %v)", ruleno, matchno, dupno), |
| Parameters: []interface{}{ruleno, matchno, dupno}, |
| })) |
| } |
| |
| analyzeUnreachableHTTPRules(virtualService.Http, warnUnused, warnIneffective) |
| analyzeUnreachableTCPRules(virtualService.Tcp, warnUnused, warnIneffective) |
| analyzeUnreachableTLSRules(virtualService.Tls, warnUnused, warnIneffective) |
| |
| return errs.Unwrap() |
| }) |
| |
| func assignExactOrPrefix(exact, prefix string) string { |
| if exact != "" { |
| return matchExact + exact |
| } |
| if prefix != "" { |
| return matchPrefix + prefix |
| } |
| return "" |
| } |
| |
| // genMatchHTTPRoutes build the match rules into struct OverlappingMatchValidationForHTTPRoute |
| // based on particular HTTPMatchRequest, according to comments on https://github.com/istio/istio/pull/32701 |
| // only support Match's port, method, authority, headers, query params and nonheaders for now. |
| func genMatchHTTPRoutes(route *networking.HTTPRoute, match *networking.HTTPMatchRequest, |
| rulen, matchn int) (matchHTTPRoutes *OverlappingMatchValidationForHTTPRoute) { |
| // skip current match if no match field for current route |
| if match == nil { |
| return nil |
| } |
| // skip current match if no URI field |
| if match.Uri == nil { |
| return nil |
| } |
| // store all httproute with prefix match uri |
| tmpPrefix := match.Uri.GetPrefix() |
| if tmpPrefix != "" { |
| // set Method |
| methodExact := match.Method.GetExact() |
| methodPrefix := match.Method.GetPrefix() |
| methodMatch := assignExactOrPrefix(methodExact, methodPrefix) |
| // if no method information, it should be GET by default |
| if methodMatch == "" { |
| methodMatch = matchExact + "GET" |
| } |
| |
| // set Authority |
| authorityExact := match.Authority.GetExact() |
| authorityPrefix := match.Authority.GetPrefix() |
| authorityMatch := assignExactOrPrefix(authorityExact, authorityPrefix) |
| |
| // set Headers |
| headerMap := make(map[string]string) |
| for hkey, hvalue := range match.Headers { |
| hvalueExact := hvalue.GetExact() |
| hvaluePrefix := hvalue.GetPrefix() |
| hvalueMatch := assignExactOrPrefix(hvalueExact, hvaluePrefix) |
| headerMap[hkey] = hvalueMatch |
| } |
| |
| // set QueryParams |
| QPMap := make(map[string]string) |
| for qpkey, qpvalue := range match.QueryParams { |
| qpvalueExact := qpvalue.GetExact() |
| qpvaluePrefix := qpvalue.GetPrefix() |
| qpvalueMatch := assignExactOrPrefix(qpvalueExact, qpvaluePrefix) |
| QPMap[qpkey] = qpvalueMatch |
| } |
| |
| // set WithoutHeaders |
| noHeaderMap := make(map[string]string) |
| for nhkey, nhvalue := range match.WithoutHeaders { |
| nhvalueExact := nhvalue.GetExact() |
| nhvaluePrefix := nhvalue.GetPrefix() |
| nhvalueMatch := assignExactOrPrefix(nhvalueExact, nhvaluePrefix) |
| noHeaderMap[nhkey] = nhvalueMatch |
| } |
| |
| matchHTTPRoutes = &OverlappingMatchValidationForHTTPRoute{ |
| routeName(route, rulen), |
| requestName(match, matchn), |
| tmpPrefix, |
| match.Port, |
| methodMatch, |
| authorityMatch, |
| headerMap, |
| QPMap, |
| noHeaderMap, |
| } |
| return |
| } |
| return nil |
| } |
| |
| // coveredValidation validate the overlapping match between two instance of OverlappingMatchValidationForHTTPRoute |
| func coveredValidation(vA, vB *OverlappingMatchValidationForHTTPRoute) bool { |
| // check the URI overlapping match, such as vB.Prefix is '/debugs' and vA.Prefix is '/debug' |
| if strings.HasPrefix(vB.Prefix, vA.Prefix) { |
| // check the port field |
| if vB.MatchPort != vA.MatchPort { |
| return false |
| } |
| |
| // check the match method |
| if vA.MatchMethod != vB.MatchMethod { |
| if !strings.HasPrefix(vA.MatchMethod, vB.MatchMethod) { |
| return false |
| } |
| } |
| |
| // check the match authority |
| if vA.MatchAuthority != vB.MatchAuthority { |
| if !strings.HasPrefix(vA.MatchAuthority, vB.MatchAuthority) { |
| return false |
| } |
| } |
| |
| // check the match Headers |
| vAHeaderLen := len(vA.MatchHeaders) |
| vBHeaderLen := len(vB.MatchHeaders) |
| if vAHeaderLen != vBHeaderLen { |
| return false |
| } |
| for hdKey, hdValue := range vA.MatchHeaders { |
| vBhdValue, ok := vB.MatchHeaders[hdKey] |
| if !ok { |
| return false |
| } else if hdValue != vBhdValue { |
| if !strings.HasPrefix(hdValue, vBhdValue) { |
| return false |
| } |
| } |
| } |
| |
| // check the match QueryParams |
| vAQPLen := len(vA.MatchQueryParams) |
| vBQPLen := len(vB.MatchQueryParams) |
| if vAQPLen != vBQPLen { |
| return false |
| } |
| for qpKey, qpValue := range vA.MatchQueryParams { |
| vBqpValue, ok := vB.MatchQueryParams[qpKey] |
| if !ok { |
| return false |
| } else if qpValue != vBqpValue { |
| if !strings.HasPrefix(qpValue, vBqpValue) { |
| return false |
| } |
| } |
| } |
| |
| // check the match NonHeaders |
| vANonHDLen := len(vA.MatchNonHeaders) |
| vBNonHDLen := len(vB.MatchNonHeaders) |
| if vANonHDLen != vBNonHDLen { |
| return false |
| } |
| for nhKey, nhValue := range vA.MatchNonHeaders { |
| vBnhValue, ok := vB.MatchNonHeaders[nhKey] |
| if !ok { |
| return false |
| } else if nhValue != vBnhValue { |
| if !strings.HasPrefix(nhValue, vBnhValue) { |
| return false |
| } |
| } |
| } |
| } else { |
| // no URI overlapping match |
| return false |
| } |
| return true |
| } |
| |
| func analyzeUnreachableHTTPRules(routes []*networking.HTTPRoute, |
| reportUnreachable func(ruleno, reason string), reportIneffective func(ruleno, matchno, dupno string)) { |
| matchesEncountered := make(map[string]int) |
| emptyMatchEncountered := -1 |
| var matchHTTPRoutes []*OverlappingMatchValidationForHTTPRoute |
| for rulen, route := range routes { |
| if route == nil { |
| continue |
| } |
| if len(route.Match) == 0 { |
| if emptyMatchEncountered >= 0 { |
| reportUnreachable(routeName(route, rulen), "only the last rule can have no matches") |
| } |
| emptyMatchEncountered = rulen |
| continue |
| } |
| |
| duplicateMatches := 0 |
| for matchn, match := range route.Match { |
| dupn, ok := matchesEncountered[asJSON(match)] |
| if ok { |
| reportIneffective(routeName(route, rulen), requestName(match, matchn), routeName(routes[dupn], dupn)) |
| duplicateMatches++ |
| // no need to handle for totally duplicated match rules |
| continue |
| } |
| matchesEncountered[asJSON(match)] = rulen |
| // build the match rules into struct OverlappingMatchValidationForHTTPRoute based on current match |
| matchHTTPRoute := genMatchHTTPRoutes(route, match, rulen, matchn) |
| if matchHTTPRoute != nil { |
| matchHTTPRoutes = append(matchHTTPRoutes, matchHTTPRoute) |
| } |
| } |
| if duplicateMatches == len(route.Match) { |
| reportUnreachable(routeName(route, rulen), "all matches used by prior rules") |
| } |
| } |
| |
| // at least 2 prefix matched routes for overlapping match validation |
| if len(matchHTTPRoutes) > 1 { |
| // check the overlapping match from the first prefix information |
| for routeIndex, routePrefix := range matchHTTPRoutes { |
| for rIndex := routeIndex + 1; rIndex < len(matchHTTPRoutes); rIndex++ { |
| // exclude the duplicate-match cases which have been validated above |
| if strings.Compare(matchHTTPRoutes[rIndex].Prefix, routePrefix.Prefix) == 0 { |
| continue |
| } |
| // Validate former prefix match does not cover the latter one. |
| if coveredValidation(routePrefix, matchHTTPRoutes[rIndex]) { |
| prefixMatchA := matchHTTPRoutes[rIndex].MatchStr + " of prefix " + matchHTTPRoutes[rIndex].Prefix |
| prefixMatchB := routePrefix.MatchStr + " of prefix " + routePrefix.Prefix + " on " + routePrefix.RouteStr |
| reportIneffective(matchHTTPRoutes[rIndex].RouteStr, prefixMatchA, prefixMatchB) |
| } |
| } |
| } |
| } |
| } |
| |
| // NOTE: This method identical to analyzeUnreachableHTTPRules. |
| func analyzeUnreachableTCPRules(routes []*networking.TCPRoute, |
| reportUnreachable func(ruleno, reason string), reportIneffective func(ruleno, matchno, dupno string)) { |
| matchesEncountered := make(map[string]int) |
| emptyMatchEncountered := -1 |
| for rulen, route := range routes { |
| if route == nil { |
| continue |
| } |
| if len(route.Match) == 0 { |
| if emptyMatchEncountered >= 0 { |
| reportUnreachable(routeName(route, rulen), "only the last rule can have no matches") |
| } |
| emptyMatchEncountered = rulen |
| continue |
| } |
| |
| duplicateMatches := 0 |
| for matchn, match := range route.Match { |
| dupn, ok := matchesEncountered[asJSON(match)] |
| if ok { |
| reportIneffective(routeName(route, rulen), requestName(match, matchn), routeName(routes[dupn], dupn)) |
| duplicateMatches++ |
| } else { |
| matchesEncountered[asJSON(match)] = rulen |
| } |
| } |
| if duplicateMatches == len(route.Match) { |
| reportUnreachable(routeName(route, rulen), "all matches used by prior rules") |
| } |
| } |
| } |
| |
| // NOTE: This method identical to analyzeUnreachableHTTPRules. |
| func analyzeUnreachableTLSRules(routes []*networking.TLSRoute, |
| reportUnreachable func(ruleno, reason string), reportIneffective func(ruleno, matchno, dupno string)) { |
| matchesEncountered := make(map[string]int) |
| emptyMatchEncountered := -1 |
| for rulen, route := range routes { |
| if route == nil { |
| continue |
| } |
| if len(route.Match) == 0 { |
| if emptyMatchEncountered >= 0 { |
| reportUnreachable(routeName(route, rulen), "only the last rule can have no matches") |
| } |
| emptyMatchEncountered = rulen |
| continue |
| } |
| |
| duplicateMatches := 0 |
| for matchn, match := range route.Match { |
| dupn, ok := matchesEncountered[asJSON(match)] |
| if ok { |
| reportIneffective(routeName(route, rulen), requestName(match, matchn), routeName(routes[dupn], dupn)) |
| duplicateMatches++ |
| } else { |
| matchesEncountered[asJSON(match)] = rulen |
| } |
| } |
| if duplicateMatches == len(route.Match) { |
| reportUnreachable(routeName(route, rulen), "all matches used by prior rules") |
| } |
| } |
| } |
| |
| // asJSON() creates a JSON serialization of a match, to use for match comparison. We don't use the JSON itself. |
| func asJSON(data interface{}) string { |
| // Remove the name, so we can create a serialization that only includes traffic routing config |
| switch mr := data.(type) { |
| case *networking.HTTPMatchRequest: |
| if mr != nil && mr.Name != "" { |
| cl := &networking.HTTPMatchRequest{} |
| protomarshal.ShallowCopy(cl, mr) |
| cl.Name = "" |
| data = cl |
| } |
| } |
| |
| b, err := json.Marshal(data) |
| if err != nil { |
| return err.Error() |
| } |
| return string(b) |
| } |
| |
| func routeName(route interface{}, routen int) string { |
| switch r := route.(type) { |
| case *networking.HTTPRoute: |
| if r.Name != "" { |
| return fmt.Sprintf("%q", r.Name) |
| } |
| // TCP and TLS routes have no names |
| } |
| |
| return fmt.Sprintf("#%d", routen) |
| } |
| |
| func requestName(match interface{}, matchn int) string { |
| switch mr := match.(type) { |
| case *networking.HTTPMatchRequest: |
| if mr != nil && mr.Name != "" { |
| return fmt.Sprintf("%q", mr.Name) |
| } |
| // TCP and TLS matches have no names |
| } |
| |
| return fmt.Sprintf("#%d", matchn) |
| } |
| |
| func validateTLSRoute(tls *networking.TLSRoute, context *networking.VirtualService) (errs Validation) { |
| if tls == nil { |
| return |
| } |
| if len(tls.Match) == 0 { |
| errs = appendValidation(errs, errors.New("TLS route must have at least one match condition")) |
| } |
| for _, match := range tls.Match { |
| errs = appendValidation(errs, validateTLSMatch(match, context)) |
| } |
| if len(tls.Route) == 0 { |
| errs = appendValidation(errs, errors.New("TLS route is required")) |
| } |
| errs = appendValidation(errs, validateRouteDestinations(tls.Route)) |
| return errs |
| } |
| |
| func validateTLSMatch(match *networking.TLSMatchAttributes, context *networking.VirtualService) (errs Validation) { |
| if match == nil { |
| errs = appendValidation(errs, errors.New("TLS match may not be null")) |
| return |
| } |
| if len(match.SniHosts) == 0 { |
| errs = appendValidation(errs, fmt.Errorf("TLS match must have at least one SNI host")) |
| } else { |
| for _, sniHost := range match.SniHosts { |
| errs = appendValidation(errs, validateSniHost(sniHost, context)) |
| } |
| } |
| |
| for _, destinationSubnet := range match.DestinationSubnets { |
| errs = appendValidation(errs, ValidateIPSubnet(destinationSubnet)) |
| } |
| |
| if match.Port != 0 { |
| errs = appendValidation(errs, ValidatePort(int(match.Port))) |
| } |
| errs = appendValidation(errs, labels.Instance(match.SourceLabels).Validate()) |
| errs = appendValidation(errs, validateGatewayNames(match.Gateways)) |
| return |
| } |
| |
| func validateSniHost(sniHost string, context *networking.VirtualService) (errs Validation) { |
| if err := ValidateWildcardDomain(sniHost); err != nil { |
| ipAddr := net.ParseIP(sniHost) // Could also be an IP |
| if ipAddr != nil { |
| errs = appendValidation(errs, WrapWarning(fmt.Errorf("using an IP address (%q) goes against SNI spec and most clients do not support this", ipAddr))) |
| return |
| } |
| return appendValidation(errs, err) |
| } |
| sniHostname := host.Name(sniHost) |
| for _, hostname := range context.Hosts { |
| if sniHostname.SubsetOf(host.Name(hostname)) { |
| return |
| } |
| } |
| return appendValidation(errs, fmt.Errorf("SNI host %q is not a compatible subset of any of the virtual service hosts: [%s]", |
| sniHost, strings.Join(context.Hosts, ", "))) |
| } |
| |
| func validateTCPRoute(tcp *networking.TCPRoute) (errs error) { |
| if tcp == nil { |
| return nil |
| } |
| for _, match := range tcp.Match { |
| errs = appendErrors(errs, validateTCPMatch(match)) |
| } |
| if len(tcp.Route) == 0 { |
| errs = appendErrors(errs, errors.New("TCP route is required")) |
| } |
| errs = appendErrors(errs, validateRouteDestinations(tcp.Route)) |
| return |
| } |
| |
| func validateTCPMatch(match *networking.L4MatchAttributes) (errs error) { |
| if match == nil { |
| errs = multierror.Append(errs, errors.New("tcp match may not be nil")) |
| return |
| } |
| for _, destinationSubnet := range match.DestinationSubnets { |
| errs = appendErrors(errs, ValidateIPSubnet(destinationSubnet)) |
| } |
| if match.Port != 0 { |
| errs = appendErrors(errs, ValidatePort(int(match.Port))) |
| } |
| errs = appendErrors(errs, labels.Instance(match.SourceLabels).Validate()) |
| errs = appendErrors(errs, validateGatewayNames(match.Gateways)) |
| return |
| } |
| |
| func validateStringMatchRegexp(sm *networking.StringMatch, where string) error { |
| switch sm.GetMatchType().(type) { |
| case *networking.StringMatch_Regex: |
| default: |
| return nil |
| } |
| re := sm.GetRegex() |
| if re == "" { |
| return fmt.Errorf("%q: regex string match should not be empty", where) |
| } |
| |
| // Envoy enforces a re2.max_program_size.error_level re2 program size is not the same as length, |
| // but it is always *larger* than length. Because goland does not have a way to evaluate the |
| // program size, we approximate by the length. To ensure that a program that is smaller than 1024 |
| // length but larger than 1024 size does not enter the system, we program Envoy to allow very large |
| // regexs to avoid NACKs. See |
| // https://github.com/jpeach/snippets/blob/889fda84cc8713af09205438b33553eb69dd5355/re2sz.cc to |
| // evaluate program size. |
| if len(re) > 1024 { |
| return fmt.Errorf("%q: regex is too large, max length allowed is 1024", where) |
| } |
| |
| _, err := regexp.Compile(re) |
| if err == nil { |
| return nil |
| } |
| |
| return fmt.Errorf("%q: %w; Istio uses RE2 style regex-based match (https://github.com/google/re2/wiki/Syntax)", where, err) |
| } |
| |
| func validateGatewayNames(gatewayNames []string) (errs Validation) { |
| for _, gatewayName := range gatewayNames { |
| parts := strings.SplitN(gatewayName, "/", 2) |
| if len(parts) != 2 { |
| if strings.Contains(gatewayName, ".") { |
| // Legacy FQDN style |
| parts := strings.Split(gatewayName, ".") |
| recommended := fmt.Sprintf("%s/%s", parts[1], parts[0]) |
| errs = appendValidation(errs, WrapWarning(fmt.Errorf( |
| "using legacy gatewayName format %q; prefer the <namespace>/<name> format: %q", gatewayName, recommended))) |
| } |
| errs = appendValidation(errs, ValidateFQDN(gatewayName)) |
| return |
| } |
| |
| if len(parts[0]) == 0 || len(parts[1]) == 0 { |
| errs = appendValidation(errs, fmt.Errorf("config namespace and gateway name cannot be empty")) |
| } |
| |
| // namespace and name must be DNS labels |
| if !labels.IsDNS1123Label(parts[0]) { |
| errs = appendValidation(errs, fmt.Errorf("invalid value for namespace: %q", parts[0])) |
| } |
| |
| if !labels.IsDNS1123Label(parts[1]) { |
| errs = appendValidation(errs, fmt.Errorf("invalid value for gateway name: %q", parts[1])) |
| } |
| } |
| return |
| } |
| |
| func validateHTTPRouteDestinations(weights []*networking.HTTPRouteDestination) (errs error) { |
| var totalWeight int32 |
| for _, weight := range weights { |
| if weight == nil { |
| errs = multierror.Append(errs, errors.New("weight may not be nil")) |
| continue |
| } |
| if weight.Destination == nil { |
| errs = multierror.Append(errs, errors.New("destination is required")) |
| } |
| |
| // header manipulations |
| for name, val := range weight.Headers.GetRequest().GetAdd() { |
| errs = appendErrors(errs, ValidateHTTPHeaderWithHostOperationName(name)) |
| errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) |
| } |
| for name, val := range weight.Headers.GetRequest().GetSet() { |
| errs = appendErrors(errs, ValidateHTTPHeaderWithHostOperationName(name)) |
| errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) |
| } |
| for _, name := range weight.Headers.GetRequest().GetRemove() { |
| errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) |
| } |
| for name, val := range weight.Headers.GetResponse().GetAdd() { |
| errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) |
| errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) |
| } |
| for name, val := range weight.Headers.GetResponse().GetSet() { |
| errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) |
| errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) |
| } |
| for _, name := range weight.Headers.GetResponse().GetRemove() { |
| errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) |
| } |
| |
| errs = appendErrors(errs, validateDestination(weight.Destination)) |
| errs = appendErrors(errs, ValidatePercent(weight.Weight)) |
| totalWeight += weight.Weight |
| } |
| if len(weights) > 1 && totalWeight != 100 { |
| errs = appendErrors(errs, fmt.Errorf("total destination weight %v != 100", totalWeight)) |
| } |
| return |
| } |
| |
| func validateRouteDestinations(weights []*networking.RouteDestination) (errs error) { |
| var totalWeight int32 |
| for _, weight := range weights { |
| if weight == nil { |
| errs = multierror.Append(errs, errors.New("weight may not be nil")) |
| continue |
| } |
| if weight.Destination == nil { |
| errs = multierror.Append(errs, errors.New("destination is required")) |
| } |
| errs = appendErrors(errs, validateDestination(weight.Destination)) |
| errs = appendErrors(errs, ValidatePercent(weight.Weight)) |
| totalWeight += weight.Weight |
| } |
| if len(weights) > 1 && totalWeight != 100 { |
| errs = appendErrors(errs, fmt.Errorf("total destination weight %v != 100", totalWeight)) |
| } |
| return |
| } |
| |
| func validateCORSPolicy(policy *networking.CorsPolicy) (errs error) { |
| if policy == nil { |
| return |
| } |
| |
| for _, origin := range policy.AllowOrigins { |
| errs = appendErrors(errs, validateAllowOrigins(origin)) |
| } |
| |
| for _, method := range policy.AllowMethods { |
| errs = appendErrors(errs, validateHTTPMethod(method)) |
| } |
| |
| for _, name := range policy.AllowHeaders { |
| errs = appendErrors(errs, ValidateHTTPHeaderName(name)) |
| } |
| |
| for _, name := range policy.ExposeHeaders { |
| errs = appendErrors(errs, ValidateHTTPHeaderName(name)) |
| } |
| |
| if policy.MaxAge != nil { |
| errs = appendErrors(errs, ValidateDuration(policy.MaxAge)) |
| if policy.MaxAge.Nanos > 0 { |
| errs = multierror.Append(errs, errors.New("max_age duration is accurate only to seconds precision")) |
| } |
| } |
| |
| return |
| } |
| |
| func validateAllowOrigins(origin *networking.StringMatch) error { |
| var match string |
| switch origin.MatchType.(type) { |
| case *networking.StringMatch_Exact: |
| match = origin.GetExact() |
| case *networking.StringMatch_Prefix: |
| match = origin.GetPrefix() |
| case *networking.StringMatch_Regex: |
| match = origin.GetRegex() |
| } |
| if match == "" { |
| return fmt.Errorf("'%v' is not a valid match type for CORS allow origins", match) |
| } |
| return validateStringMatchRegexp(origin, "corsPolicy.allowOrigins") |
| } |
| |
| func validateHTTPMethod(method string) error { |
| if !supportedMethods[method] { |
| return fmt.Errorf("%q is not a supported HTTP method", method) |
| } |
| return nil |
| } |
| |
| func validateHTTPFaultInjection(fault *networking.HTTPFaultInjection) (errs error) { |
| if fault == nil { |
| return |
| } |
| |
| if fault.Abort == nil && fault.Delay == nil { |
| errs = multierror.Append(errs, errors.New("HTTP fault injection must have an abort and/or a delay")) |
| } |
| |
| errs = appendErrors(errs, validateHTTPFaultInjectionAbort(fault.Abort)) |
| errs = appendErrors(errs, validateHTTPFaultInjectionDelay(fault.Delay)) |
| |
| return |
| } |
| |
| func validateHTTPFaultInjectionAbort(abort *networking.HTTPFaultInjection_Abort) (errs error) { |
| if abort == nil { |
| return |
| } |
| |
| errs = appendErrors(errs, validatePercentage(abort.Percentage)) |
| |
| switch abort.ErrorType.(type) { |
| case *networking.HTTPFaultInjection_Abort_GrpcStatus: |
| // TODO: gRPC status validation |
| errs = multierror.Append(errs, errors.New("gRPC abort fault injection not supported yet")) |
| case *networking.HTTPFaultInjection_Abort_Http2Error: |
| // TODO: HTTP2 error validation |
| errs = multierror.Append(errs, errors.New("HTTP/2 abort fault injection not supported yet")) |
| case *networking.HTTPFaultInjection_Abort_HttpStatus: |
| errs = appendErrors(errs, validateHTTPStatus(abort.GetHttpStatus())) |
| } |
| |
| return |
| } |
| |
| func validateHTTPStatus(status int32) error { |
| if status < 200 || status > 600 { |
| return fmt.Errorf("HTTP status %d is not in range 200-599", status) |
| } |
| return nil |
| } |
| |
| func validateHTTPFaultInjectionDelay(delay *networking.HTTPFaultInjection_Delay) (errs error) { |
| if delay == nil { |
| return |
| } |
| |
| errs = appendErrors(errs, validatePercentage(delay.Percentage)) |
| |
| switch v := delay.HttpDelayType.(type) { |
| case *networking.HTTPFaultInjection_Delay_FixedDelay: |
| errs = appendErrors(errs, ValidateDuration(v.FixedDelay)) |
| case *networking.HTTPFaultInjection_Delay_ExponentialDelay: |
| errs = appendErrors(errs, ValidateDuration(v.ExponentialDelay)) |
| errs = multierror.Append(errs, fmt.Errorf("exponentialDelay not supported yet")) |
| } |
| |
| return |
| } |
| |
| func validateDestination(destination *networking.Destination) (errs error) { |
| if destination == nil { |
| return |
| } |
| |
| hostname := destination.Host |
| if hostname == "*" { |
| errs = appendErrors(errs, fmt.Errorf("invalid destination host %s", hostname)) |
| } else { |
| errs = appendErrors(errs, ValidateWildcardDomain(hostname)) |
| } |
| if destination.Subset != "" { |
| errs = appendErrors(errs, validateSubsetName(destination.Subset)) |
| } |
| if destination.Port != nil { |
| errs = appendErrors(errs, validatePortSelector(destination.Port)) |
| } |
| |
| return |
| } |
| |
| func validateSubsetName(name string) error { |
| if len(name) == 0 { |
| return fmt.Errorf("subset name cannot be empty") |
| } |
| if !labels.IsDNS1123Label(name) { |
| return fmt.Errorf("subset name is invalid: %s", name) |
| } |
| return nil |
| } |
| |
| func validatePortSelector(selector *networking.PortSelector) (errs error) { |
| if selector == nil { |
| return nil |
| } |
| |
| // port must be a number |
| number := int(selector.GetNumber()) |
| errs = appendErrors(errs, ValidatePort(number)) |
| return |
| } |
| |
| func validateHTTPRetry(retries *networking.HTTPRetry) (errs error) { |
| if retries == nil { |
| return |
| } |
| |
| if retries.Attempts < 0 { |
| errs = multierror.Append(errs, errors.New("attempts cannot be negative")) |
| } |
| |
| if retries.Attempts == 0 && (retries.PerTryTimeout != nil || retries.RetryOn != "" || retries.RetryRemoteLocalities != nil) { |
| errs = appendErrors(errs, errors.New("http retry policy configured when attempts are set to 0 (disabled)")) |
| } |
| |
| if retries.PerTryTimeout != nil { |
| errs = appendErrors(errs, ValidateDuration(retries.PerTryTimeout)) |
| } |
| if retries.RetryOn != "" { |
| retryOnPolicies := strings.Split(retries.RetryOn, ",") |
| for _, policy := range retryOnPolicies { |
| // Try converting it to an integer to see if it's a valid HTTP status code. |
| i, _ := strconv.Atoi(policy) |
| |
| if http.StatusText(i) == "" && !supportedRetryOnPolicies[policy] { |
| errs = appendErrors(errs, fmt.Errorf("%q is not a valid retryOn policy", policy)) |
| } |
| } |
| } |
| |
| return |
| } |
| |
| func validateHTTPRedirect(redirect *networking.HTTPRedirect) error { |
| if redirect == nil { |
| return nil |
| } |
| if redirect.Uri == "" && redirect.Authority == "" && redirect.RedirectPort == nil && redirect.Scheme == "" { |
| return errors.New("redirect must specify URI, authority, scheme, or port") |
| } |
| |
| if redirect.RedirectCode != 0 { |
| if redirect.RedirectCode < 300 || redirect.RedirectCode > 399 { |
| return fmt.Errorf("%d is not a valid redirect code, must be 3xx", redirect.RedirectCode) |
| } |
| } |
| if redirect.Scheme != "" && redirect.Scheme != "http" && redirect.Scheme != "https" { |
| return fmt.Errorf(`invalid redirect scheme, must be "http" or "https"`) |
| } |
| if redirect.GetPort() > 0 { |
| if err := ValidatePort(int(redirect.GetPort())); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func validateHTTPRewrite(rewrite *networking.HTTPRewrite) error { |
| if rewrite != nil && rewrite.Uri == "" && rewrite.Authority == "" { |
| return errors.New("rewrite must specify URI, authority, or both") |
| } |
| return nil |
| } |
| |
| // ValidateWorkloadEntry validates a workload entry. |
| var ValidateWorkloadEntry = registerValidateFunc("ValidateWorkloadEntry", |
| func(cfg config.Config) (Warning, error) { |
| we, ok := cfg.Spec.(*networking.WorkloadEntry) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to workload entry") |
| } |
| return validateWorkloadEntry(we) |
| }) |
| |
| func validateWorkloadEntry(we *networking.WorkloadEntry) (Warning, error) { |
| errs := Validation{} |
| if we.Address == "" { |
| return nil, fmt.Errorf("address must be set") |
| } |
| // Since we don't know if its meant to be DNS or STATIC type without association with a ServiceEntry, |
| // check based on content and try validations. |
| addr := we.Address |
| // First check if it is a Unix endpoint - this will be specified for STATIC. |
| if strings.HasPrefix(we.Address, UnixAddressPrefix) { |
| errs = appendValidation(errs, ValidateUnixAddress(strings.TrimPrefix(addr, UnixAddressPrefix))) |
| if len(we.Ports) != 0 { |
| errs = appendValidation(errs, fmt.Errorf("unix endpoint %s must not include ports", we.Address)) |
| } |
| } else { |
| // This could be IP (in STATIC resolution) or DNS host name (for DNS). |
| ipAddr := net.ParseIP(we.Address) |
| if ipAddr == nil { |
| if err := ValidateFQDN(we.Address); err != nil { // Otherwise could be an FQDN |
| errs = appendValidation(errs, |
| fmt.Errorf("endpoint address %q is not a valid FQDN or an IP address", we.Address)) |
| } |
| } |
| } |
| |
| errs = appendValidation(errs, |
| labels.Instance(we.Labels).Validate()) |
| for name, port := range we.Ports { |
| // TODO: Validate port is part of Service Port - which is tricky to validate with out service entry. |
| errs = appendValidation(errs, |
| ValidatePortName(name), |
| ValidatePort(int(port))) |
| } |
| return errs.Unwrap() |
| } |
| |
| // ValidateWorkloadGroup validates a workload group. |
| var ValidateWorkloadGroup = registerValidateFunc("ValidateWorkloadGroup", |
| func(cfg config.Config) (warnings Warning, errs error) { |
| wg, ok := cfg.Spec.(*networking.WorkloadGroup) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to workload entry") |
| } |
| |
| if wg.Template == nil { |
| return nil, fmt.Errorf("template is required") |
| } |
| // Do not call validateWorkloadEntry. Some fields, such as address, are required in WorkloadEntry |
| // but not in the template since they are auto populated |
| |
| if wg.Metadata != nil { |
| if err := labels.Instance(wg.Metadata.Labels).Validate(); err != nil { |
| return nil, fmt.Errorf("invalid labels: %v", err) |
| } |
| } |
| |
| return nil, validateReadinessProbe(wg.Probe) |
| }) |
| |
| func validateReadinessProbe(probe *networking.ReadinessProbe) (errs error) { |
| if probe == nil { |
| return nil |
| } |
| if probe.PeriodSeconds < 0 { |
| errs = appendErrors(errs, fmt.Errorf("periodSeconds must be non-negative")) |
| } |
| if probe.InitialDelaySeconds < 0 { |
| errs = appendErrors(errs, fmt.Errorf("initialDelaySeconds must be non-negative")) |
| } |
| if probe.TimeoutSeconds < 0 { |
| errs = appendErrors(errs, fmt.Errorf("timeoutSeconds must be non-negative")) |
| } |
| if probe.SuccessThreshold < 0 { |
| errs = appendErrors(errs, fmt.Errorf("successThreshold must be non-negative")) |
| } |
| if probe.FailureThreshold < 0 { |
| errs = appendErrors(errs, fmt.Errorf("failureThreshold must be non-negative")) |
| } |
| switch m := probe.HealthCheckMethod.(type) { |
| case *networking.ReadinessProbe_HttpGet: |
| h := m.HttpGet |
| if h == nil { |
| errs = appendErrors(errs, fmt.Errorf("httpGet may not be nil")) |
| break |
| } |
| errs = appendErrors(errs, ValidatePort(int(h.Port))) |
| if h.Scheme != "" && h.Scheme != string(apimirror.URISchemeHTTPS) && h.Scheme != string(apimirror.URISchemeHTTP) { |
| errs = appendErrors(errs, fmt.Errorf(`httpGet.scheme must be one of "http", "https"`)) |
| } |
| for _, header := range h.HttpHeaders { |
| if header == nil { |
| errs = appendErrors(errs, fmt.Errorf("invalid nil header")) |
| continue |
| } |
| errs = appendErrors(errs, ValidateHTTPHeaderName(header.Name)) |
| } |
| case *networking.ReadinessProbe_TcpSocket: |
| h := m.TcpSocket |
| if h == nil { |
| errs = appendErrors(errs, fmt.Errorf("tcpSocket may not be nil")) |
| break |
| } |
| errs = appendErrors(errs, ValidatePort(int(h.Port))) |
| case *networking.ReadinessProbe_Exec: |
| h := m.Exec |
| if h == nil { |
| errs = appendErrors(errs, fmt.Errorf("exec may not be nil")) |
| break |
| } |
| if len(h.Command) == 0 { |
| errs = appendErrors(errs, fmt.Errorf("exec.command is required")) |
| } |
| default: |
| errs = appendErrors(errs, fmt.Errorf("unknown health check method %T", m)) |
| } |
| return errs |
| } |
| |
| // ValidateServiceEntry validates a service entry. |
| var ValidateServiceEntry = registerValidateFunc("ValidateServiceEntry", |
| func(cfg config.Config) (Warning, error) { |
| serviceEntry, ok := cfg.Spec.(*networking.ServiceEntry) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to service entry") |
| } |
| |
| if err := validateAlphaWorkloadSelector(serviceEntry.WorkloadSelector); err != nil { |
| return nil, err |
| } |
| |
| errs := Validation{} |
| |
| if serviceEntry.WorkloadSelector != nil && serviceEntry.Endpoints != nil { |
| errs = appendValidation(errs, fmt.Errorf("only one of WorkloadSelector or Endpoints is allowed in Service Entry")) |
| } |
| |
| if len(serviceEntry.Hosts) == 0 { |
| errs = appendValidation(errs, fmt.Errorf("service entry must have at least one host")) |
| } |
| for _, hostname := range serviceEntry.Hosts { |
| // Full wildcard is not allowed in the service entry. |
| if hostname == "*" { |
| errs = appendValidation(errs, fmt.Errorf("invalid host %s", hostname)) |
| } else { |
| errs = appendValidation(errs, ValidateWildcardDomain(hostname)) |
| } |
| } |
| |
| cidrFound := false |
| for _, address := range serviceEntry.Addresses { |
| cidrFound = cidrFound || strings.Contains(address, "/") |
| errs = appendValidation(errs, ValidateIPSubnet(address)) |
| } |
| |
| if cidrFound { |
| if serviceEntry.Resolution != networking.ServiceEntry_NONE && serviceEntry.Resolution != networking.ServiceEntry_STATIC { |
| errs = appendValidation(errs, fmt.Errorf("CIDR addresses are allowed only for NONE/STATIC resolution types")) |
| } |
| } |
| |
| servicePortNumbers := make(map[uint32]bool) |
| servicePorts := make(map[string]bool, len(serviceEntry.Ports)) |
| for _, port := range serviceEntry.Ports { |
| if port == nil { |
| errs = appendValidation(errs, fmt.Errorf("service entry port may not be null")) |
| continue |
| } |
| if servicePorts[port.Name] { |
| errs = appendValidation(errs, fmt.Errorf("service entry port name %q already defined", port.Name)) |
| } |
| servicePorts[port.Name] = true |
| if servicePortNumbers[port.Number] { |
| errs = appendValidation(errs, fmt.Errorf("service entry port %d already defined", port.Number)) |
| } |
| servicePortNumbers[port.Number] = true |
| if port.TargetPort != 0 { |
| errs = appendValidation(errs, ValidatePort(int(port.TargetPort))) |
| } |
| errs = appendValidation(errs, |
| ValidatePortName(port.Name), |
| ValidateProtocol(port.Protocol), |
| ValidatePort(int(port.Number))) |
| } |
| |
| switch serviceEntry.Resolution { |
| case networking.ServiceEntry_NONE: |
| if len(serviceEntry.Endpoints) != 0 { |
| errs = appendValidation(errs, fmt.Errorf("no endpoints should be provided for resolution type none")) |
| } |
| case networking.ServiceEntry_STATIC: |
| unixEndpoint := false |
| for _, endpoint := range serviceEntry.Endpoints { |
| addr := endpoint.GetAddress() |
| if strings.HasPrefix(addr, UnixAddressPrefix) { |
| unixEndpoint = true |
| errs = appendValidation(errs, ValidateUnixAddress(strings.TrimPrefix(addr, UnixAddressPrefix))) |
| if len(endpoint.Ports) != 0 { |
| errs = appendValidation(errs, fmt.Errorf("unix endpoint %s must not include ports", addr)) |
| } |
| } else { |
| errs = appendValidation(errs, ValidateIPAddress(addr)) |
| |
| for name, port := range endpoint.Ports { |
| if !servicePorts[name] { |
| errs = appendValidation(errs, fmt.Errorf("endpoint port %v is not defined by the service entry", port)) |
| } |
| } |
| } |
| errs = appendValidation(errs, labels.Instance(endpoint.Labels).Validate()) |
| } |
| if unixEndpoint && len(serviceEntry.Ports) != 1 { |
| errs = appendValidation(errs, errors.New("exactly 1 service port required for unix endpoints")) |
| } |
| case networking.ServiceEntry_DNS, networking.ServiceEntry_DNS_ROUND_ROBIN: |
| if len(serviceEntry.Endpoints) == 0 { |
| for _, hostname := range serviceEntry.Hosts { |
| if err := ValidateFQDN(hostname); err != nil { |
| errs = appendValidation(errs, |
| fmt.Errorf("hosts must be FQDN if no endpoints are provided for resolution mode %s", serviceEntry.Resolution)) |
| } |
| } |
| } |
| |
| for _, endpoint := range serviceEntry.Endpoints { |
| ipAddr := net.ParseIP(endpoint.Address) // Typically it is an IP address |
| if ipAddr == nil { |
| if err := ValidateFQDN(endpoint.Address); err != nil { // Otherwise could be an FQDN |
| errs = appendValidation(errs, |
| fmt.Errorf("endpoint address %q is not a valid FQDN or an IP address", endpoint.Address)) |
| } |
| } |
| errs = appendValidation(errs, |
| labels.Instance(endpoint.Labels).Validate()) |
| for name, port := range endpoint.Ports { |
| if !servicePorts[name] { |
| errs = appendValidation(errs, fmt.Errorf("endpoint port %v is not defined by the service entry", port)) |
| } |
| errs = appendValidation(errs, |
| ValidatePortName(name), |
| ValidatePort(int(port))) |
| } |
| } |
| if len(serviceEntry.Addresses) > 0 { |
| for _, port := range serviceEntry.Ports { |
| p := protocol.Parse(port.Protocol) |
| if p.IsTCP() { |
| if len(serviceEntry.Hosts) > 1 { |
| // TODO: prevent this invalid setting, maybe in 1.11+ |
| errs = appendValidation(errs, WrapWarning(fmt.Errorf("service entry can not have more than one host specified "+ |
| "simultaneously with address and tcp port"))) |
| } |
| break |
| } |
| } |
| } |
| default: |
| errs = appendValidation(errs, fmt.Errorf("unsupported resolution type %s", |
| networking.ServiceEntry_Resolution_name[int32(serviceEntry.Resolution)])) |
| } |
| |
| // multiple hosts and TCP is invalid unless the resolution type is NONE. |
| // depending on the protocol, we can differentiate between hosts when proxying: |
| // - with HTTP, the authority header can be used |
| // - with HTTPS/TLS with SNI, the ServerName can be used |
| // however, for plain TCP there is no way to differentiate between the |
| // hosts so we consider it invalid, unless the resolution type is NONE |
| // (because the hosts are ignored). |
| if serviceEntry.Resolution != networking.ServiceEntry_NONE && len(serviceEntry.Hosts) > 1 { |
| for _, port := range serviceEntry.Ports { |
| p := protocol.Parse(port.Protocol) |
| if !p.IsHTTP() && !p.IsTLS() { |
| errs = appendValidation(errs, fmt.Errorf("multiple hosts provided with non-HTTP, non-TLS ports")) |
| break |
| } |
| } |
| } |
| |
| errs = appendValidation(errs, validateExportTo(cfg.Namespace, serviceEntry.ExportTo, true, false)) |
| return errs.Unwrap() |
| }) |
| |
| // ValidatePortName validates a port name to DNS-1123 |
| func ValidatePortName(name string) error { |
| if !labels.IsDNS1123Label(name) { |
| return fmt.Errorf("invalid port name: %s", name) |
| } |
| return nil |
| } |
| |
| // ValidateProtocol validates a portocol name is known |
| func ValidateProtocol(protocolStr string) error { |
| // Empty string is used for protocol sniffing. |
| if protocolStr != "" && protocol.Parse(protocolStr) == protocol.Unsupported { |
| return fmt.Errorf("unsupported protocol: %s", protocolStr) |
| } |
| return nil |
| } |
| |
| // wrapper around multierror.Append that enforces the invariant that if all input errors are nil, the output |
| // error is nil (allowing validation without branching). |
| func appendValidation(v Validation, vs ...error) Validation { |
| appendError := func(err, err2 error) error { |
| if err == nil { |
| return err2 |
| } else if err2 == nil { |
| return err |
| } |
| return multierror.Append(err, err2) |
| } |
| |
| for _, nv := range vs { |
| switch t := nv.(type) { |
| case Validation: |
| v.Err = appendError(v.Err, t.Err) |
| v.Warning = appendError(v.Warning, t.Warning) |
| default: |
| v.Err = appendError(v.Err, t) |
| } |
| } |
| return v |
| } |
| |
| // appendErrorf appends a formatted error string |
| // nolint: unparam |
| func appendErrorf(v Validation, format string, a ...interface{}) Validation { |
| return appendValidation(v, fmt.Errorf(format, a...)) |
| } |
| |
| // appendWarningf appends a formatted warning string |
| // nolint: unparam |
| func appendWarningf(v Validation, format string, a ...interface{}) Validation { |
| return appendValidation(v, Warningf(format, a...)) |
| } |
| |
| // wrapper around multierror.Append that enforces the invariant that if all input errors are nil, the output |
| // error is nil (allowing validation without branching). |
| func appendErrors(err error, errs ...error) error { |
| appendError := func(err, err2 error) error { |
| if err == nil { |
| return err2 |
| } else if err2 == nil { |
| return err |
| } |
| return multierror.Append(err, err2) |
| } |
| |
| for _, err2 := range errs { |
| switch t := err2.(type) { |
| case Validation: |
| err = appendError(err, t.Err) |
| default: |
| err = appendError(err, err2) |
| } |
| } |
| return err |
| } |
| |
| // validateLocalityLbSetting checks the LocalityLbSetting of MeshConfig |
| func validateLocalityLbSetting(lb *networking.LocalityLoadBalancerSetting) error { |
| if lb == nil { |
| return nil |
| } |
| |
| if len(lb.GetDistribute()) > 0 && len(lb.GetFailover()) > 0 { |
| return fmt.Errorf("can not simultaneously specify 'distribute' and 'failover'") |
| } |
| |
| srcLocalities := make([]string, 0, len(lb.GetDistribute())) |
| for _, locality := range lb.GetDistribute() { |
| srcLocalities = append(srcLocalities, locality.From) |
| var totalWeight uint32 |
| destLocalities := make([]string, 0) |
| for loc, weight := range locality.To { |
| destLocalities = append(destLocalities, loc) |
| if weight <= 0 || weight > 100 { |
| return fmt.Errorf("locality weight must be in range [1, 100]") |
| } |
| totalWeight += weight |
| } |
| if totalWeight != 100 { |
| return fmt.Errorf("total locality weight %v != 100", totalWeight) |
| } |
| if err := validateLocalities(destLocalities); err != nil { |
| return err |
| } |
| } |
| |
| if err := validateLocalities(srcLocalities); err != nil { |
| return err |
| } |
| |
| for _, failover := range lb.GetFailover() { |
| if failover.From == failover.To { |
| return fmt.Errorf("locality lb failover settings must specify different regions") |
| } |
| if strings.Contains(failover.From, "/") || strings.Contains(failover.To, "/") { |
| return fmt.Errorf("locality lb failover only specify region") |
| } |
| if strings.Contains(failover.To, "*") || strings.Contains(failover.From, "*") { |
| return fmt.Errorf("locality lb failover region should not contain '*' wildcard") |
| } |
| } |
| |
| return nil |
| } |
| |
| func validateLocalities(localities []string) error { |
| regionZoneSubZoneMap := map[string]map[string]map[string]bool{} |
| for _, locality := range localities { |
| if n := strings.Count(locality, "*"); n > 0 { |
| if n > 1 || !strings.HasSuffix(locality, "*") { |
| return fmt.Errorf("locality %s wildcard '*' number can not exceed 1 and must be in the end", locality) |
| } |
| } |
| if _, exist := regionZoneSubZoneMap["*"]; exist { |
| return fmt.Errorf("locality %s overlap with previous specified ones", locality) |
| } |
| |
| region, zone, subZone, localityIndex, err := getLocalityParam(locality) |
| if err != nil { |
| return fmt.Errorf("locality %s must not contain empty region/zone/subzone info", locality) |
| } |
| |
| switch localityIndex { |
| case regionIndex: |
| if _, exist := regionZoneSubZoneMap[region]; exist { |
| return fmt.Errorf("locality %s overlap with previous specified ones", locality) |
| } |
| regionZoneSubZoneMap[region] = map[string]map[string]bool{"*": {"*": true}} |
| case zoneIndex: |
| if _, exist := regionZoneSubZoneMap[region]; exist { |
| if _, exist := regionZoneSubZoneMap[region]["*"]; exist { |
| return fmt.Errorf("locality %s overlap with previous specified ones", locality) |
| } |
| if _, exist := regionZoneSubZoneMap[region][zone]; exist { |
| return fmt.Errorf("locality %s overlap with previous specified ones", locality) |
| } |
| regionZoneSubZoneMap[region][zone] = map[string]bool{"*": true} |
| } else { |
| regionZoneSubZoneMap[region] = map[string]map[string]bool{zone: {"*": true}} |
| } |
| case subZoneIndex: |
| if _, exist := regionZoneSubZoneMap[region]; exist { |
| if _, exist := regionZoneSubZoneMap[region]["*"]; exist { |
| return fmt.Errorf("locality %s overlap with previous specified ones", locality) |
| } |
| if _, exist := regionZoneSubZoneMap[region][zone]; exist { |
| if regionZoneSubZoneMap[region][zone]["*"] { |
| return fmt.Errorf("locality %s overlap with previous specified ones", locality) |
| } |
| if regionZoneSubZoneMap[region][zone][subZone] { |
| return fmt.Errorf("locality %s overlap with previous specified ones", locality) |
| } |
| regionZoneSubZoneMap[region][zone][subZone] = true |
| } else { |
| regionZoneSubZoneMap[region][zone] = map[string]bool{subZone: true} |
| } |
| } else { |
| regionZoneSubZoneMap[region] = map[string]map[string]bool{zone: {subZone: true}} |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func getLocalityParam(locality string) (string, string, string, int, error) { |
| var region, zone, subZone string |
| items := strings.SplitN(locality, "/", 3) |
| for i, item := range items { |
| if item == "" { |
| return "", "", "", -1, errors.New("item is nil") |
| } |
| switch i { |
| case regionIndex: |
| region = items[i] |
| case zoneIndex: |
| zone = items[i] |
| case subZoneIndex: |
| subZone = items[i] |
| } |
| } |
| return region, zone, subZone, len(items) - 1, nil |
| } |
| |
| // ValidateMeshNetworks validates meshnetworks. |
| func ValidateMeshNetworks(meshnetworks *meshconfig.MeshNetworks) (errs error) { |
| // TODO validate using the same gateway on multiple networks? |
| for name, network := range meshnetworks.Networks { |
| if err := validateNetwork(network); err != nil { |
| errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid network %v:", name))) |
| } |
| } |
| return |
| } |
| |
| func validateNetwork(network *meshconfig.Network) (errs error) { |
| for _, n := range network.Endpoints { |
| switch e := n.Ne.(type) { |
| case *meshconfig.Network_NetworkEndpoints_FromCidr: |
| if err := ValidateIPSubnet(e.FromCidr); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| case *meshconfig.Network_NetworkEndpoints_FromRegistry: |
| if ok := labels.IsDNS1123Label(e.FromRegistry); !ok { |
| errs = multierror.Append(errs, fmt.Errorf("invalid registry name: %v", e.FromRegistry)) |
| } |
| } |
| } |
| for _, n := range network.Gateways { |
| switch g := n.Gw.(type) { |
| case *meshconfig.Network_IstioNetworkGateway_RegistryServiceName: |
| if err := ValidateFQDN(g.RegistryServiceName); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| case *meshconfig.Network_IstioNetworkGateway_Address: |
| if ipErr := ValidateIPAddress(g.Address); ipErr != nil { |
| if !features.ResolveHostnameGateways { |
| err := fmt.Errorf("%v (hostname is allowed if RESOLVE_HOSTNAME_GATEWAYS is enabled)", ipErr) |
| errs = multierror.Append(errs, err) |
| } else if fqdnErr := ValidateFQDN(g.Address); fqdnErr != nil { |
| errs = multierror.Append(fmt.Errorf("%v is not a valid IP address or DNS name", g.Address)) |
| } |
| } |
| } |
| if err := ValidatePort(int(n.Port)); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| } |
| return |
| } |
| |
| func (aae *AnalysisAwareError) Error() string { |
| return aae.Msg |
| } |
| |
| // ValidateProxyConfig validates a ProxyConfig CR (as opposed to the MeshConfig field). |
| var ValidateProxyConfig = registerValidateFunc("ValidateProxyConfig", |
| func(cfg config.Config) (Warning, error) { |
| spec, ok := cfg.Spec.(*networkingv1beta1.ProxyConfig) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to proxyconfig") |
| } |
| |
| errs := Validation{} |
| |
| errs = appendValidation(errs, |
| validateWorkloadSelector(spec.Selector), |
| validateConcurrency(spec.Concurrency.GetValue()), |
| ) |
| return errs.Unwrap() |
| }) |
| |
| func validateConcurrency(concurrency int32) (v Validation) { |
| if concurrency < 0 { |
| v = appendErrorf(v, "concurrency must be greater than or equal to 0") |
| } |
| return |
| } |
| |
| // ValidateTelemetry validates a Telemetry. |
| var ValidateTelemetry = registerValidateFunc("ValidateTelemetry", |
| func(cfg config.Config) (Warning, error) { |
| spec, ok := cfg.Spec.(*telemetry.Telemetry) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to telemetry") |
| } |
| |
| errs := Validation{} |
| |
| errs = appendValidation(errs, |
| validateWorkloadSelector(spec.Selector), |
| validateTelemetryMetrics(spec.Metrics), |
| validateTelemetryTracing(spec.Tracing), |
| validateTelemetryAccessLogging(spec.AccessLogging), |
| ) |
| return errs.Unwrap() |
| }) |
| |
| func validateTelemetryAccessLogging(logging []*telemetry.AccessLogging) (v Validation) { |
| if len(logging) > 1 { |
| v = appendWarningf(v, "multiple accessLogging is not currently supported") |
| } |
| for idx, l := range logging { |
| if l == nil { |
| continue |
| } |
| if len(l.Providers) > 1 { |
| v = appendValidation(v, Warningf("accessLogging[%d]: multiple providers is not currently supported", idx)) |
| } |
| if l.Filter != nil { |
| v = appendValidation(v, validateTelemetryFilter(l.Filter)) |
| } |
| v = appendValidation(v, validateTelemetryProviders(l.Providers)) |
| } |
| return |
| } |
| |
| func validateTelemetryTracing(tracing []*telemetry.Tracing) (v Validation) { |
| if len(tracing) > 1 { |
| v = appendWarningf(v, "multiple tracing is not currently supported") |
| } |
| for _, l := range tracing { |
| if l == nil { |
| continue |
| } |
| if len(l.Providers) > 1 { |
| v = appendWarningf(v, "multiple providers is not currently supported") |
| } |
| v = appendValidation(v, validateTelemetryProviders(l.Providers)) |
| if l.RandomSamplingPercentage.GetValue() < 0 || l.RandomSamplingPercentage.GetValue() > 100 { |
| v = appendErrorf(v, "randomSamplingPercentage must be in range [0.0, 100.0]") |
| } |
| for name, tag := range l.CustomTags { |
| if name == "" { |
| v = appendErrorf(v, "tag name may not be empty") |
| } |
| if tag == nil { |
| v = appendErrorf(v, "tag '%s' may not have a nil value", name) |
| continue |
| } |
| switch t := tag.Type.(type) { |
| case *telemetry.Tracing_CustomTag_Literal: |
| if t.Literal.GetValue() == "" { |
| v = appendErrorf(v, "literal tag value may not be empty") |
| } |
| case *telemetry.Tracing_CustomTag_Header: |
| if t.Header.GetName() == "" { |
| v = appendErrorf(v, "header tag name may not be empty") |
| } |
| case *telemetry.Tracing_CustomTag_Environment: |
| if t.Environment.GetName() == "" { |
| v = appendErrorf(v, "environment tag name may not be empty") |
| } |
| } |
| } |
| } |
| return |
| } |
| |
| func validateTelemetryMetrics(metrics []*telemetry.Metrics) (v Validation) { |
| for _, l := range metrics { |
| if l == nil { |
| continue |
| } |
| v = appendValidation(v, validateTelemetryProviders(l.Providers)) |
| for _, o := range l.Overrides { |
| if o == nil { |
| v = appendErrorf(v, "tagOverrides may not be null") |
| continue |
| } |
| for tagName, to := range o.TagOverrides { |
| if tagName == "" { |
| v = appendWarningf(v, "tagOverrides.name may not be empty") |
| } |
| if to == nil { |
| v = appendErrorf(v, "tagOverrides may not be null") |
| continue |
| } |
| switch to.Operation { |
| case telemetry.MetricsOverrides_TagOverride_UPSERT: |
| if to.Value == "" { |
| v = appendErrorf(v, "tagOverrides.value must be set set when operation is UPSERT") |
| } |
| case telemetry.MetricsOverrides_TagOverride_REMOVE: |
| if to.Value != "" { |
| v = appendErrorf(v, "tagOverrides.value may only be set when operation is UPSERT") |
| } |
| } |
| } |
| if o.Match != nil { |
| switch mm := o.Match.MetricMatch.(type) { |
| case *telemetry.MetricSelector_CustomMetric: |
| if mm.CustomMetric == "" { |
| v = appendErrorf(v, "customMetric may not be empty") |
| } |
| } |
| } |
| } |
| } |
| return |
| } |
| |
| func validateTelemetryProviders(providers []*telemetry.ProviderRef) error { |
| for _, p := range providers { |
| if p == nil || p.Name == "" { |
| return fmt.Errorf("providers.name may not be empty") |
| } |
| } |
| return nil |
| } |
| |
| // ValidateWasmPlugin validates a WasmPlugin. |
| var ValidateWasmPlugin = registerValidateFunc("ValidateWasmPlugin", |
| func(cfg config.Config) (Warning, error) { |
| spec, ok := cfg.Spec.(*extensions.WasmPlugin) |
| if !ok { |
| return nil, fmt.Errorf("cannot cast to wasmplugin") |
| } |
| |
| errs := Validation{} |
| errs = appendValidation(errs, |
| validateWorkloadSelector(spec.Selector), |
| validateWasmPluginURL(spec.Url), |
| validateWasmPluginSHA(spec), |
| validateWasmPluginVMConfig(spec.VmConfig), |
| ) |
| return errs.Unwrap() |
| }) |
| |
| func validateWasmPluginURL(pluginURL string) error { |
| if pluginURL == "" { |
| return fmt.Errorf("url field needs to be set") |
| } |
| validSchemes := map[string]bool{ |
| "": true, "file": true, "http": true, "https": true, "oci": true, |
| } |
| |
| u, err := url.Parse(pluginURL) |
| if err != nil { |
| return fmt.Errorf("failed to parse url: %s", err) |
| } |
| if _, found := validSchemes[u.Scheme]; !found { |
| return fmt.Errorf("url contains unsupported scheme: %s", u.Scheme) |
| } |
| return nil |
| } |
| |
| func validateWasmPluginSHA(plugin *extensions.WasmPlugin) error { |
| if plugin.Sha256 == "" { |
| return nil |
| } |
| if len(plugin.Sha256) != 64 { |
| return fmt.Errorf("sha256 field must be 64 characters long") |
| } |
| for _, r := range plugin.Sha256 { |
| if !('a' <= r && r <= 'f' || '0' <= r && r <= '9') { |
| return fmt.Errorf("sha256 field must match [a-f0-9]{64} pattern") |
| } |
| } |
| return nil |
| } |
| |
| func validateWasmPluginVMConfig(vm *extensions.VmConfig) error { |
| if vm == nil || len(vm.Env) == 0 { |
| return nil |
| } |
| |
| keys := sets.New() |
| for _, env := range vm.Env { |
| if env == nil { |
| continue |
| } |
| |
| if env.Name == "" { |
| return fmt.Errorf("spec.vmConfig.env invalid") |
| } |
| |
| if keys.Contains(env.Name) { |
| return fmt.Errorf("duplicate env") |
| } |
| keys.Insert(env.Name) |
| } |
| |
| return nil |
| } |