1. 项目概述:Helm CEL 插件,为你的 Helm Chart 注入表达力

在 Kubernetes 生态里,Helm 是当之无愧的包管理标准。我们写 Chart,定义 values.yaml ,然后通过 helm install helm upgrade 将应用部署到集群。但不知道你有没有遇到过这样的场景:一个复杂的 Chart, values.yaml 文件里几十个甚至上百个配置项,你小心翼翼地修改了几个参数,执行 helm upgrade ,结果因为某个值类型不对、范围不对,或者依赖关系没满足,导致整个发布失败,甚至回滚。更头疼的是,Helm 自带的 values.schema.json (JSON Schema)虽然能做一些基础校验,但它的表达能力有限,写起来也相当繁琐,尤其是涉及到跨字段的逻辑校验时,简直让人抓狂。

这就是 idsulik/helm-cel 这个项目要解决的问题。它是一个 Helm 插件,用 Common Expression Language (CEL) 来替代或增强 JSON Schema,为你的 Helm Chart 提供强大、灵活且极具表达力的值验证能力。简单来说,它让你能用一种接近编程语言的、声明式的方式来定义你的配置规则。比如,你可以轻松写出这样的规则:“如果 service.type NodePort ,那么 nodePort 必须在 30000-32767 之间,否则 nodePort 字段必须为空”。这种条件逻辑,用原生的 JSON Schema 实现起来非常别扭,而用 CEL 则是一行表达式的事。

我是在为一个内部中间件 Chart 设计复杂的配置验证时发现这个工具的。当时我们要求某些配置项必须成对出现,某些数值范围依赖于另一个配置项的值,用 JSON Schema 写出来的验证逻辑又长又难维护。直到用了 helm-cel ,整个配置验证的清晰度和可维护性都上了一个台阶。接下来,我就结合自己的使用经验,带你深入了解一下这个工具,从设计思路到实战避坑,希望能帮你把 Helm Chart 的配置管理提升到一个新的水平。

2. 核心设计思路:为什么是 CEL?

在深入使用之前,我们得先搞清楚,为什么这个插件选择 CEL 作为验证语言,而不是其他选择,比如 Rego(Open Policy Agent 用的)、或者直接写 Go 插件。

2.1 CEL 是什么?它解决了什么问题?

CEL 的全称是 Common Expression Language,最初由 Google 设计,用于在其安全策略和配置验证场景中提供一种安全、可移植、高性能的表达式语言。它的核心设计目标有几个:

  1. 安全性 :CEL 表达式是在一个沙箱环境中求值的,它无法访问文件系统、网络或进行任何有副作用的操作。这对于验证来自不可信来源(如用户输入)的数据至关重要。
  2. 确定性 :相同的输入和表达式,总是产生相同的输出,没有随机性。
  3. 高性能 :CEL 表达式通常会被编译成可执行的中间代码,执行速度很快。
  4. 强类型 :CEL 是静态类型的,支持整数、浮点数、字符串、布尔值、列表、映射等基本类型,并且有清晰的类型检查规则。

在 Kubernetes 领域,CEL 已经被广泛采纳。最著名的例子就是 Kubernetes 1.25 引入的 Validating Admission Policy ,它允许你使用 CEL 来编写准入控制策略,而不再仅仅依赖 Webhook。这意味着 helm-cel 的设计理念与 Kubernetes 社区的发展方向是一致的。你用 CEL 为 Helm Chart 写的验证规则,其语法和思想可以很自然地迁移到集群侧的准入策略中,形成配置管理的“左移”(在部署前验证)和“右移”(在准入时验证)的统一。

2.2 与 JSON Schema 的对比:从“是什么”到“应该怎样”

Helm 原生的 values.schema.json 主要回答的问题是:“这个配置项 是什么 类型?” 它是一个结构描述器。

  • 优点 :标准、简单,对于基础类型和格式校验(如字符串格式、枚举值)很有效。
  • 缺点 :表达能力弱。很难(或非常冗长)表达“ 应该怎样 ”的逻辑。例如:
    • “字段 A 和字段 B 不能同时存在。”
    • “当字段 X 为 true 时,字段 Y 必须被设置且大于 10。”
    • “这个列表里的所有元素都必须满足某个条件。”

