/* * * Copyright 2021 gRPC 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 xdsresource import ( "encoding/json" "errors" "fmt" "net" "strconv" "strings" "time" v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" v3aggregateclusterpb "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3" v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/pretty" iserviceconfig "google.golang.org/grpc/internal/serviceconfig" "google.golang.org/grpc/internal/xds/bootstrap" "google.golang.org/grpc/internal/xds/matcher" "google.golang.org/grpc/xds/internal/xdsclient/xdslbregistry" "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/structpb" ) // ValidateClusterAndConstructClusterUpdateForTesting exports the // validateClusterAndConstructClusterUpdate function for testing purposes. var ValidateClusterAndConstructClusterUpdateForTesting = validateClusterAndConstructClusterUpdate // TransportSocket proto message has a `name` field which is expected to be set // to this value by the management server. const transportSocketName = "envoy.transport_sockets.tls" func unmarshalClusterResource(r *anypb.Any, serverCfg *bootstrap.ServerConfig) (string, ClusterUpdate, error) { r, err := UnwrapResource(r) if err != nil { return "", ClusterUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err) } if !IsClusterResource(r.GetTypeUrl()) { return "", ClusterUpdate{}, fmt.Errorf("unexpected resource type: %q ", r.GetTypeUrl()) } cluster := &v3clusterpb.Cluster{} if err := proto.Unmarshal(r.GetValue(), cluster); err != nil { return "", ClusterUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err) } cu, err := validateClusterAndConstructClusterUpdate(cluster, serverCfg) if err != nil { return cluster.GetName(), ClusterUpdate{}, err } cu.Raw = r return cluster.GetName(), cu, nil } const ( defaultRingHashMinSize = 1024 defaultRingHashMaxSize = 8 * 1024 * 1024 // 8M ringHashSizeUpperBound = 8 * 1024 * 1024 // 8M defaultLeastRequestChoiceCount = 2 ) func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster, serverCfg *bootstrap.ServerConfig) (ClusterUpdate, error) { telemetryLabels := make(map[string]string) if fmd := cluster.GetMetadata().GetFilterMetadata(); fmd != nil { if val, ok := fmd["com.google.csm.telemetry_labels"]; ok { if fields := val.GetFields(); fields != nil { if val, ok := fields["service_name"]; ok { if _, ok := val.GetKind().(*structpb.Value_StringValue); ok { telemetryLabels["service_name"] = val.GetStringValue() } } if val, ok := fields["service_namespace"]; ok { if _, ok := val.GetKind().(*structpb.Value_StringValue); ok { telemetryLabels["service_namespace"] = val.GetStringValue() } } } } } var lbPolicy json.RawMessage var err error switch cluster.GetLbPolicy() { case v3clusterpb.Cluster_ROUND_ROBIN: lbPolicy = []byte(`[{"xds_wrr_locality_experimental": {"childPolicy": [{"round_robin": {}}]}}]`) case v3clusterpb.Cluster_RING_HASH: rhc := cluster.GetRingHashLbConfig() if rhc.GetHashFunction() != v3clusterpb.Cluster_RingHashLbConfig_XX_HASH { return ClusterUpdate{}, fmt.Errorf("unsupported ring_hash hash function %v in response: %+v", rhc.GetHashFunction(), cluster) } // Minimum defaults to 1024 entries, and limited to 8M entries Maximum // defaults to 8M entries, and limited to 8M entries var minSize, maxSize uint64 = defaultRingHashMinSize, defaultRingHashMaxSize if min := rhc.GetMinimumRingSize(); min != nil { minSize = min.GetValue() } if max := rhc.GetMaximumRingSize(); max != nil { maxSize = max.GetValue() } rhLBCfg := []byte(fmt.Sprintf("{\"minRingSize\": %d, \"maxRingSize\": %d}", minSize, maxSize)) lbPolicy = []byte(fmt.Sprintf(`[{"ring_hash_experimental": %s}]`, rhLBCfg)) case v3clusterpb.Cluster_LEAST_REQUEST: if !envconfig.LeastRequestLB { return ClusterUpdate{}, fmt.Errorf("unexpected lbPolicy %v in response: %+v", cluster.GetLbPolicy(), cluster) } // "The configuration for the Least Request LB policy is the // least_request_lb_config field. The field is optional; if not present, // defaults will be assumed for all of its values." - A48 lr := cluster.GetLeastRequestLbConfig() var choiceCount uint32 = defaultLeastRequestChoiceCount if cc := lr.GetChoiceCount(); cc != nil { choiceCount = cc.GetValue() } // "If choice_count < 2, the config will be rejected." - A48 if choiceCount < 2 { return ClusterUpdate{}, fmt.Errorf("Cluster_LeastRequestLbConfig.ChoiceCount must be >= 2, got: %v", choiceCount) } lrLBCfg := []byte(fmt.Sprintf("{\"choiceCount\": %d}", choiceCount)) lbPolicy = []byte(fmt.Sprintf(`[{"least_request_experimental": %s}]`, lrLBCfg)) default: return ClusterUpdate{}, fmt.Errorf("unexpected lbPolicy %v in response: %+v", cluster.GetLbPolicy(), cluster) } // Process security configuration received from the control plane iff the // corresponding environment variable is set. var sc *SecurityConfig if sc, err = securityConfigFromCluster(cluster); err != nil { return ClusterUpdate{}, err } // Process outlier detection received from the control plane iff the // corresponding environment variable is set. var od json.RawMessage if od, err = outlierConfigFromCluster(cluster); err != nil { return ClusterUpdate{}, err } if cluster.GetLoadBalancingPolicy() != nil { lbPolicy, err = xdslbregistry.ConvertToServiceConfig(cluster.GetLoadBalancingPolicy(), 0) if err != nil { return ClusterUpdate{}, fmt.Errorf("error converting LoadBalancingPolicy %v in response: %+v: %v", cluster.GetLoadBalancingPolicy(), cluster, err) } // "It will be the responsibility of the XdsClient to validate the // converted configuration. It will do this by having the gRPC LB policy // registry parse the configuration." - A52 bc := &iserviceconfig.BalancerConfig{} if err := json.Unmarshal(lbPolicy, bc); err != nil { return ClusterUpdate{}, fmt.Errorf("JSON generated from xDS LB policy registry: %s is invalid: %v", pretty.FormatJSON(lbPolicy), err) } } ret := ClusterUpdate{ ClusterName: cluster.GetName(), SecurityCfg: sc, MaxRequests: circuitBreakersFromCluster(cluster), LBPolicy: lbPolicy, OutlierDetection: od, TelemetryLabels: telemetryLabels, } if lrs := cluster.GetLrsServer(); lrs != nil { if lrs.GetSelf() == nil { return ClusterUpdate{}, fmt.Errorf("unsupported config_source_specifier %T in lrs_server field", lrs.ConfigSourceSpecifier) } ret.LRSServerConfig = serverCfg } // Validate and set cluster type from the response. switch { case cluster.GetType() == v3clusterpb.Cluster_EDS: if configsource := cluster.GetEdsClusterConfig().GetEdsConfig(); configsource.GetAds() == nil && configsource.GetSelf() == nil { return ClusterUpdate{}, fmt.Errorf("CDS's EDS config source is not ADS or Self: %+v", cluster) } ret.ClusterType = ClusterTypeEDS ret.EDSServiceName = cluster.GetEdsClusterConfig().GetServiceName() if strings.HasPrefix(ret.ClusterName, "xdstp:") && ret.EDSServiceName == "" { return ClusterUpdate{}, fmt.Errorf("CDS's EDS service name is not set with a new-style cluster name: %+v", cluster) } return ret, nil case cluster.GetType() == v3clusterpb.Cluster_LOGICAL_DNS: ret.ClusterType = ClusterTypeLogicalDNS dnsHN, err := dnsHostNameFromCluster(cluster) if err != nil { return ClusterUpdate{}, err } ret.DNSHostName = dnsHN return ret, nil case cluster.GetClusterType() != nil && cluster.GetClusterType().Name == "envoy.clusters.aggregate": clusters := &v3aggregateclusterpb.ClusterConfig{} if err := proto.Unmarshal(cluster.GetClusterType().GetTypedConfig().GetValue(), clusters); err != nil { return ClusterUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err) } if len(clusters.Clusters) == 0 { return ClusterUpdate{}, fmt.Errorf("xds: aggregate cluster has empty clusters field in response: %+v", cluster) } ret.ClusterType = ClusterTypeAggregate ret.PrioritizedClusterNames = clusters.Clusters return ret, nil default: return ClusterUpdate{}, fmt.Errorf("unsupported cluster type (%v, %v) in response: %+v", cluster.GetType(), cluster.GetClusterType(), cluster) } } // dnsHostNameFromCluster extracts the DNS host name from the cluster's load // assignment. // // There should be exactly one locality, with one endpoint, whose address // contains the address and port. func dnsHostNameFromCluster(cluster *v3clusterpb.Cluster) (string, error) { loadAssignment := cluster.GetLoadAssignment() if loadAssignment == nil { return "", fmt.Errorf("load_assignment not present for LOGICAL_DNS cluster") } if len(loadAssignment.GetEndpoints()) != 1 { return "", fmt.Errorf("load_assignment for LOGICAL_DNS cluster must have exactly one locality, got: %+v", loadAssignment) } endpoints := loadAssignment.GetEndpoints()[0].GetLbEndpoints() if len(endpoints) != 1 { return "", fmt.Errorf("locality for LOGICAL_DNS cluster must have exactly one endpoint, got: %+v", endpoints) } endpoint := endpoints[0].GetEndpoint() if endpoint == nil { return "", fmt.Errorf("endpoint for LOGICAL_DNS cluster not set") } socketAddr := endpoint.GetAddress().GetSocketAddress() if socketAddr == nil { return "", fmt.Errorf("socket address for endpoint for LOGICAL_DNS cluster not set") } if socketAddr.GetResolverName() != "" { return "", fmt.Errorf("socket address for endpoint for LOGICAL_DNS cluster not set has unexpected custom resolver name: %v", socketAddr.GetResolverName()) } host := socketAddr.GetAddress() if host == "" { return "", fmt.Errorf("host for endpoint for LOGICAL_DNS cluster not set") } port := socketAddr.GetPortValue() if port == 0 { return "", fmt.Errorf("port for endpoint for LOGICAL_DNS cluster not set") } return net.JoinHostPort(host, strconv.Itoa(int(port))), nil } // securityConfigFromCluster extracts the relevant security configuration from // the received Cluster resource. func securityConfigFromCluster(cluster *v3clusterpb.Cluster) (*SecurityConfig, error) { if tsm := cluster.GetTransportSocketMatches(); len(tsm) != 0 { return nil, fmt.Errorf("unsupport transport_socket_matches field is non-empty: %+v", tsm) } // The Cluster resource contains a `transport_socket` field, which contains // a oneof `typed_config` field of type `protobuf.Any`. The any proto // contains a marshaled representation of an `UpstreamTlsContext` message. ts := cluster.GetTransportSocket() if ts == nil { return nil, nil } if name := ts.GetName(); name != transportSocketName { return nil, fmt.Errorf("transport_socket field has unexpected name: %s", name) } any := ts.GetTypedConfig() if any == nil || any.TypeUrl != version.V3UpstreamTLSContextURL { return nil, fmt.Errorf("transport_socket field has unexpected typeURL: %s", any.TypeUrl) } upstreamCtx := &v3tlspb.UpstreamTlsContext{} if err := proto.Unmarshal(any.GetValue(), upstreamCtx); err != nil { return nil, fmt.Errorf("failed to unmarshal UpstreamTlsContext in CDS response: %v", err) } // The following fields from `UpstreamTlsContext` are ignored: // - sni // - allow_renegotiation // - max_session_keys if upstreamCtx.GetCommonTlsContext() == nil { return nil, errors.New("UpstreamTlsContext in CDS response does not contain a CommonTlsContext") } return securityConfigFromCommonTLSContext(upstreamCtx.GetCommonTlsContext(), false) } // common is expected to be not nil. // The `alpn_protocols` field is ignored. func securityConfigFromCommonTLSContext(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) { if common.GetTlsParams() != nil { return nil, fmt.Errorf("unsupported tls_params field in CommonTlsContext message: %+v", common) } if common.GetCustomHandshaker() != nil { return nil, fmt.Errorf("unsupported custom_handshaker field in CommonTlsContext message: %+v", common) } // For now, if we can't get a valid security config from the new fields, we // fallback to the old deprecated fields. // TODO: Drop support for deprecated fields. NACK if err != nil here. sc, _ := securityConfigFromCommonTLSContextUsingNewFields(common, server) if sc == nil || sc.Equal(&SecurityConfig{}) { var err error sc, err = securityConfigFromCommonTLSContextWithDeprecatedFields(common, server) if err != nil { return nil, err } } if sc != nil { // sc == nil is a valid case where the control plane has not sent us any // security configuration. xDS creds will use fallback creds. if server { if sc.IdentityInstanceName == "" { return nil, errors.New("security configuration on the server-side does not contain identity certificate provider instance name") } } else { if sc.RootInstanceName == "" { return nil, errors.New("security configuration on the client-side does not contain root certificate provider instance name") } } } return sc, nil } func securityConfigFromCommonTLSContextWithDeprecatedFields(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) { // The `CommonTlsContext` contains a // `tls_certificate_certificate_provider_instance` field of type // `CertificateProviderInstance`, which contains the provider instance name // and the certificate name to fetch identity certs. sc := &SecurityConfig{} if identity := common.GetTlsCertificateCertificateProviderInstance(); identity != nil { sc.IdentityInstanceName = identity.GetInstanceName() sc.IdentityCertName = identity.GetCertificateName() } // The `CommonTlsContext` contains a `validation_context_type` field which // is a oneof. We can get the values that we are interested in from two of // those possible values: // - combined validation context: // - contains a default validation context which holds the list of // matchers for accepted SANs. // - contains certificate provider instance configuration // - certificate provider instance configuration // - in this case, we do not get a list of accepted SANs. switch t := common.GetValidationContextType().(type) { case *v3tlspb.CommonTlsContext_CombinedValidationContext: combined := common.GetCombinedValidationContext() var matchers []matcher.StringMatcher if def := combined.GetDefaultValidationContext(); def != nil { for _, m := range def.GetMatchSubjectAltNames() { matcher, err := matcher.StringMatcherFromProto(m) if err != nil { return nil, err } matchers = append(matchers, matcher) } } if server && len(matchers) != 0 { return nil, fmt.Errorf("match_subject_alt_names field in validation context is not supported on the server: %v", common) } sc.SubjectAltNameMatchers = matchers if pi := combined.GetValidationContextCertificateProviderInstance(); pi != nil { sc.RootInstanceName = pi.GetInstanceName() sc.RootCertName = pi.GetCertificateName() } case *v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance: pi := common.GetValidationContextCertificateProviderInstance() sc.RootInstanceName = pi.GetInstanceName() sc.RootCertName = pi.GetCertificateName() case nil: // It is valid for the validation context to be nil on the server side. default: return nil, fmt.Errorf("validation context contains unexpected type: %T", t) } return sc, nil } // gRFC A29 https://github.com/grpc/proposal/blob/master/A29-xds-tls-security.md // specifies the new way to fetch security configuration and says the following: // // Although there are various ways to obtain certificates as per this proto // (which are supported by Envoy), gRPC supports only one of them and that is // the `CertificateProviderPluginInstance` proto. // // This helper function attempts to fetch security configuration from the // `CertificateProviderPluginInstance` message, given a CommonTlsContext. func securityConfigFromCommonTLSContextUsingNewFields(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) { // The `tls_certificate_provider_instance` field of type // `CertificateProviderPluginInstance` is used to fetch the identity // certificate provider. sc := &SecurityConfig{} identity := common.GetTlsCertificateProviderInstance() if identity == nil && len(common.GetTlsCertificates()) != 0 { return nil, fmt.Errorf("expected field tls_certificate_provider_instance is not set, while unsupported field tls_certificates is set in CommonTlsContext message: %+v", common) } if identity == nil && common.GetTlsCertificateSdsSecretConfigs() != nil { return nil, fmt.Errorf("expected field tls_certificate_provider_instance is not set, while unsupported field tls_certificate_sds_secret_configs is set in CommonTlsContext message: %+v", common) } sc.IdentityInstanceName = identity.GetInstanceName() sc.IdentityCertName = identity.GetCertificateName() // The `CommonTlsContext` contains a oneof field `validation_context_type`, // which contains the `CertificateValidationContext` message in one of the // following ways: // - `validation_context` field // - this is directly of type `CertificateValidationContext` // - `combined_validation_context` field // - this is of type `CombinedCertificateValidationContext` and contains // a `default validation context` field of type // `CertificateValidationContext` // // The `CertificateValidationContext` message has the following fields that // we are interested in: // - `ca_certificate_provider_instance` // - this is of type `CertificateProviderPluginInstance` // - `match_subject_alt_names` // - this is a list of string matchers // // The `CertificateProviderPluginInstance` message contains two fields // - instance_name // - this is the certificate provider instance name to be looked up in // the bootstrap configuration // - certificate_name // - this is an opaque name passed to the certificate provider var validationCtx *v3tlspb.CertificateValidationContext switch typ := common.GetValidationContextType().(type) { case *v3tlspb.CommonTlsContext_ValidationContext: validationCtx = common.GetValidationContext() case *v3tlspb.CommonTlsContext_CombinedValidationContext: validationCtx = common.GetCombinedValidationContext().GetDefaultValidationContext() case nil: // It is valid for the validation context to be nil on the server side. return sc, nil default: return nil, fmt.Errorf("validation context contains unexpected type: %T", typ) } // If we get here, it means that the `CertificateValidationContext` message // was found through one of the supported ways. It is an error if the // validation context is specified, but it does not contain the // ca_certificate_provider_instance field which contains information about // the certificate provider to be used for the root certificates. if validationCtx.GetCaCertificateProviderInstance() == nil { return nil, fmt.Errorf("expected field ca_certificate_provider_instance is missing in CommonTlsContext message: %+v", common) } // The following fields are ignored: // - trusted_ca // - watched_directory // - allow_expired_certificate // - trust_chain_verification switch { case len(validationCtx.GetVerifyCertificateSpki()) != 0: return nil, fmt.Errorf("unsupported verify_certificate_spki field in CommonTlsContext message: %+v", common) case len(validationCtx.GetVerifyCertificateHash()) != 0: return nil, fmt.Errorf("unsupported verify_certificate_hash field in CommonTlsContext message: %+v", common) case validationCtx.GetRequireSignedCertificateTimestamp().GetValue(): return nil, fmt.Errorf("unsupported require_sugned_ceritificate_timestamp field in CommonTlsContext message: %+v", common) case validationCtx.GetCrl() != nil: return nil, fmt.Errorf("unsupported crl field in CommonTlsContext message: %+v", common) case validationCtx.GetCustomValidatorConfig() != nil: return nil, fmt.Errorf("unsupported custom_validator_config field in CommonTlsContext message: %+v", common) } if rootProvider := validationCtx.GetCaCertificateProviderInstance(); rootProvider != nil { sc.RootInstanceName = rootProvider.GetInstanceName() sc.RootCertName = rootProvider.GetCertificateName() } var matchers []matcher.StringMatcher for _, m := range validationCtx.GetMatchSubjectAltNames() { matcher, err := matcher.StringMatcherFromProto(m) if err != nil { return nil, err } matchers = append(matchers, matcher) } if server && len(matchers) != 0 { return nil, fmt.Errorf("match_subject_alt_names field in validation context is not supported on the server: %v", common) } sc.SubjectAltNameMatchers = matchers return sc, nil } // circuitBreakersFromCluster extracts the circuit breakers configuration from // the received cluster resource. Returns nil if no CircuitBreakers or no // Thresholds in CircuitBreakers. func circuitBreakersFromCluster(cluster *v3clusterpb.Cluster) *uint32 { for _, threshold := range cluster.GetCircuitBreakers().GetThresholds() { if threshold.GetPriority() != v3corepb.RoutingPriority_DEFAULT { continue } maxRequestsPb := threshold.GetMaxRequests() if maxRequestsPb == nil { return nil } maxRequests := maxRequestsPb.GetValue() return &maxRequests } return nil } // idurationp takes a time.Duration and converts it to an internal duration, and // returns a pointer to that internal duration. func idurationp(d time.Duration) *iserviceconfig.Duration { id := iserviceconfig.Duration(d) return &id } func uint32p(i uint32) *uint32 { return &i } // Helper types to prepare Outlier Detection JSON. Pointer types to distinguish // between unset and a zero value. type successRateEjection struct { StdevFactor *uint32 `json:"stdevFactor,omitempty"` EnforcementPercentage *uint32 `json:"enforcementPercentage,omitempty"` MinimumHosts *uint32 `json:"minimumHosts,omitempty"` RequestVolume *uint32 `json:"requestVolume,omitempty"` } type failurePercentageEjection struct { Threshold *uint32 `json:"threshold,omitempty"` EnforcementPercentage *uint32 `json:"enforcementPercentage,omitempty"` MinimumHosts *uint32 `json:"minimumHosts,omitempty"` RequestVolume *uint32 `json:"requestVolume,omitempty"` } type odLBConfig struct { Interval *iserviceconfig.Duration `json:"interval,omitempty"` BaseEjectionTime *iserviceconfig.Duration `json:"baseEjectionTime,omitempty"` MaxEjectionTime *iserviceconfig.Duration `json:"maxEjectionTime,omitempty"` MaxEjectionPercent *uint32 `json:"maxEjectionPercent,omitempty"` SuccessRateEjection *successRateEjection `json:"successRateEjection,omitempty"` FailurePercentageEjection *failurePercentageEjection `json:"failurePercentageEjection,omitempty"` } // outlierConfigFromCluster converts the received Outlier Detection // configuration into JSON configuration for Outlier Detection, taking into // account xDS Defaults. Returns nil if no OutlierDetection field set in the // cluster resource. func outlierConfigFromCluster(cluster *v3clusterpb.Cluster) (json.RawMessage, error) { od := cluster.GetOutlierDetection() if od == nil { return nil, nil } // "The outlier_detection field of the Cluster resource should have its fields // validated according to the rules for the corresponding LB policy config // fields in the above "Validation" section. If any of these requirements is // violated, the Cluster resource should be NACKed." - A50 // "The google.protobuf.Duration fields interval, base_ejection_time, and // max_ejection_time must obey the restrictions in the // google.protobuf.Duration documentation and they must have non-negative // values." - A50 var interval *iserviceconfig.Duration if i := od.GetInterval(); i != nil { if err := i.CheckValid(); err != nil { return nil, fmt.Errorf("outlier_detection.interval is invalid with error: %v", err) } if interval = idurationp(i.AsDuration()); *interval < 0 { return nil, fmt.Errorf("outlier_detection.interval = %v; must be a valid duration and >= 0", *interval) } } var baseEjectionTime *iserviceconfig.Duration if bet := od.GetBaseEjectionTime(); bet != nil { if err := bet.CheckValid(); err != nil { return nil, fmt.Errorf("outlier_detection.base_ejection_time is invalid with error: %v", err) } if baseEjectionTime = idurationp(bet.AsDuration()); *baseEjectionTime < 0 { return nil, fmt.Errorf("outlier_detection.base_ejection_time = %v; must be >= 0", *baseEjectionTime) } } var maxEjectionTime *iserviceconfig.Duration if met := od.GetMaxEjectionTime(); met != nil { if err := met.CheckValid(); err != nil { return nil, fmt.Errorf("outlier_detection.max_ejection_time is invalid: %v", err) } if maxEjectionTime = idurationp(met.AsDuration()); *maxEjectionTime < 0 { return nil, fmt.Errorf("outlier_detection.max_ejection_time = %v; must be >= 0", *maxEjectionTime) } } // "The fields max_ejection_percent, enforcing_success_rate, // failure_percentage_threshold, and enforcing_failure_percentage must have // values less than or equal to 100. If any of these requirements is // violated, the Cluster resource should be NACKed." - A50 var maxEjectionPercent *uint32 if mep := od.GetMaxEjectionPercent(); mep != nil { if maxEjectionPercent = uint32p(mep.GetValue()); *maxEjectionPercent > 100 { return nil, fmt.Errorf("outlier_detection.max_ejection_percent = %v; must be <= 100", *maxEjectionPercent) } } // "if the enforcing_success_rate field is set to 0, the config // success_rate_ejection field will be null and all success_rate_* fields // will be ignored." - A50 var enforcingSuccessRate *uint32 if esr := od.GetEnforcingSuccessRate(); esr != nil { if enforcingSuccessRate = uint32p(esr.GetValue()); *enforcingSuccessRate > 100 { return nil, fmt.Errorf("outlier_detection.enforcing_success_rate = %v; must be <= 100", *enforcingSuccessRate) } } var failurePercentageThreshold *uint32 if fpt := od.GetFailurePercentageThreshold(); fpt != nil { if failurePercentageThreshold = uint32p(fpt.GetValue()); *failurePercentageThreshold > 100 { return nil, fmt.Errorf("outlier_detection.failure_percentage_threshold = %v; must be <= 100", *failurePercentageThreshold) } } // "If the enforcing_failure_percent field is set to 0 or null, the config // failure_percent_ejection field will be null and all failure_percent_* // fields will be ignored." - A50 var enforcingFailurePercentage *uint32 if efp := od.GetEnforcingFailurePercentage(); efp != nil { if enforcingFailurePercentage = uint32p(efp.GetValue()); *enforcingFailurePercentage > 100 { return nil, fmt.Errorf("outlier_detection.enforcing_failure_percentage = %v; must be <= 100", *enforcingFailurePercentage) } } var successRateStdevFactor *uint32 if srsf := od.GetSuccessRateStdevFactor(); srsf != nil { successRateStdevFactor = uint32p(srsf.GetValue()) } var successRateMinimumHosts *uint32 if srmh := od.GetSuccessRateMinimumHosts(); srmh != nil { successRateMinimumHosts = uint32p(srmh.GetValue()) } var successRateRequestVolume *uint32 if srrv := od.GetSuccessRateRequestVolume(); srrv != nil { successRateRequestVolume = uint32p(srrv.GetValue()) } var failurePercentageMinimumHosts *uint32 if fpmh := od.GetFailurePercentageMinimumHosts(); fpmh != nil { failurePercentageMinimumHosts = uint32p(fpmh.GetValue()) } var failurePercentageRequestVolume *uint32 if fprv := od.GetFailurePercentageRequestVolume(); fprv != nil { failurePercentageRequestVolume = uint32p(fprv.GetValue()) } // "if the enforcing_success_rate field is set to 0, the config // success_rate_ejection field will be null and all success_rate_* fields // will be ignored." - A50 var sre *successRateEjection if enforcingSuccessRate == nil || *enforcingSuccessRate != 0 { sre = &successRateEjection{ StdevFactor: successRateStdevFactor, EnforcementPercentage: enforcingSuccessRate, MinimumHosts: successRateMinimumHosts, RequestVolume: successRateRequestVolume, } } // "If the enforcing_failure_percent field is set to 0 or null, the config // failure_percent_ejection field will be null and all failure_percent_* // fields will be ignored." - A50 var fpe *failurePercentageEjection if enforcingFailurePercentage != nil && *enforcingFailurePercentage != 0 { fpe = &failurePercentageEjection{ Threshold: failurePercentageThreshold, EnforcementPercentage: enforcingFailurePercentage, MinimumHosts: failurePercentageMinimumHosts, RequestVolume: failurePercentageRequestVolume, } } odLBCfg := &odLBConfig{ Interval: interval, BaseEjectionTime: baseEjectionTime, MaxEjectionTime: maxEjectionTime, MaxEjectionPercent: maxEjectionPercent, SuccessRateEjection: sre, FailurePercentageEjection: fpe, } return json.Marshal(odLBCfg) }