文章目录

  • Kiali中的Istio配置管理
  • 配置获取及验证
  • Api 路径
  • 入口方法
  • 验证配置方法( GetIstioObjectValidations)
  • 网关配置验证(GatewayChecker)


Kiali中的Istio配置管理

在 Kiali 的管理界面中,最后一项是 Istio Config,在这里可以看到 Istio 定义的所有 CRD 的配置。

istio控制面怎么下发配置 istio管理界面_istio控制面怎么下发配置


在上方 Istio Type 的选项框中,可以看到 Kiali 支持这些类型的配置查看,但是目前的配置验证只支持其中的部分配置。

istio控制面怎么下发配置 istio管理界面_Kiali_02


如果配置验证有问题,会在列表的旁边显示红色的叹号,点击进去就可以看到错误的详情。

istio控制面怎么下发配置 istio管理界面_Istio_03


这里可以直接在界面上进行配置的修改,保存之后,如果修改的配置无误,红色标记就会消失。

接下来我们看下代码,这些功能是如何实现的。

配置获取及验证

在这里把重点放在单个配置的获取和验证上。

Api 路径

// swagger:route GET /namespaces/{namespace}/istio/{object_type}/{object} config istioConfigDetails
		// ---
		// Endpoint to get the Istio Config of an Istio object
		//
		//     Produces:
		//     - application/json
		//
		//     Schemes: http, https
		//
		// responses:
		//      400: badRequestError
		//      404: notFoundError
		//      500: internalError
		//      200: istioConfigDetailsResponse
		//
		{
			"IstioConfigDetails",
			"GET",
			"/api/namespaces/{namespace}/istio/{object_type}/{object}",
			handlers.IstioConfigDetails,
			true,
		},

Api 的路径是 /api/namespaces/{namespace}/istio/{object_type}/{object},对应的方法是 handlers 中的 IstioConfigDetails 方法,接下来进入这个方法当中。

入口方法

func IstioConfigDetails(w http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	namespace := params["namespace"]
	objectType := params["object_type"]
	objectSubtype := params["object_subtype"]
	object := params["object"]

	includeValidations := false
	query := r.URL.Query()
	// 判断请求中是否需要验证
	if _, found := query["validate"]; found {
		includeValidations = true
	}

	// 检查目标类型是否合法
	if !checkObjectType(objectType) {
		RespondWithError(w, http.StatusBadRequest, "Object type not managed: "+objectType)
		return
	}

	// Get business layer
	business, err := getBusiness(r)
	if err != nil {
		RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
		return
	}

	var istioConfigValidations models.IstioValidations

	wg := sync.WaitGroup{}
	// 如果需要验证,那么开启一个协程来进行验证操作
	if includeValidations {
		wg.Add(1)
		go func(istioConfigValidations *models.IstioValidations, err *error) {
			defer wg.Done()
			istioConfigValidationResults, errValidations := business.Validations.GetIstioObjectValidations(namespace, objectType, object)
			if errValidations != nil && *err == nil {
				*err = errValidations
			} else {
				*istioConfigValidations = istioConfigValidationResults
			}
		}(&istioConfigValidations, &err)
	}
	// 获取配置的详情
	istioConfigDetails, err := business.IstioConfig.GetIstioConfigDetails(namespace, objectType, objectSubtype, object)

	if includeValidations && err == nil {
		wg.Wait()
		
		// 获取验证结果,这里的 key 是由 objectType 和 name 组成的,value 中存储的是验证的结果
		if validation, found := istioConfigValidations[models.IstioValidationKey{ObjectType: models.ObjectTypeSingular[objectType], Name: object}]; found {
			istioConfigDetails.IstioValidation = validation
		}
	}

	if errors.IsNotFound(err) {
		RespondWithError(w, http.StatusNotFound, err.Error())
		return
	} else if statusError, isStatus := err.(*errors.StatusError); isStatus {
		RespondWithError(w, http.StatusInternalServerError, statusError.ErrStatus.Message)
		return
	} else if err != nil {
		RespondWithError(w, http.StatusInternalServerError, err.Error())
		return
	}

	RespondWithJSON(w, http.StatusOK, istioConfigDetails)
}

看一下 IstioValidations 的数据结构

// IstioValidations represents a set of IstioValidation grouped by IstioValidationKey.
type IstioValidations map[IstioValidationKey]*IstioValidation

这是一个map类型的数据。为了直观的展现数据结构,在这里贴一下 debug 的结构图。

istio控制面怎么下发配置 istio管理界面_Istio_04

验证配置方法( GetIstioObjectValidations)

这个方法会读取 Istio 定义的配置,然后检测配置的正确性。