helm-cel values.cel.yaml 则专注于回答:“这些配置值 应该怎样 才有效?” 它是一个逻辑断言器。

  • 优点 :极强的逻辑表达能力。你可以使用 && (与)、 || (或)、 ! (非)等逻辑运算符,以及丰富的标准库函数(字符串处理、列表操作、类型检查等),构建复杂的业务规则。
  • 缺点 :不擅长描述纯粹的数据结构。它通常需要和 values.schema.json (或 Chart 的默认结构)配合使用,一个定义骨架,一个定义血肉规则。

实操心得:混合使用策略 在我的项目中,我通常两者都用。 values.schema.json 用来保证最基本的类型安全和必填字段,比如 image.repository 必须是字符串。而所有复杂的业务逻辑校验,比如资源配额合理性、端口冲突检查、环境特定的约束,全部交给 values.cel.yaml 。这样分工明确,各司其职。

2.3 插件架构浅析

helm-cel 本身是一个用 Go 编写的标准 Helm 插件。安装后,它会向 Helm CLI 注册一个 cel 子命令。当你运行 helm cel validate 时,插件会做以下几件事:

  1. 加载与合并 :加载指定的 values.yaml 文件(支持多个,按顺序合并),形成一个完整的配置值对象。
  2. 加载规则 :加载指定的 values.cel.yaml 文件(同样支持多个),解析其中的 rules expressions
  3. 编译与求值 :使用 Go 的 CEL 库( google/cel-go )将每条规则的 expr 字符串编译成可执行程序。然后,将上一步得到的配置值对象作为输入,对每条规则进行求值。
  4. 输出结果 :根据求值结果( true false )和规则的 severity ,生成成功、警告或错误信息,并以相应的退出码结束。

这个过程是纯函数式的,没有副作用,非常适合集成到 CI/CD 流水线中。

3. 从安装到上手:快速构建验证防线

3.1 多种安装方式及其适用场景

项目提供了几种安装方式,选择哪种取决于你的使用环境。

1. 标准安装(推荐给所有 Helm 用户) 这是最直接的方式,使用 Helm 自带的插件管理器。

helm plugin install https://github.com/idsulik/helm-cel

安装完成后,直接运行 helm cel --help 验证是否成功。这种方式将插件安装在你的本地 $HELM_PLUGINS 目录下,与你的 Helm 环境完全集成。

2. Docker 方式(适合 CI/CD 或隔离环境) 如果你在 Docker 容器内运行 CI 任务,或者不想污染本地环境,Docker 镜像是很好的选择。

# 拉取最新镜像
docker pull idsulik/helm-cel:latest
# 或指定版本,生产环境推荐用固定版本
docker pull idsulik/helm-cel:2.1.2

使用方式略有不同,你需要将你的 Chart 目录挂载到容器内:

docker run --rm -v $(pwd)/my-chart:/charts idsulik/helm-cel:latest validate /charts

这种方式确保了验证环境的一致性,非常适合在 Jenkins、GitLab CI 等流水线中使用。

3. 从源码构建(适合开发者或需要定制) 如果你想了解内部机制、调试问题或参与贡献,可以从源码构建。

git clone https://github.com/idsulik/helm-cel
cd helm-cel
# 确保已安装 Go 1.22+
make install

make install 会编译并将插件安装到你的 Helm 插件目录。这对于测试新功能或修复特定问题非常有用。

注意事项:网络问题 在国内环境,直接从 GitHub 安装插件或拉取 Docker 镜像可能会遇到网络延迟。对于 Docker 方式,可以考虑先将镜像推送到内部的镜像仓库。对于 helm plugin install ,如果失败,可以尝试先 git clone 仓库到本地,然后使用 helm plugin install /local/path/to/helm-cel 进行本地安装。

3.2 创建你的第一个验证规则

假设我们有一个最简单的 Chart,用于部署一个 Web 服务,它的 values.yaml 如下:

replicaCount: 2
image:
  repository: nginx
  tag: "1.21"
service:
  type: ClusterIP
  port: 80
resources:
  requests:
    memory: "128Mi"
    cpu: "100m"
  limits:
    memory: "256Mi"
    cpu: "200m"

