验证配置



通过验证已有的配置来学习 CUE 是最简单的方法之一,不需要改动任何现有的代码,所以影响和风险很低。

当我们验证已有配置的时候,将会设计一个结构然后通过 CUE 测试它。

一个简单的例子

让我们从 album 对象的一个简单例子开始,下面是对 album 的 JSON 和 CUE 表示。

album.json

{
  "album": {
    "artist": "Led Zeppelin",
    "title": "BBC Sessions",
    "date": "1997-11-11"
  }
}

album.cue

import "time"

#Album: {
	artist: string
	title:  string
	date:   string
	date:   time.Format("2006-01-02")
}

album: #Album

我们首先看下 CUE 中 #Album定义结构, 我们没有直接设置字段的数据(值),而是将其设置为类型 string

可以看到 date 字段重复了两次,CUE 可以让我们在文件中不断的对一个字段进行约束,甚至跨文件或跨 package 也可以(应对公司策略变化变得更容易)。

在不同的行对进行约束,和使用连接符(&)的效果是一样的,所以也可以写为 date: string & time.Format(...)

我们通过 CUE 的标准库添加了一个额外的 约束import "time" 然后用 time.Format("format") 强制限制日期的格式,日期格式和 Go 的是一样的。 j 通过 [time 的 Go 文档] (https://golang.org/pkg/time/) 了解更多 Go 格式的相关内容。

需要特别注意的是设置 format 的时候,时间格式是 Go 的诞生的时间(Mon Jan 2 15:04:05 MST 2006)。

可以从 CUE 的标准库 中获取更多的辅助类,但是要注意你不能在普通 CUE 文件中 tool/... 相关的包,它们是给 脚本层 保留的,我们之后将会介绍到。

#Album结构 定义好之后,我们需要将其赋值给 album。 由于我们目前构建数据的方式以及 CUE vet 的工作方式,这在我们的设置中是必需的。 随着学习的深入,我们将会看到其他的代码风格。

现在我们就能验证我们 ablum 的 JSON 文件了:

cue vet album.json album-schema.cue

这里没有任何输出,让我们看看错误长什么样。

如果我们将 album 的 date,修改为 97-11-11 然后重新运行同样的命令,你将会看到下面的错误信息:

$ cue vet album-lhs.json album-validate.cue 
error in call to time.Format: invalid time "97-11-11"

一个 Kubernetes 的例子

让我们校验下本站的 Kubernetes 文件,下面是一个 YAML 文件,从这里查看原始文件

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cuetorials
  namespace: websites
  labels:
    app: cuetorials
spec:
  selector:
    matchLabels:
      app: cuetorials

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  minReadySeconds: 5

  template:
    metadata:
      labels:
        app: cuetorials
    spec:
      containers:
      - name: website
        image: us.gcr.io/hof-io--develop/cuetorials.com:manual
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          protocol: "TCP"

        readinessProbe:
          httpGet:
            port: 80
          initialDelaySeconds: 6
          failureThreshold: 3
          periodSeconds: 10
        livenessProbe:
          httpGet:
            port: 80
          initialDelaySeconds: 6
          failureThreshold: 3
          periodSeconds: 10

---
apiVersion: v1
kind: Service
metadata:
  name: cuetorials
  namespace: websites
  labels:
    app: cuetorials
spec:
  selector:
    app: cuetorials
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: cuetorials
  namespace: websites
  labels:
    app: cuetorials
  annotations:
    kubernetes.io/tls-acme: "true"
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod

    kubernetes.io/configuration-snippet: |
      location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
        expires 1h;
        access_log off;
        add_header Cache-Control "public";
      }
      location / {
        expires -1;
      }      

spec:
  tls:
  - hosts:
    - cuetorials.com
    secretName: cuetorials-tls

  rules:
  - host: cuetorials.com
    http:
      paths:
        - backend:
            serviceName: cuetorials
            servicePort: 80


Schema v1

第一步,要给我们的 Kubernetes 资源定义一些顶级的 定义(Definition), 对每种资源类型都有定义,然后 #Schema 将使用析取符(|)来表明必须通过其中一个实体来进行联合,就像 “or” 的作用一样。

#Schema: #Deployment | #Service | #Ingress

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	...
}