func (in *IstioValidationsService) GetIstioObjectValidations(namespace string, objectType string, object string) (models.IstioValidations, error) {
	var err error
	promtimer := internalmetrics.GetGoFunctionMetric("business", "IstioValidationsService", "GetIstioObjectValidations")
	defer promtimer.ObserveNow(&err)

	var istioDetails kubernetes.IstioDetails
	var services []core_v1.Service
	var workloads models.WorkloadList
	var gatewaysPerNamespace [][]kubernetes.IstioObject
	var mtlsDetails kubernetes.MTLSDetails
	var rbacDetails kubernetes.RBACDetails

	var objectCheckers []ObjectChecker

	wg := sync.WaitGroup{}
	errChan := make(chan error, 1)

	// 从当前 namespace 中获取 istio 配置,gateway 的配置则从所有namespace中获取。
	wg.Add(6)
	// 获取当前命名空间下的 ServiceEntry、Gateway、VirtrualServcie、DestinationRule
	go in.fetchDetails(&istioDetails, namespace, errChan, &wg)
	// 获取当前命名空间下的 Service
	go in.fetchServices(&services, namespace, errChan, &wg)
	// 获取当前命名空间下的工作负载
	// 工作负载包括 pod、deployment、replicaset、replicationcontroller、deploymentconfig、statefulset、cronjob、job这些资源
	go in.fetchWorkloads(&workloads, namespace, errChan, &wg)
	// 获取所有命名空间下的 gateway
	// 这里获取的 gateway 包含 fetchDetails 中的 gateway 
	// 获取所有命名空间下的目的处理 virtrualservice 使用其他命名空间下的 gateway 的情况
	go in.fetchGatewaysPerNamespace(&gatewaysPerNamespace, errChan, &wg)
	// 获取当前命名空间下 Istio mtls 配置,在这里是获取 meshpolicy 和 policy 两类配置
	go in.fetchNonLocalmTLSConfigs(&mtlsDetails, namespace, errChan, &wg)
	// 获取当前命名空间下权限相关配置,在这里是获取 servicerolebinding、servicerole、clusterrbacconfig 配置
	go in.fetchAuthorizationDetails(&rbacDetails, namespace, errChan, &wg)
	wg.Wait()
	// 生成一个 noServiceChecker,这个是检查除了 Service 之外的资源
	noServiceChecker := checkers.NoServiceChecker{Namespace: namespace, IstioDetails: &istioDetails, Services: services, WorkloadList: workloads, GatewaysPerNamespace: gatewaysPerNamespace, AuthorizationDetails: &rbacDetails}

	// 判断目标的类型,选择不同的 Checker 进行检查
	switch objectType {
	case Gateways:
		objectCheckers = []ObjectChecker{
			checkers.GatewayChecker{GatewaysPerNamespace: gatewaysPerNamespace, Namespace: namespace, WorkloadList: workloads},
		}
	case VirtualServices:
		virtualServiceChecker := checkers.VirtualServiceChecker{Namespace: namespace, VirtualServices: istioDetails.VirtualServices, DestinationRules: istioDetails.DestinationRules}
		objectCheckers = []ObjectChecker{noServiceChecker, virtualServiceChecker}
	case DestinationRules:
		destinationRulesChecker := checkers.DestinationRulesChecker{DestinationRules: istioDetails.DestinationRules, MTLSDetails: mtlsDetails, ServiceEntries: istioDetails.ServiceEntries}
		objectCheckers = []ObjectChecker{noServiceChecker, destinationRulesChecker}
	case MeshPolicies:
		meshPoliciesChecker := checkers.MeshPolicyChecker{MeshPolicies: mtlsDetails.MeshPolicies, MTLSDetails: mtlsDetails}
		objectCheckers = []ObjectChecker{meshPoliciesChecker}
	case Policies:
		policiesChecker := checkers.PolicyChecker{Policies: mtlsDetails.Policies, MTLSDetails: mtlsDetails}
		objectCheckers = []ObjectChecker{policiesChecker}
	case ServiceEntries:
		serviceEntryChecker := checkers.ServiceEntryChecker{ServiceEntries: istioDetails.ServiceEntries}
		objectCheckers = []ObjectChecker{serviceEntryChecker}
	// 以下类型的检测暂不支持
	case Rules:
		// Validations on Istio Rules are not yet in place
	case Templates:
		// Validations on Templates are not yet in place
		// TODO Support subtypes
	case Adapters:
		// Validations on Adapters are not yet in place
		// TODO Support subtypes
	case QuotaSpecs:
		// Validations on QuotaSpecs are not yet in place
	case QuotaSpecBindings:
		// Validations on QuotaSpecBindings are not yet in place
	case ClusterRbacConfigs:
		// Validations on ClusterRbacConfigs are not yet in place
	case RbacConfigs:
		// Validations on RbacConfigs are not yet in place
	case Sidecars:
		// Validations on Sidecars are not yet in place
	case ServiceRoles:
		objectCheckers = []ObjectChecker{noServiceChecker}
	case ServiceRoleBindings:
		roleBindChecker := checkers.ServiceRoleBindChecker{RBACDetails: rbacDetails}
		objectCheckers = []ObjectChecker{roleBindChecker}
	default:
		err = fmt.Errorf("object type not found: %v", objectType)
	}

	close(errChan)
	// 判断资源获取的时候是否出现错误,如果有错误就返回
	for e := range errChan {
		if e != nil { // Check that default value wasn't returned
			return nil, err
		}
	}
	
	// 如果没有对应的Checker,返回空结果
	if objectCheckers == nil {
		return models.IstioValidations{}, err
	}
	
	// 运行 Checker 并返回结果
	// FilterByKey 是筛选出当前要判断的资源类型
	return runObjectCheckers(objectCheckers).FilterByKey(models.ObjectTypeSingular[objectType], object), nil
}