我们想添加一些验证规则:

  1. replicaCount 必须至少为 1。
  2. image.tag 不能是 latest (避免浮动标签导致的不确定性)。
  3. service.port 必须在 1-65535 之间。
  4. 如果设置了资源限制( limits ),那么请求( requests )也必须设置,且 limits 必须大于等于 requests

在 Chart 根目录下创建 values.cel.yaml 文件:

rules:
  # 规则 1: 副本数校验
  - expr: "values.replicaCount >= 1"
    desc: "replicaCount must be at least 1"

  # 规则 2: 镜像标签校验
  - expr: 'values.image.tag != "latest"'
    desc: "image.tag cannot be 'latest' for production stability"

  # 规则 3: 服务端口校验
  - expr: "values.service.port >= 1 && values.service.port <= 65535"
    desc: "service.port must be a valid port number"

  # 规则 4: 资源请求与限制的关联校验
  - expr: >
      !has(values.resources.limits) ||
      (
        has(values.resources.requests) &&
        has(values.resources.limits.memory) &&
        has(values.resources.limits.cpu) &&
        // 注意:这里需要解析字符串如 "128Mi",实际项目中可能需要更复杂的逻辑
        // 此处简化为例,假设它们已是可比较的数字
        values.resources.limits.memory >= values.resources.requests.memory &&
        values.resources.limits.cpu >= values.resources.requests.cpu
      )
    desc: "If resources.limits are set, then resources.requests must also be set and limits must be >= requests"

注意 :规则 4 的表达式比较复杂,使用了 > YAML 块标量符号来书写多行表达式,并使用了 has() 函数来检查字段是否存在。在实际中,比较 "128Mi" 这样的字符串需要先转换,我们稍后在高级用法里会讲到。

现在,运行验证:

helm cel validate .

如果一切配置正确,你会看到绿色的 ✅ Values validation successful! 输出。

3.3 基础命令详解

helm cel 有两个核心子命令: validate generate

validate 命令 :这是最常用的命令,用于验证配置。

  • --values-file, -v : 指定要验证的 values 文件。默认是 values.yaml 。你可以指定多个,用逗分隔或多次使用 -v 标志。 文件顺序很重要 ,后面的文件会覆盖前面的同名配置,这与 helm install -f file1.yaml -f file2.yaml 的行为一致。
  • --rules-file, -r : 指定规则文件。默认是 values.cel.yaml 。同样支持多个文件,所有规则会被合并,但表达式名必须唯一。
  • --output, -o : 输出格式。 text (默认,人类可读)、 json yaml (机器可读,用于 CI)。

一个复杂的验证命令可能长这样:

helm cel validate ./my-chart \
  -v values.yaml \
  -v environments/prod/overrides.yaml \
  -r values.cel.yaml \
  -r rules/network.cel.yaml \
  -r rules/security.cel.yaml \
  -o json

这个命令会合并 values.yaml overrides.yaml 得到最终值,然后应用三个规则文件中的所有规则,并以 JSON 格式输出结果。

generate 命令 :这是一个辅助命令,可以根据现有的 values.yaml 文件结构,自动生成一个基础的 values.cel.yaml 规则骨架。这对于快速开始或为一个遗留 Chart 添加基础验证很有用。

helm cel generate ./my-chart --force

--force 会覆盖已存在的 values.cel.yaml 文件。生成的内容主要是基于字段存在性的检查( has(values.xxx) ),你需要在此基础上补充具体的业务逻辑规则。

4. 规则编写实战:从基础到高级模式

掌握了基本命令后,我们来深入探讨如何编写强大而清晰的 CEL 验证规则。这是 helm-cel 的核心价值所在。

4.1 规则文件结构与组织

一个规则文件的基本结构如下:

# 可选的表达式定义区,用于定义可复用的子表达式
expressions:
  isValidPort: "values.port >= 1 && values.port <= 65535"
  isValidHostname: 'values.hostname.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$")'

