blob: e972a057b646a1142b669574adfd4cc2ceeb36c1 [file] [log] [blame]
// 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 (
"errors"
"fmt"
"reflect"
"strings"
"unicode"
)
import (
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
"istio.io/api/operator/v1alpha1"
"k8s.io/apimachinery/pkg/util/intstr"
)
import (
valuesv1alpha1 "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1"
"github.com/apache/dubbo-go-pixiu/operator/pkg/tpath"
"github.com/apache/dubbo-go-pixiu/operator/pkg/util"
)
const (
validationMethodName = "Validate"
)
type deprecatedSettings struct {
old string
new string
// In ordered to distinguish between unset for non-pointer values, we need to specify the default value
def interface{}
}
// ValidateConfig calls validation func for every defined element in Values
func ValidateConfig(failOnMissingValidation bool, iopls *v1alpha1.IstioOperatorSpec) (util.Errors, string) {
var validationErrors util.Errors
var warningMessages []string
iopvalString := util.ToYAMLWithJSONPB(iopls.Values)
values := &valuesv1alpha1.Values{}
if err := util.UnmarshalWithJSONPB(iopvalString, values, true); err != nil {
return util.NewErrs(err), ""
}
validationErrors = util.AppendErrs(validationErrors, ValidateSubTypes(reflect.ValueOf(values).Elem(), failOnMissingValidation, values, iopls))
featureErrors, featureWarningMessages := validateFeatures(values, iopls)
validationErrors = util.AppendErrs(validationErrors, featureErrors)
warningMessages = append(warningMessages, featureWarningMessages...)
deprecatedErrors, deprecatedWarningMessages := checkDeprecatedSettings(iopls)
if deprecatedErrors != nil {
validationErrors = util.AppendErr(validationErrors, deprecatedErrors)
}
warningMessages = append(warningMessages, deprecatedWarningMessages...)
return validationErrors, strings.Join(warningMessages, "\n")
}
// Converts from struct paths to helm paths
// Global.Proxy.AccessLogFormat -> global.proxy.accessLogFormat
func firstCharsToLower(s string) string {
// Use a closure here to remember state.
// Hackish but effective. Depends on Map scanning in order and calling
// the closure once per rune.
prev := '.'
return strings.Map(
func(r rune) rune {
if prev == '.' {
prev = r
return unicode.ToLower(r)
}
prev = r
return r
},
s)
}
func checkDeprecatedSettings(iop *v1alpha1.IstioOperatorSpec) (util.Errors, []string) {
var errs util.Errors
messages := []string{}
warningSettings := []deprecatedSettings{
{"Values.global.certificates", "meshConfig.certificates", nil},
{"Values.global.outboundTrafficPolicy", "meshConfig.outboundTrafficPolicy", nil},
{"Values.global.localityLbSetting", "meshConfig.localityLbSetting", nil},
{"Values.global.policyCheckFailOpen", "meshConfig.policyCheckFailOpen", false},
{"Values.global.enableTracing", "meshConfig.enableTracing", false},
{"Values.global.proxy.accessLogFormat", "meshConfig.accessLogFormat", ""},
{"Values.global.proxy.accessLogFile", "meshConfig.accessLogFile", ""},
{"Values.global.proxy.concurrency", "meshConfig.defaultConfig.concurrency", uint32(0)},
{"Values.global.proxy.envoyAccessLogService", "meshConfig.defaultConfig.envoyAccessLogService", nil},
{"Values.global.proxy.envoyAccessLogService.enabled", "meshConfig.enableEnvoyAccessLogService", nil},
{"Values.global.proxy.envoyMetricsService", "meshConfig.defaultConfig.envoyMetricsService", nil},
{"Values.global.proxy.protocolDetectionTimeout", "meshConfig.protocolDetectionTimeout", ""},
{"Values.global.proxy.holdApplicationUntilProxyStarts", "meshConfig.defaultConfig.holdApplicationUntilProxyStarts", false},
{"Values.pilot.ingress", "meshConfig.ingressService, meshConfig.ingressControllerMode, and meshConfig.ingressClass", nil},
{"Values.global.mtls.enabled", "the PeerAuthentication resource", nil},
{"Values.global.mtls.auto", "meshConfig.enableAutoMtls", nil},
{"Values.global.tracer.lightstep.address", "meshConfig.defaultConfig.tracing.lightstep.address", ""},
{"Values.global.tracer.lightstep.accessToken", "meshConfig.defaultConfig.tracing.lightstep.accessToken", ""},
{"Values.global.tracer.zipkin.address", "meshConfig.defaultConfig.tracing.zipkin.address", nil},
{"Values.global.tracer.stackdriver.debug", "meshConfig.defaultConfig.tracing.stackdriver.debug", false},
{"Values.global.tracer.stackdriver.maxNumberOfAttributes", "meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfAttributes", 0},
{"Values.global.tracer.stackdriver.maxNumberOfAnnotations", "meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfAnnotations", 0},
{"Values.global.tracer.stackdriver.maxNumberOfMessageEvents", "meshConfig.defaultConfig.tracing.stackdriver.maxNumberOfMessageEvents", 0},
{"Values.global.tracer.datadog.address", "meshConfig.defaultConfig.tracing.datadog.address", ""},
{"Values.global.meshExpansion.enabled", "Gateway and other Istio networking resources, such as in samples/multicluster/", false},
{"Values.gateways.istio-ingressgateway.meshExpansionPorts", "components.ingressGateways[name=istio-ingressgateway].k8s.service.ports", nil},
{"AddonComponents.istiocoredns.Enabled", "the in-proxy DNS capturing (ISTIO_META_DNS_CAPTURE)", false},
{"Values.istiocoredns.enabled", "the in-proxy DNS capturing (ISTIO_META_DNS_CAPTURE)", false},
// nolint: lll
{"Values.global.jwtPolicy", "Values.global.jwtPolicy=third-party-jwt. See http://istio.io/latest/docs/ops/best-practices/security/#configure-third-party-service-account-tokens for more information", "third-party-jwt"},
{
"Values.telemetry.v2.stackdriver.logging", "Values.telemetry.v2.stackdriver.outboundAccessLogging and Values.telemetry.v2.stackdriver.inboundAccessLogging",
false,
},
{"Values.global.centralIstiod", "Values.global.externalIstiod", false},
{"Values.global.arch", "the affinity of k8s settings", nil},
}
failHardSettings := []deprecatedSettings{
{"Values.grafana.enabled", "the samples/addons/ deployments", false},
{"Values.tracing.enabled", "the samples/addons/ deployments", false},
{"Values.kiali.enabled", "the samples/addons/ deployments", false},
{"Values.prometheus.enabled", "the samples/addons/ deployments", false},
{"AddonComponents.grafana.Enabled", "the samples/addons/ deployments", false},
{"AddonComponents.tracing.Enabled", "the samples/addons/ deployments", false},
{"AddonComponents.kiali.Enabled", "the samples/addons/ deployments", false},
{"AddonComponents.prometheus.Enabled", "the samples/addons/ deployments", false},
}
for _, d := range warningSettings {
v, f, _ := tpath.GetFromStructPath(iop, d.old)
if f {
switch t := v.(type) {
// need to do conversion for bool value defined in IstioOperator component spec.
case *wrappers.BoolValue:
v = t.Value
}
if v != d.def {
messages = append(messages, fmt.Sprintf("! %s is deprecated; use %s instead", firstCharsToLower(d.old), d.new))
}
}
}
for _, d := range failHardSettings {
v, f, _ := tpath.GetFromStructPath(iop, d.old)
if f {
switch t := v.(type) {
// need to do conversion for bool value defined in IstioOperator component spec.
case *wrappers.BoolValue:
v = t.Value
}
if v != d.def {
ms := fmt.Sprintf("! %s is deprecated; use %s instead", firstCharsToLower(d.old), d.new)
errs = util.AppendErr(errs, errors.New(ms+"\n"))
}
}
}
return errs, messages
}
type FeatureValidator func(*valuesv1alpha1.Values, *v1alpha1.IstioOperatorSpec) (util.Errors, []string)
// validateFeatures check whether the config sematically make sense. For example, feature X and feature Y can't be enabled together.
func validateFeatures(values *valuesv1alpha1.Values, spec *v1alpha1.IstioOperatorSpec) (errs util.Errors, warnings []string) {
validators := []FeatureValidator{
CheckServicePorts,
CheckAutoScaleAndReplicaCount,
}
for _, validator := range validators {
newErrs, newWarnings := validator(values, spec)
errs = util.AppendErrs(errs, newErrs)
warnings = append(warnings, newWarnings...)
}
return
}
// CheckAutoScaleAndReplicaCount warns when autoscaleEnabled is true and k8s replicaCount is set.
func CheckAutoScaleAndReplicaCount(values *valuesv1alpha1.Values, spec *v1alpha1.IstioOperatorSpec) (errs util.Errors, warnings []string) {
if values.GetPilot().GetAutoscaleEnabled().GetValue() && spec.GetComponents().GetPilot().GetK8S().GetReplicaCount() != 0 {
warnings = append(warnings,
"components.pilot.k8s.replicaCount should not be set when values.pilot.autoscaleEnabled is true")
}
validateGateways := func(gateways []*v1alpha1.GatewaySpec, gwType string) {
const format = "components.%sGateways[name=%s].k8s.replicaCount should not be set when values.gateways.istio-%sgateway.autoscaleEnabled is true"
for _, gw := range gateways {
if gw.GetK8S().GetReplicaCount() != 0 {
warnings = append(warnings, fmt.Sprintf(format, gwType, gw.Name, gwType))
}
}
}
if values.GetGateways().GetIstioIngressgateway().GetAutoscaleEnabled().GetValue() {
validateGateways(spec.GetComponents().GetIngressGateways(), "ingress")
}
if values.GetGateways().GetIstioEgressgateway().GetAutoscaleEnabled().GetValue() {
validateGateways(spec.GetComponents().GetEgressGateways(), "egress")
}
return
}
// CheckServicePorts validates Service ports. Specifically, this currently
// asserts that all ports will bind to a port number greater than 1024 when not
// running as root.
func CheckServicePorts(values *valuesv1alpha1.Values, spec *v1alpha1.IstioOperatorSpec) (errs util.Errors, warnings []string) {
if !values.GetGateways().GetIstioIngressgateway().GetRunAsRoot().GetValue() {
errs = util.AppendErrs(errs, validateGateways(spec.GetComponents().GetIngressGateways(), "istio-ingressgateway"))
}
if !values.GetGateways().GetIstioEgressgateway().GetRunAsRoot().GetValue() {
errs = util.AppendErrs(errs, validateGateways(spec.GetComponents().GetEgressGateways(), "istio-egressgateway"))
}
for _, raw := range values.GetGateways().GetIstioIngressgateway().GetIngressPorts() {
p := raw.AsMap()
var tp int
if p["targetPort"] != nil {
t, ok := p["targetPort"].(float64)
if !ok {
continue
}
tp = int(t)
}
rport, ok := p["port"].(float64)
if !ok {
continue
}
portnum := int(rport)
if tp == 0 && portnum > 1024 {
// Target port defaults to port. If its >1024, it is safe.
continue
}
if tp < 1024 {
// nolint: lll
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))
}
}
return
}
func validateGateways(gw []*v1alpha1.GatewaySpec, name string) util.Errors {
// nolint: lll
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"
var errs util.Errors
for _, gw := range gw {
for _, p := range gw.GetK8S().GetService().GetPorts() {
tp := 0
if p.TargetPort != nil && p.TargetPort.Type == int64(intstr.String) {
// Do not validate named ports
continue
}
if p.TargetPort != nil && p.TargetPort.Type == int64(intstr.Int) {
tp = int(p.TargetPort.IntVal.GetValue())
}
if tp == 0 && p.Port > 1024 {
// Target port defaults to port. If its >1024, it is safe.
continue
}
if tp < 1024 {
errs = util.AppendErr(errs, fmt.Errorf(format, p.Name, p.Port, gw.Name, tp, name))
}
}
}
return errs
}
func ValidateSubTypes(e reflect.Value, failOnMissingValidation bool, values *valuesv1alpha1.Values, iopls *v1alpha1.IstioOperatorSpec) util.Errors {
// Dealing with receiver pointer and receiver value
ptr := e
k := e.Kind()
if k == reflect.Ptr || k == reflect.Interface {
e = e.Elem()
}
if !e.IsValid() {
return nil
}
// check for method on value
method := e.MethodByName(validationMethodName)
if !method.IsValid() {
method = ptr.MethodByName(validationMethodName)
}
var validationErrors util.Errors
if util.IsNilOrInvalidValue(method) {
if failOnMissingValidation {
validationErrors = append(validationErrors, fmt.Errorf("type %s is missing Validation method", e.Type().String()))
}
} else {
r := method.Call([]reflect.Value{reflect.ValueOf(failOnMissingValidation), reflect.ValueOf(values), reflect.ValueOf(iopls)})[0].Interface().(util.Errors)
if len(r) != 0 {
validationErrors = append(validationErrors, r...)
}
}
// If it is not a struct nothing to do, returning previously collected validation errors
if e.Kind() != reflect.Struct {
return validationErrors
}
for i := 0; i < e.NumField(); i++ {
// Corner case of a slice of something, if something is defined type, then process it recursiveley.
if e.Field(i).Kind() == reflect.Slice {
validationErrors = append(validationErrors, processSlice(e.Field(i), failOnMissingValidation, values, iopls)...)
continue
}
if e.Field(i).Kind() == reflect.Map {
validationErrors = append(validationErrors, processMap(e.Field(i), failOnMissingValidation, values, iopls)...)
continue
}
// Validation is not required if it is not a defined type
if e.Field(i).Kind() != reflect.Interface && e.Field(i).Kind() != reflect.Ptr {
continue
}
val := e.Field(i).Elem()
if util.IsNilOrInvalidValue(val) {
continue
}
validationErrors = append(validationErrors, ValidateSubTypes(e.Field(i), failOnMissingValidation, values, iopls)...)
}
return validationErrors
}
func processSlice(e reflect.Value, failOnMissingValidation bool, values *valuesv1alpha1.Values, iopls *v1alpha1.IstioOperatorSpec) util.Errors {
var validationErrors util.Errors
for i := 0; i < e.Len(); i++ {
validationErrors = append(validationErrors, ValidateSubTypes(e.Index(i), failOnMissingValidation, values, iopls)...)
}
return validationErrors
}
func processMap(e reflect.Value, failOnMissingValidation bool, values *valuesv1alpha1.Values, iopls *v1alpha1.IstioOperatorSpec) util.Errors {
var validationErrors util.Errors
for _, k := range e.MapKeys() {
v := e.MapIndex(k)
validationErrors = append(validationErrors, ValidateSubTypes(v, failOnMissingValidation, values, iopls)...)
}
return validationErrors
}