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 }