istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/apis/istio/v1alpha1/validation/validation.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package validation
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"reflect"
    21  	"strings"
    22  	"unicode"
    23  
    24  	wrappers "google.golang.org/protobuf/types/known/wrapperspb"
    25  	"k8s.io/apimachinery/pkg/util/intstr"
    26  
    27  	"istio.io/api/operator/v1alpha1"
    28  	valuesv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    29  	"istio.io/istio/operator/pkg/tpath"
    30  	"istio.io/istio/operator/pkg/util"
    31  )
    32  
    33  const (
    34  	validationMethodName = "Validate"
    35  )
    36  
    37  type deprecatedSettings struct {
    38  	old string
    39  	new string
    40  	// In ordered to distinguish between unset for non-pointer values, we need to specify the default value
    41  	def any
    42  }
    43  
    44  // ValidateConfig  calls validation func for every defined element in Values
    45  func ValidateConfig(failOnMissingValidation bool, iopls *v1alpha1.IstioOperatorSpec) (util.Errors, string) {
    46  	var validationErrors util.Errors
    47  	var warningMessages []string
    48  	iopvalString := util.ToYAMLWithJSONPB(iopls.Values)
    49  	values := &valuesv1alpha1.Values{}
    50  	if err := util.UnmarshalWithJSONPB(iopvalString, values, true); err != nil {
    51  		return util.NewErrs(err), ""
    52  	}
    53  
    54  	validationErrors = util.AppendErrs(validationErrors, ValidateSubTypes(reflect.ValueOf(values).Elem(), failOnMissingValidation, values, iopls))
    55  
    56  	featureErrors, featureWarningMessages := validateFeatures(values, iopls)
    57  	validationErrors = util.AppendErrs(validationErrors, featureErrors)
    58  	warningMessages = append(warningMessages, featureWarningMessages...)
    59  
    60  	deprecatedErrors, deprecatedWarningMessages := checkDeprecatedSettings(iopls)
    61  	if deprecatedErrors != nil {
    62  		validationErrors = util.AppendErr(validationErrors, deprecatedErrors)
    63  	}
    64  	warningMessages = append(warningMessages, deprecatedWarningMessages...)
    65  	return validationErrors, strings.Join(warningMessages, "\n")
    66  }
    67  
    68  // Converts from struct paths to helm paths
    69  // Global.Proxy.AccessLogFormat -> global.proxy.accessLogFormat
    70  func firstCharsToLower(s string) string {
    71  	// Use a closure here to remember state.
    72  	// Hackish but effective. Depends on Map scanning in order and calling
    73  	// the closure once per rune.
    74  	prev := '.'
    75  	return strings.Map(
    76  		func(r rune) rune {
    77  			if prev == '.' {
    78  				prev = r
    79  				return unicode.ToLower(r)
    80  			}
    81  			prev = r
    82  			return r
    83  		},
    84  		s)
    85  }
    86  
    87  func checkDeprecatedSettings(iop *v1alpha1.IstioOperatorSpec) (util.Errors, []string) {
    88  	var errs util.Errors
    89  	messages := []string{}
    90  	warningSettings := []deprecatedSettings{
    91  		{"Values.global.certificates", "meshConfig.certificates", nil},
    92  		{"Values.global.outboundTrafficPolicy", "meshConfig.outboundTrafficPolicy", nil},
    93  		{"Values.global.localityLbSetting", "meshConfig.localityLbSetting", nil},
    94  		{"Values.global.policyCheckFailOpen", "meshConfig.policyCheckFailOpen", false},
    95  		{"Values.global.enableTracing", "meshConfig.enableTracing", false},
    96  		{"Values.global.proxy.accessLogFormat", "meshConfig.accessLogFormat", ""},
    97  		{"Values.global.proxy.accessLogFile", "meshConfig.accessLogFile", ""},
    98  		{"Values.global.proxy.concurrency", "meshConfig.defaultConfig.concurrency", uint32(0)},
    99  		{"Values.global.proxy.envoyAccessLogService", "meshConfig.defaultConfig.envoyAccessLogService", nil},
   100  		{"Values.global.proxy.envoyAccessLogService.enabled", "meshConfig.enableEnvoyAccessLogService", nil},
   101  		{"Values.global.proxy.envoyMetricsService", "meshConfig.defaultConfig.envoyMetricsService", nil},
   102  		{"Values.global.proxy.protocolDetectionTimeout", "meshConfig.protocolDetectionTimeout", ""},
   103  		{"Values.global.proxy.holdApplicationUntilProxyStarts", "meshConfig.defaultConfig.holdApplicationUntilProxyStarts", false},
   104  		{"Values.pilot.ingress", "meshConfig.ingressService, meshConfig.ingressControllerMode, and meshConfig.ingressClass", nil},
   105  		{"Values.global.mtls.enabled", "the PeerAuthentication resource", nil},
   106  		{"Values.global.mtls.auto", "meshConfig.enableAutoMtls", nil},
   107  		{"Values.global.tracer.lightstep.address", "meshConfig.defaultConfig.tracing.lightstep.address", ""},
   108  		{"Values.global.tracer.lightstep.accessToken", "meshConfig.defaultConfig.tracing.lightstep.accessToken", ""},
   109  		{"Values.global.tracer.zipkin.address", "meshConfig.defaultConfig.tracing.zipkin.address", nil},
   110  		{"Values.global.tracer.datadog.address", "meshConfig.defaultConfig.tracing.datadog.address", ""},
   111  		{"Values.global.meshExpansion.enabled", "Gateway and other Istio networking resources, such as in samples/multicluster/", false},
   112  		{"Values.gateways.istio-ingressgateway.meshExpansionPorts", "components.ingressGateways[name=istio-ingressgateway].k8s.service.ports", nil},
   113  		{"AddonComponents.istiocoredns.Enabled", "the in-proxy DNS capturing (ISTIO_META_DNS_CAPTURE)", false},
   114  		{"Values.istiocoredns.enabled", "the in-proxy DNS capturing (ISTIO_META_DNS_CAPTURE)", false},
   115  		// nolint: lll
   116  		{"Values.global.jwtPolicy", "Values.global.jwtPolicy=third-party-jwt. See https://istio.io/latest/docs/ops/best-practices/security/#configure-third-party-service-account-tokens for more information", "third-party-jwt"},
   117  		{"Values.global.centralIstiod", "Values.global.externalIstiod", false},
   118  		{"Values.global.arch", "the affinity of k8s settings", nil},
   119  	}
   120  
   121  	failHardSettings := []deprecatedSettings{
   122  		{"Values.grafana.enabled", "the samples/addons/ deployments", false},
   123  		{"Values.tracing.enabled", "the samples/addons/ deployments", false},
   124  		{"Values.kiali.enabled", "the samples/addons/ deployments", false},
   125  		{"Values.prometheus.enabled", "the samples/addons/ deployments", false},
   126  		{"AddonComponents.grafana.Enabled", "the samples/addons/ deployments", false},
   127  		{"AddonComponents.tracing.Enabled", "the samples/addons/ deployments", false},
   128  		{"AddonComponents.kiali.Enabled", "the samples/addons/ deployments", false},
   129  		{"AddonComponents.prometheus.Enabled", "the samples/addons/ deployments", false},
   130  		{"Values.global.tracer.stackdriver.debug", "meshConfig.defaultConfig.tracing.stackdriver.debug", false},
   131  		{"Values.global.tracer.stackdriver.maxNumberOfAttributes", "meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfAttributes", 0},
   132  		{"Values.global.tracer.stackdriver.maxNumberOfAnnotations", "meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfAnnotations", 0},
   133  		{"Values.global.tracer.stackdriver.maxNumberOfMessageEvents", "meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfMessageEvents", 0},
   134  		{"telemetry.v2.prometheus.configOverride", "custom configuration", nil},
   135  		{"telemetry.v2.stackdriver.configOverride", "custom configuration", nil},
   136  		{"telemetry.v2.stackdriver.disableOutbound", "custom configuration", nil},
   137  		{"telemetry.v2.stackdriver.outboundAccessLogging", "custom configuration", nil},
   138  		{"meshConfig.defaultConfig.tracing.stackdriver.debug", "Istio supported tracers", false},
   139  		{"meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfAttributes", "Istio supported tracers", 0},
   140  		{"meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfAnnotations", "Istio supported tracers", 0},
   141  		{"meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfMessageEvents", "Istio supported tracers", 0},
   142  	}
   143  
   144  	for _, d := range warningSettings {
   145  		v, f, _ := tpath.GetFromStructPath(iop, d.old)
   146  		if f {
   147  			switch t := v.(type) {
   148  			// need to do conversion for bool value defined in IstioOperator component spec.
   149  			case *wrappers.BoolValue:
   150  				v = t.Value
   151  			}
   152  			if v != d.def {
   153  				messages = append(messages, fmt.Sprintf("! %s is deprecated; use %s instead", firstCharsToLower(d.old), d.new))
   154  			}
   155  		}
   156  	}
   157  	for _, d := range failHardSettings {
   158  		v, f, _ := tpath.GetFromStructPath(iop, d.old)
   159  		if f {
   160  			switch t := v.(type) {
   161  			// need to do conversion for bool value defined in IstioOperator component spec.
   162  			case *wrappers.BoolValue:
   163  				v = t.Value
   164  			}
   165  			if v != d.def {
   166  				ms := fmt.Sprintf("! %s is deprecated; use %s instead", firstCharsToLower(d.old), d.new)
   167  				errs = util.AppendErr(errs, errors.New(ms+"\n"))
   168  			}
   169  		}
   170  	}
   171  	return errs, messages
   172  }
   173  
   174  type FeatureValidator func(*valuesv1alpha1.Values, *v1alpha1.IstioOperatorSpec) (util.Errors, []string)
   175  
   176  // validateFeatures check whether the config semantically make sense. For example, feature X and feature Y can't be enabled together.
   177  func validateFeatures(values *valuesv1alpha1.Values, spec *v1alpha1.IstioOperatorSpec) (errs util.Errors, warnings []string) {
   178  	validators := []FeatureValidator{
   179  		CheckServicePorts,
   180  		CheckAutoScaleAndReplicaCount,
   181  	}
   182  
   183  	for _, validator := range validators {
   184  		newErrs, newWarnings := validator(values, spec)
   185  		errs = util.AppendErrs(errs, newErrs)
   186  		warnings = append(warnings, newWarnings...)
   187  	}
   188  
   189  	return
   190  }
   191  
   192  // CheckAutoScaleAndReplicaCount warns when autoscaleEnabled is true and k8s replicaCount is set.
   193  func CheckAutoScaleAndReplicaCount(values *valuesv1alpha1.Values, spec *v1alpha1.IstioOperatorSpec) (errs util.Errors, warnings []string) {
   194  	if values.GetPilot().GetAutoscaleEnabled().GetValue() && spec.GetComponents().GetPilot().GetK8S().GetReplicaCount() > 1 {
   195  		warnings = append(warnings,
   196  			"components.pilot.k8s.replicaCount should not be set when values.pilot.autoscaleEnabled is true")
   197  	}
   198  
   199  	validateGateways := func(gateways []*v1alpha1.GatewaySpec, gwType string) {
   200  		const format = "components.%sGateways[name=%s].k8s.replicaCount should not be set when values.gateways.istio-%sgateway.autoscaleEnabled is true"
   201  		for _, gw := range gateways {
   202  			if gw.GetK8S().GetReplicaCount() != 0 {
   203  				warnings = append(warnings, fmt.Sprintf(format, gwType, gw.Name, gwType))
   204  			}
   205  		}
   206  	}
   207  
   208  	if values.GetGateways().GetIstioIngressgateway().GetAutoscaleEnabled().GetValue() {
   209  		validateGateways(spec.GetComponents().GetIngressGateways(), "ingress")
   210  	}
   211  
   212  	if values.GetGateways().GetIstioEgressgateway().GetAutoscaleEnabled().GetValue() {
   213  		validateGateways(spec.GetComponents().GetEgressGateways(), "egress")
   214  	}
   215  
   216  	return
   217  }
   218  
   219  // CheckServicePorts validates Service ports. Specifically, this currently
   220  // asserts that all ports will bind to a port number greater than 1024 when not
   221  // running as root.
   222  func CheckServicePorts(values *valuesv1alpha1.Values, spec *v1alpha1.IstioOperatorSpec) (errs util.Errors, warnings []string) {
   223  	if !values.GetGateways().GetIstioIngressgateway().GetRunAsRoot().GetValue() {
   224  		errs = util.AppendErrs(errs, validateGateways(spec.GetComponents().GetIngressGateways(), "istio-ingressgateway"))
   225  	}
   226  	if !values.GetGateways().GetIstioEgressgateway().GetRunAsRoot().GetValue() {
   227  		errs = util.AppendErrs(errs, validateGateways(spec.GetComponents().GetEgressGateways(), "istio-egressgateway"))
   228  	}
   229  	for _, raw := range values.GetGateways().GetIstioIngressgateway().GetIngressPorts() {
   230  		p := raw.AsMap()
   231  		var tp int
   232  		if p["targetPort"] != nil {
   233  			t, ok := p["targetPort"].(float64)
   234  			if !ok {
   235  				continue
   236  			}
   237  			tp = int(t)
   238  		}
   239  
   240  		rport, ok := p["port"].(float64)
   241  		if !ok {
   242  			continue
   243  		}
   244  		portnum := int(rport)
   245  		if tp == 0 && portnum > 1024 {
   246  			// Target port defaults to port. If its >1024, it is safe.
   247  			continue
   248  		}
   249  		if tp < 1024 {
   250  			// nolint: lll
   251  			errs = util.AppendErr(errs, fmt.Errorf("port %v is invalid: targetPort is set to %v, which requires root. Set targetPort to be greater than 1024 or configure values.gateways.istio-ingressgateway.runAsRoot=true", portnum, tp))
   252  		}
   253  	}
   254  	return
   255  }
   256  
   257  func validateGateways(gw []*v1alpha1.GatewaySpec, name string) util.Errors {
   258  	// nolint: lll
   259  	format := "port %v/%v in gateway %v invalid: targetPort is set to %d, which requires root. Set targetPort to be greater than 1024 or configure values.gateways.%s.runAsRoot=true"
   260  	var errs util.Errors
   261  	for _, gw := range gw {
   262  		for _, p := range gw.GetK8S().GetService().GetPorts() {
   263  			tp := 0
   264  			if p == nil {
   265  				continue
   266  			}
   267  			if p.TargetPort != nil && p.TargetPort.Type == int64(intstr.String) {
   268  				// Do not validate named ports
   269  				continue
   270  			}
   271  			if p.TargetPort != nil && p.TargetPort.Type == int64(intstr.Int) {
   272  				tp = int(p.TargetPort.IntVal.GetValue())
   273  			}
   274  			if tp == 0 && p.Port > 1024 {
   275  				// Target port defaults to port. If its >1024, it is safe.
   276  				continue
   277  			}
   278  			if tp < 1024 {
   279  				errs = util.AppendErr(errs, fmt.Errorf(format, p.Name, p.Port, gw.Name, tp, name))
   280  			}
   281  		}
   282  	}
   283  	return errs
   284  }
   285  
   286  func ValidateSubTypes(e reflect.Value, failOnMissingValidation bool, values *valuesv1alpha1.Values, iopls *v1alpha1.IstioOperatorSpec) util.Errors {
   287  	// Dealing with receiver pointer and receiver value
   288  	ptr := e
   289  	k := e.Kind()
   290  	if k == reflect.Ptr || k == reflect.Interface {
   291  		e = e.Elem()
   292  	}
   293  	if !e.IsValid() {
   294  		return nil
   295  	}
   296  	// check for method on value
   297  	method := e.MethodByName(validationMethodName)
   298  	if !method.IsValid() {
   299  		method = ptr.MethodByName(validationMethodName)
   300  	}
   301  
   302  	var validationErrors util.Errors
   303  	if util.IsNilOrInvalidValue(method) {
   304  		if failOnMissingValidation {
   305  			validationErrors = append(validationErrors, fmt.Errorf("type %s is missing Validation method", e.Type().String()))
   306  		}
   307  	} else {
   308  		r := method.Call([]reflect.Value{reflect.ValueOf(failOnMissingValidation), reflect.ValueOf(values), reflect.ValueOf(iopls)})[0].Interface().(util.Errors)
   309  		if len(r) != 0 {
   310  			validationErrors = append(validationErrors, r...)
   311  		}
   312  	}
   313  	// If it is not a struct nothing to do, returning previously collected validation errors
   314  	if e.Kind() != reflect.Struct {
   315  		return validationErrors
   316  	}
   317  	for i := 0; i < e.NumField(); i++ {
   318  		// Corner case of a slice of something, if something is defined type, then process it recursively.
   319  		if e.Field(i).Kind() == reflect.Slice {
   320  			validationErrors = append(validationErrors, processSlice(e.Field(i), failOnMissingValidation, values, iopls)...)
   321  			continue
   322  		}
   323  		if e.Field(i).Kind() == reflect.Map {
   324  			validationErrors = append(validationErrors, processMap(e.Field(i), failOnMissingValidation, values, iopls)...)
   325  			continue
   326  		}
   327  		// Validation is not required if it is not a defined type
   328  		if e.Field(i).Kind() != reflect.Interface && e.Field(i).Kind() != reflect.Ptr {
   329  			continue
   330  		}
   331  		val := e.Field(i).Elem()
   332  		if util.IsNilOrInvalidValue(val) {
   333  			continue
   334  		}
   335  		validationErrors = append(validationErrors, ValidateSubTypes(e.Field(i), failOnMissingValidation, values, iopls)...)
   336  	}
   337  
   338  	return validationErrors
   339  }
   340  
   341  func processSlice(e reflect.Value, failOnMissingValidation bool, values *valuesv1alpha1.Values, iopls *v1alpha1.IstioOperatorSpec) util.Errors {
   342  	var validationErrors util.Errors
   343  	for i := 0; i < e.Len(); i++ {
   344  		validationErrors = append(validationErrors, ValidateSubTypes(e.Index(i), failOnMissingValidation, values, iopls)...)
   345  	}
   346  
   347  	return validationErrors
   348  }
   349  
   350  func processMap(e reflect.Value, failOnMissingValidation bool, values *valuesv1alpha1.Values, iopls *v1alpha1.IstioOperatorSpec) util.Errors {
   351  	var validationErrors util.Errors
   352  	for _, k := range e.MapKeys() {
   353  		v := e.MapIndex(k)
   354  		validationErrors = append(validationErrors, ValidateSubTypes(v, failOnMissingValidation, values, iopls)...)
   355  	}
   356  
   357  	return validationErrors
   358  }