# 核心规则区
rules:
  - expr: "has(values.requiredField)" # 使用原生表达式
    desc: "requiredField must be set"
    severity: error # 可选,默认是 error

  - expr: "${isValidPort}" # 引用自定义表达式
    desc: "Port must be valid"

  - expr: 'values.type == "external" ? ${isValidHostname} : true' # 条件逻辑中使用
    desc: "Hostname must be valid if service is external"

规则组织的最佳实践 :不要把所有规则都堆在一个 values.cel.yaml 文件里。随着 Chart 变复杂,规则会变得难以管理。建议按功能模块拆分:

my-chart/
├── Chart.yaml
├── values.yaml
└── cel/
    ├── 00-required.cel.yaml      # 全局必填字段校验
    ├── 10-resources.cel.yaml     # 资源配额校验
    ├── 20-network.cel.yaml       # 网络、服务、Ingress校验
    ├── 30-storage.cel.yaml       # 存储卷校验
    └── 99-custom.cel.yaml        # 业务特定逻辑校验

然后在验证时通过 -r cel/*.cel.yaml 来加载所有规则。注意文件名前缀数字可以帮助控制加载顺序(虽然规则本身没有顺序依赖,但这样更清晰)。

4.2 常用 CEL 函数与技巧

CEL 提供了丰富的标准函数库。以下是在 Helm Chart 验证中最常用的一些:

1. 存在性检查 ( has() ) 这是最基础的函数,用于检查一个字段是否存在于 values 映射中。

- expr: "has(values.image.repository)"
  desc: "Docker image repository is required"

检查嵌套字段:

- expr: "has(values.image) && has(values.image.repository)"
  desc: "Image repository is required"
# 更简洁的写法(如果 values.image 可能不存在,会安全地返回 false)
- expr: "has(values.image.repository)"
  desc: "Image repository is required"

2. 类型检查 ( type() ) 确保字段是预期的类型。

- expr: "type(values.replicaCount) == int"
  desc: "replicaCount must be an integer"
- expr: "type(values.tolerations) == list"
  desc: "tolerations must be a list"
- expr: 'type(values.nodeSelector) == map'
  desc: "nodeSelector must be a map/dictionary"

3. 字符串操作 ( matches() , contains() , startsWith() ) 正则表达式是验证字符串格式的利器。

# 验证镜像标签格式(类似 v1.2.3)
- expr: 'values.image.tag.matches("^v?[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9]+)?$")'
  desc: "Image tag must follow semantic versioning pattern"

# 验证 Kubernetes 标签值
- expr: 'values.appLabel.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") && size(values.appLabel) <= 63'
  desc: "appLabel must be a valid Kubernetes label value"

# 检查是否包含子字符串
- expr: '!values.image.repository.contains("localhost/")'
  desc: "Image repository should not point to localhost in production"

4. 列表与映射操作 ( size() , all() , exists() , map() ) 处理数组和对象结构。

# 检查列表长度
- expr: "size(values.extraEnv) <= 20"
  desc: "Cannot have more than 20 extra environment variables"

# 检查列表中所有元素是否满足条件 (all)
- expr: "values.containers.all(c, has(c.image))"
  desc: "All containers must have an image specified"

# 检查列表中是否存在满足条件的元素 (exists)
- expr: 'values.ingress.hosts.exists(h, h.host.matches("^.*\\.example\\.com$"))'
  desc: "At least one ingress host must be under example.com domain"

# 对列表元素进行转换和检查 (map)
- expr: "values.ports.map(p, p.containerPort).all(port, port >= 30000 && port <= 32767)"
  desc: "All containerPorts in NodePort range must be between 30000 and 32767"

5. 数字与比较运算 基本的数学和逻辑运算。

- expr: "values.replicaCount >= 1 && values.replicaCount <= 10"
  desc: "replicaCount must be between 1 and 10"

# 百分比检查
- expr: "values.autoscaling.targetCPUUtilizationPercentage >= 50 && values.autoscaling.targetCPUUtilizationPercentage <= 90"
  desc: "CPU utilization target must be between 50% and 90%"

# 利用三元运算符进行条件逻辑
- expr: 'values.service.type == "NodePort" ? values.service.nodePort >= 30000 && values.service.nodePort <= 32767 : true'
  desc: "NodePort must be in range 30000-32767 when service type is NodePort"

4.3 高级验证模式示例

让我们看几个更贴近真实场景的复杂例子。

示例1:资源请求与限制的关联性及格式校验 Kubernetes 资源请求和限制的格式如 “128Mi” “0.5” ,需要解析。我们可以写一个辅助表达式来转换。

expressions:
  # 将 Kubernetes 内存字符串(如 "128Mi", "2Gi")转换为以 Mi 为单位的整数
  memoryToMi: |
    (mem) -> 
      mem.endsWith('Mi') ? int(mem.replace('Mi', '')) :
      mem.endsWith('Gi') ? int(mem.replace('Gi', '')) * 1024 :
      mem.endsWith('Ki') ? int(mem.replace('Ki', '')) / 1024 :
      /*** 这里可以扩展更多单位 ***/
      -1  # 无效格式返回 -1