每种类型都定义为 struct{}),并添加字段 apiVersionskind,然后通过结构体最后添加 ... 表明可以添加更多字段。 默认情况下, definition 是封闭的,任何未定义的字段将会导致错误。

apiVersionkind 都是具体的数值,表明我们要验证的配置必须完全匹配。

让我们通过以下命令验证 YAML:

cue vet cuetorials.yaml cuetorials.cue -d "#Schema"

额外增加的 -d "<path>"" 告诉 CUE 我们想要通过哪个值验证每个 Yaml 条目。

必须添加 -d 因为 CUE 默认会使用顶级定义进行验证,但是我们有三种不同的结构,每一种分别代表不同资源类型,所以我们也需要分隔符(|)和 -d 来让命令生效。


Schema v2

现在我们有了一些模板代码,让我们定义更多复杂的验证。

#Schema: #Deployment | #Service | #Ingress

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata: {
		name:       string
		namespace?: string
		labels: [string]:       string
		annotations?: [string]: string
	}
	spec: {
		selector: {
			matchLabels: [string]: string
		}
		strategy: {...}
		minReadySeconds: uint
		template: {...}
	}
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	metadata: {
		name:       string
		namespace?: string
		labels: [string]:       string
		annotations?: [string]: string
	}
	spec: {
		selector: [string]: string
		type: string
		ports: [...{...}]
	}
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	metadata: {
		name:       string
		namespace?: string
		labels: [string]:       string
		annotations?: [string]: string
	}
	spec: {...}
	...
}

有几个点我们需要注意:

  • 我们虽然已经增加了一些结构定义,但是并不是全部。我们仍然需要增加 ...,但是如果为了避免增加额外的字段,就需要将其去掉。
  • 最通用的 struct 是 {...},空 struct 是 {}
  • labels: [string]: string 在 CUE 中被称为 “模板(Template)",然后就可以像 struct 定义 label,但是 struct 的"字段"和"值"都是 string 类型。也可以将其定位为更复杂的结构。
  • ports: [...{...}] 是一个 struct 的列表,你可以用 ints: [...int] 定义一个 int 的列表。
  • namespaceannotations 后面有个 ? 表明它们是可选的。

Schema v3

你会注意到我们上个章节,有几个定义是重复的。

我们可以给 labelmetadata 使用一些可以复用的定义来减少重复,重复的部分可以用新定义的标签替换。

#Schema: #Deployment | #Service | #Ingress

#labels: [string]: string

#metadata: {
	name:       string
	namespace?: string
	labels:     #labels
	annotations?: [string]: string
}

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata:   #metadata
	spec: {
		selector: {
			matchLabels: #labels
		}
		strategy: {...}
		minReadySeconds: uint
		template: {...}
	}
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	metadata:   #metadata
	spec: {
		selector: #labels
		type:     string
		ports: [...{...}]
	}
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	metadata:   #metadata
	spec: {...}
	...
}

Schema v4

我们经常要确保 label 中特定的标签被指定,这样它们就能在资源不同的部分进行匹配。

所以将结构修改为:

  • 增加 labels: app: string 以确保 app 标签所有资源都有. 注意我们可以通过 path: to: nested: value 在一行定义被嵌套的资源
  • #Deployment.spec.selector.matchLabels#Service.spec.selector 使用 metadata.labels,这是为了在资源的不同部分也能确保 label 的值是相同的。
#Schema: #Deployment | #Service | #Ingress

#labels: [string]: string
#labels: app:      string

#metadata: {
	name:       string
	namespace?: string
	labels:     #labels
	annotations?: [string]: string
}

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata:   #metadata
	spec: {
		selector: {
			matchLabels: metadata.labels
		}
		strategy: {...}
		minReadySeconds: uint
		template: {...}
	}
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	metadata:   #metadata
	spec: {
		selector: metadata.labels
		type:     string
		ports: [...{...}]
	}
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	metadata:   #metadata
	spec: {...}
	...
}

最终的结构定义

这个定义我们还可以补充更多,把它当做一个读者的一个练习吧。 特别 port 的定义和验证应该比我们展示的更完善。

我们绝不会将你的邮箱分享给任何人。
2022 Hofstadter, Inc