接下来看下每个 Checker 的逻辑是什么样的

网关配置验证(GatewayChecker)
func (g GatewayChecker) Check() models.IstioValidations {
	// 多个命名空间下 gateway 配置检测
	validations := gateways.MultiMatchChecker{
		GatewaysPerNamespace: g.GatewaysPerNamespace,
	}.Check()

	// 单个命名空间下 gateway 配置检测
	for _, nssGw := range g.GatewaysPerNamespace {
		for _, gw := range nssGw {
			if gw.GetObjectMeta().Namespace == g.Namespace {
				validations.MergeValidations(g.runSingleChecks(gw))
			}
		}
	}

	return validations
}

为了让大家有一个更直观的体现,这里贴一个官方的 gateway 配置例子。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: my-gateway
  namespace: some-config-namespace
spec:
  selector:
    app: my-gateway-controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - uk.bookinfo.com
    - eu.bookinfo.com
    tls:
      httpsRedirect: true # sends 301 redirect for http requests
  - port:
      number: 443
      name: https-443
      protocol: HTTPS
    hosts:
    - uk.bookinfo.com
    - eu.bookinfo.com
    tls:
      mode: SIMPLE # enables HTTPS on this port
      serverCertificate: /etc/certs/servercert.pem
      privateKey: /etc/certs/privatekey.pem
  - port:
      number: 9443
      name: https-9443
      protocol: HTTPS
    hosts:
    - "bookinfo-namespace/*.bookinfo.com"
    tls:
      mode: SIMPLE # enables HTTPS on this port
      credentialName: bookinfo-secret # fetches certs from Kubernetes secret
  - port:
      number: 9080
      name: http-wildcard
      protocol: HTTP
    hosts:
    - "*"
  - port:
      number: 2379 # to expose internal service via external port 2379
      name: mongo
      protocol: MONGO
    hosts:
    - "*"

下面先看下多网关验证的方法。

// 检查网关之间是是否有着相同的 host + port 的组合
func (m MultiMatchChecker) Check() models.IstioValidations {
	validations := models.IstioValidations{}
	m.existingList = make([]Host, 0)

	for _, nsG := range m.GatewaysPerNamespace {
		for _, g := range nsG {
			gatewayRuleName := g.GetObjectMeta().Name
			// 读取配置中的 servers 字段
			if specServers, found := g.GetSpec()["servers"]; found {
				if servers, ok := specServers.([]interface{}); ok {
					// 遍历 servers 中的数据,也就是 port 和 hosts 的组合
					for i, def := range servers {
						if serverDef, ok := def.(map[string]interface{}); ok {
							// 组装 hosts 和 端口号,存储在数组中
							hosts := parsePortAndHostnames(serverDef)
							// 遍历 hosts 数组
							for hi, host := range hosts {
								// 赋值 serverIndex、hostIndex 和 Gateway 的名称
								host.ServerIndex = i
								host.HostIndex = hi
								host.GatewayRuleName = gatewayRuleName
								// 查找配置是否有重复
								duplicate, dhosts := m.findMatch(host)、
								// 如果有重复,添加错误信息,并且把 index 也加入,方便定位
								if duplicate {
									validations = addError(validations, gatewayRuleName, i, hi)
									// 把与当前信息重复的 gateway 信息也加入
									for _, dh := range dhosts {
										validations = addError(validations, dh.GatewayRuleName, dh.ServerIndex, dh.HostIndex)
									}
								}
								m.existingList = append(m.existingList, host)
							}
						}
					}
				}
			}
		}
	}
	return validations
}

这是hosts存储的数据结构

type Host struct {
	Port            int
	Hostname        string
	ServerIndex     int
	HostIndex       int
	GatewayRuleName string
}

看下程序是如何找到重复的 Gateway 配置的,也就是 findMatch 方法。