rules:
  - expr: >
      !has(values.resources) || 
      !has(values.resources.limits) || 
      (
        has(values.resources.requests) &&
        has(values.resources.limits.memory) &&
        has(values.resources.limits.cpu) &&
        has(values.resources.requests.memory) &&
        has(values.resources.requests.cpu) &&
        ${memoryToMi}(values.resources.limits.memory) >= ${memoryToMi}(values.resources.requests.memory) &&
        // 注意:CPU 的解析更复杂("100m" -> 0.1),此处简化,假设已是数字或可解析字符串
        double(values.resources.limits.cpu.replace('m', '')) / 1000.0 >= double(values.resources.requests.cpu.replace('m', '')) / 1000.0
      )
    desc: "If limits are set, requests must also be set and limits must be >= requests"

注意 :上面的 memoryToMi 表达式是一个 lambda 函数,但请注意,当前版本的 CEL 可能对复杂 lambda 的支持有限,且 helm-cel 的表达式引用通常用于布尔表达式片段。对于这种复杂的转换,更稳妥的做法是在规则外部预处理,或者使用多个规则分步校验格式和大小。这里主要是展示思路。

示例2:Ingress 主机与 TLS 证书的配对检查

rules:
  # 规则1: 如果指定了 TLS,那么每个 TLS 条目必须有对应的 hosts
  - expr: >
      !has(values.ingress) || 
      !has(values.ingress.tls) || 
      size(values.ingress.tls) == 0 ||
      values.ingress.tls.all(tlsEntry, 
        has(tlsEntry.hosts) && 
        size(tlsEntry.hosts) > 0 &&
        tlsEntry.hosts.all(host, host != "")
      )
    desc: "Each TLS configuration must have a non-empty list of hosts"

  # 规则2: 如果启用了 TLS,且 hosts 列表不为空,那么每个 host 都应该在 TLS 配置中找到(或使用通配符匹配)
  # 这是一个简化版,实际可能需要更复杂的匹配逻辑
  - expr: >
      !has(values.ingress) || 
      !has(values.ingress.hosts) ||
      size(values.ingress.hosts) == 0 ||
      !has(values.ingress.tls) ||
      size(values.ingress.tls) == 0 ||
      values.ingress.hosts.all(hostObj,
        values.ingress.tls.exists(tlsEntry, 
          tlsEntry.hosts.exists(tlsHost, tlsHost == hostObj.host || tlsHost == "*.example.com") # 示例通配符
        )
      )
    desc: "All ingress hosts should be covered by a TLS certificate"
    severity: warning # 可能只是警告,因为可能有默认证书

示例3:亲和性与反亲和性的约束

rules:
  # 检查 podAntiAffinity 的 requiredDuringSchedulingIgnoredDuringExecution 不能同时指定 labelSelector 和 topologyKey 为空
  - expr: >
      !has(values.affinity) ||
      !has(values.affinity.podAntiAffinity) ||
      !has(values.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution) ||
      values.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution.all(req,
        has(req.topologyKey) && req.topologyKey != "" &&
        has(req.labelSelector) &&
        has(req.labelSelector.matchExpressions)
      )
    desc: "Pod anti-affinity required rules must have both topologyKey and labelSelector.matchExpressions"

4.4 错误信息与严重级别

