文章目录
- Kiali中的Istio配置管理
- 配置获取及验证
- Api 路径
- 入口方法
- 验证配置方法( GetIstioObjectValidations)
- 网关配置验证(GatewayChecker)
Kiali中的Istio配置管理
在 Kiali 的管理界面中,最后一项是 Istio Config,在这里可以看到 Istio 定义的所有 CRD 的配置。
在上方 Istio Type 的选项框中,可以看到 Kiali 支持这些类型的配置查看,但是目前的配置验证只支持其中的部分配置。
如果配置验证有问题,会在列表的旁边显示红色的叹号,点击进去就可以看到错误的详情。
这里可以直接在界面上进行配置的修改,保存之后,如果修改的配置无误,红色标记就会消失。
接下来我们看下代码,这些功能是如何实现的。
配置获取及验证
在这里把重点放在单个配置的获取和验证上。
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 的结构图。
验证配置方法( 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
}