// findMatch 方法使用了线性的方式进行查找,在将来有可能会成为性能瓶颈
func (m MultiMatchChecker) findMatch(host Host) (bool, []Host) {
	duplicates := make([]Host, 0)
	// 遍历已存在的hosts
	for _, h := range m.existingList {
		// 如果监听的 port 相同,就继续进行比较
		if h.Port == host.Port {
			// 只要其中一个为通配符(*)、那么两者肯定会重复
			if host.Hostname == wildCardMatch || h.Hostname == wildCardMatch {
				duplicates = append(duplicates, h)
				break
			}

			// 将 * 替换为 .* ,这么做的原因是因为 * 不是标准的正则表达式写法
			// * 的含义是匹配前面的子表达式任意次。例如,zo*能匹配“z”,也能匹配“zo”以及“zoo”。
			// 所以这里 * 前面要有表达式,也就是 . 。. 的含义是匹配除“\n”和"\r"之外的任何单个字符
			// 这里替换为 .* 就可以代表任意匹配
			current := strings.ToLower(strings.Replace(host.Hostname, "*", ".*", -1))
			previous := strings.ToLower(strings.Replace(h.Hostname, "*", ".*", -1))

			// 在字符串的前后加上 ^ 和 $ 定位符
			// ^ 是匹配输入字符串开始的位置
			// $ 是匹配输入字符串结尾的位置
			// 这么做的原因是让字符串的首位完全匹配,防止出现 "example.com" 会和 "foo.example.com" 匹配的情况
			current_re := strings.Join([]string{"^", current, "$"}, "")
			previous_re := strings.Join([]string{"^", previous, "$"}, "")
			
			// 进行正则表达式比配,如果有匹配,就加到数组中
			if regexp.MustCompile(current_re).MatchString(previous) ||
				regexp.MustCompile(previous_re).MatchString(current) {
				duplicates = append(duplicates, h)
				break
			}
		}
	}
	return len(duplicates) > 0, duplicates
}

接下来看下单个 gateway 是如何进行检查的。

func (g GatewayChecker) runSingleChecks(gw kubernetes.IstioObject) models.IstioValidations {
	key, validations := EmptyValidValidation(gw.GetObjectMeta().Name, GatewayCheckerType)

	// 组装 checker,这里使用的是 SelectorChecker
	enabledCheckers := []Checker{
		gateways.SelectorChecker{
			WorkloadList: g.WorkloadList,
			Gateway:      gw,
		},
	}
	// 这里理论上只有一个 checker,不用遍历也可以
	for _, checker := range enabledCheckers {
		// 进行单个 gateway 检查
		checks, validChecker := checker.Check()
		validations.Checks = append(validations.Checks, checks...)
		validations.Valid = validations.Valid && validChecker
	}

	return models.IstioValidations{key: validations}
}

进入到 check 的逻辑中。

// 检查 gateway 的 selector 标签在同一命名空间下是否有匹配的服务(workload)
// 这里代码只是检测有没有匹配的 workload,但是实际上如果这个 gateway 要生效的话,需要有对应的 pod 在运行
// 如果只是有 deployment,而没有 pod,gateway 同样是不起作用
func (s SelectorChecker) Check() ([]*models.IstioCheck, bool) {
	validations := make([]*models.IstioCheck, 0)

	// 获取 selector 区域的属性配置
	if selectorSpec, found := s.Gateway.GetSpec()["selector"]; found {
		if selectors, ok := selectorSpec.(map[string]interface{}); ok {
			// 设置 map 结构来存储 selector,key 是标签名称,value 是标签内容
			labelSelectors := make(map[string]string, len(selectors))
			// 遍历所有 selector 属性
			for k, v := range selectors {
				labelSelectors[k] = v.(string)
			}
			// 判断是否有 workload 匹配
			// 如果没有找到匹配,构建验证结果
			if !s.hasMatchingWorkload(labelSelectors) {
				validation := models.Build("gateways.selector", "spec/selector")
				validations = append(validations, &validation)
			}
		}
	}

	return validations, len(validations) == 0
}
func (s SelectorChecker) hasMatchingWorkload(labelSelector map[string]string) bool {
	selector := labels.SelectorFromSet(labels.Set(labelSelector))

	// 特殊情况,如果是官方的两个 label,那么不再进行 workload 检测
	if selector.Matches(IstioIngressGatewayLabels) || selector.Matches(IstioEgressGatewayLabels) {
		return true
	}
	
	// 遍历 workload 列表
	for _, wl := range s.WorkloadList.Workloads {
		wlLabelSet := labels.Set(wl.Labels)
		// 找到一个匹配的就返回
		if selector.Matches(wlLabelSet) {
			return true
		}
	}
	return false
}