每条规则都可以指定一个 severity

  • error (默认):验证失败将导致整个验证过程失败,退出码为 1。用于必须遵守的规则。
  • warning :验证失败会显示警告信息,但验证结果仍为成功(有警告),退出码为 2。用于推荐性、最佳实践或可能有问题但非阻塞的规则。

合理使用 warning 非常重要。例如,你可以将“生产环境不应使用 latest 标签”设置为 warning ,这样开发环境可以快速测试,而 CI 流水线会发出提醒但不会阻断部署。

rules:
  - expr: 'values.image.tag == "latest"'
    desc: "Using 'latest' tag is not recommended for production"
    severity: warning

5. 集成到 CI/CD 流水线:让验证成为部署守门员

helm-cel 的真正威力在于将其集成到自动化流程中。它清晰的退出码(0成功,1错误,2警告)和结构化输出(JSON/YAML)使其成为 CI/CD 流水线的理想选择。

5.1 基础 Git Hooks 集成

在团队开发中,可以在 Git 的 pre-commit pre-push 钩子中加入验证,确保提交到仓库的 Chart 配置都是有效的。

一个简单的 .pre-commit-config.yaml 配置示例:

repos:
  - repo: local
    hooks:
      - id: helm-cel-validate
        name: Validate Helm Chart values with CEL
        entry: bash -c 'for chart in $(find . -name "Chart.yaml" -type f | xargs dirname); do echo "Validating $chart"; helm cel validate "$chart" || exit 1; done'
        language: system
        files: '^.*/values\.yaml$|^.*/values\.cel\.yaml$'
        pass_filenames: false

这个钩子会在你提交任何 values.yaml values.cel.yaml 文件时,递归地查找并验证项目中的所有 Helm Chart。

5.2 GitHub Actions 工作流示例

在 GitHub Actions 中,你可以创建一个专门的验证工作流,或者将其作为测试工作流的一部分。

name: Validate Helm Charts

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Helm
        uses: azure/setup-helm@v3
        with:
          version: 'latest'

      - name: Install helm-cel plugin
        run: |
          helm plugin install https://github.com/idsulik/helm-cel

      - name: Find and validate all Helm charts
        run: |
          # 查找所有 Chart.yaml 文件
          find . -name 'Chart.yaml' -type f | while read chart_file; do
            chart_dir=$(dirname "$chart_file")
            echo "🔍 Validating chart in: $chart_dir"
            
            # 运行验证,捕获输出和退出码
            if output=$(helm cel validate "$chart_dir" -o json 2>&1); then
              exit_code=$?
              echo "$output" | jq -r '.result.errors[]? | "❌ ERROR: \(.description) (path: \(.path))"' >&2
              echo "$output" | jq -r '.result.warnings[]? | "⚠️ WARNING: \(.description) (path: \(.path))"' >&2
              
              if [[ $exit_code -eq 1 ]]; then
                echo "❌ Validation failed with errors for $chart_dir"
                exit 1
              elif [[ $exit_code -eq 2 ]]; then
                echo "⚠️ Validation passed with warnings for $chart_dir"
                # 可以选择让 warnings 也导致失败,取决于团队策略
                # exit 1
              else
                echo "✅ Validation successful for $chart_dir"
              fi
            else
              echo "❌ Failed to run validation for $chart_dir"
              exit 1
            fi
          done

这个工作流会检查仓库中的所有 Chart,并以 JSON 格式解析输出,分别处理错误和警告。你可以根据团队策略决定是否让警告( exit code 2 )也导致工作流失败。

5.3 GitLab CI 流水线示例

对于 GitLab CI,配置也类似:

stages:
  - validate

helm-cel-validation:
  stage: validate
  image: alpine/helm:latest
  before_script:
    - apk add --no-cache git
    - helm plugin install https://github.com/idsulik/helm-cel
  script:
    - |
      find . -name 'Chart.yaml' | while read chart; do
        chart_dir=$(dirname "$chart")
        echo "Validating $chart_dir"
        helm cel validate "$chart_dir" || {
          # 如果 exit code 是 1 (错误),则失败
          if [ $? -eq 1 ]; then
            echo "Validation failed with ERRORS for $chart_dir"
            exit 1
          fi
          # 如果 exit code 是 2 (​​警告),​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​

更多推荐