5.10. 使用 scorecard 工具验证 Operator

作为 Operator 作者,您可以使用 Operator SDK 中的 scorecard 工具来执行以下任务:

  • 验证您的 Operator 项目没有语法错误,并正确打包
  • 查看有关如何改进 Operator 的建议

5.10.1. 关于 scorecard 工具

虽然 Operator SDK bundle validate 子命令可为内容和结构验证本地捆绑包目录和远程捆绑包镜像,但您可以使用 scorecard 命令基于配置文件和测试镜像对 Operator 运行测试。这些测试在由 scorecard 配置并组成执行的测试镜像中实施。

Scorecard 假设它是在可以访问已配置的 Kubernetes 集群(如 OpenShift Container Platform)的情况下运行的。Scorecard 在 pod 中运行每个测试,从中聚合 pod 日志并将测试结果发送到控制台。Scorecard 内置了基本测试和 Operator Lifecycle Manager(OLM)测试,同时还提供了执行自定义测试定义的方法。

Scorecard 工作流

  1. 创建任何相关的自定义资源(CR)和 Operator 所需的所有资源
  2. 在 Operator 部署中创建代理容器,记录对 API 服务器的调用并运行测试
  3. 检查 CR 中的参数

Scorecard 测试不会假定要测试的 Operator 状态。为 Operator 创建 Operator 和 CR 超出了 scorecard 本身的范围。但是,如果测试是为创建资源而设计的,则 scorecard 测试可以创建其所需的任何资源。

scorecard 命令语法

$ operator-sdk scorecard <bundle_dir_or_image> [flags]

Scorecard 需要一个位置参数,它是指向 Operator 捆绑包的磁盘路径或捆绑包镜像的名称。

如需有关标记的更多信息,请运行:

$ operator-sdk scorecard -h

5.10.2. Scorecard 配置

Scorecard 工具使用一个配置来供您配置内部插件以及几个全局配置选项。测试是由名为 config.yaml 的配置文件驱动的,该文件由 make bundle 命令生成,位于 bundle/ 目录中:

./bundle
...
└── tests
    └── scorecard
        └── config.yaml

Scorecard 配置文件示例

kind: Configuration
apiversion: scorecard.operatorframework.io/v1alpha3
metadata:
  name: config
stages:
- parallel: true
  tests:
  - image: quay.io/operator-framework/scorecard-test:v1.25.4
    entrypoint:
    - scorecard-test
    - basic-check-spec
    labels:
      suite: basic
      test: basic-check-spec-test
  - image: quay.io/operator-framework/scorecard-test:v1.25.4
    entrypoint:
    - scorecard-test
    - olm-bundle-validation
    labels:
      suite: olm
      test: olm-bundle-validation-test

配置文件定义 scorecard 可执行的每个测试。Scorecard 配置文件的以下字段定义测试,如下所示:

配置字段描述

image

测试实现测试的容器镜像名称

entrypoint

测试镜像中调用的命令和参数来执行测试

labels

选择要运行的测试的 scorecard 定义或自定义标签

5.10.3. 内置 scorecard 测试

Scorecard 附带预定义的测试,这些测试被放在套件中:基本测试套件和 Operator Lifecycle Manager(OLM)套件。

表 5.16. 基本测试套件

测试描述短名称

Spec Block Exists

此测试会检查集群中创建的自定义资源(CR)以确保所有 CR 都有一个 spec 块。

basic-check-spec-test

表 5.17. OLM 测试套件

测试描述短名称

捆绑包验证

此测试会验证传递给 scorecard 的捆绑包中的捆绑包清单。如果捆绑包内容包含错误,那么测试结果输出中将包括验证器日志以及验证库中的错误消息。

olm-bundle-validation-test

Provided APIs Have Validation

此测试会验证提供的 CR 的自定义资源定义(CRD)是否包含一个验证部分,并且 CR 中检测到的每个 specstatus 字段是否已验证。

olm-crds-have-validation-test

Owned CRDs Have Resources Listed

此测试确保通过 cr-manifest 选项提供的每个 CR 的 CRD 在 ClusterServiceVersion(CSV)的 owned CRDs 部分中有一个 resources 子部分。如果测试检测到未在 resources 部分中列出的已使用资源,它会在测试结束时将它们列在建议中。为这个测试通过初始代码生成后,用户需要填写 resources 部分。

olm-crds-have-resources-test

Spec Fields With Descriptors

此测试会验证 CRs spec 部分中的每一个字段是否都在 CSV 中列出对应的描述符。

olm-spec-descriptors-test

Status Fields With Descriptors

此测试会验证 CRs status 部分中的每一个字段是否都在 CSV 中列出对应的描述符。

olm-status-descriptors-test

5.10.4. 运行 scorecard 工具

Operator SDK 在运行 init 命令后生成一组默认 Kustomize 文件。生成的默认 bundle/tests/scorecard/config.yaml 文件可立即用于针对 Operator 运行 scorecard 工具,或者您可以根据测试规格修改该文件。

先决条件

  • 使用 Operator SDK 生成的 operator 项目

流程

  1. 为 Operator 生成或重新生成捆绑包清单和元数据:

    $ make bundle

    此命令自动将 scorecard 注解添加到捆绑包元数据中,由 scorecard 命令用来运行测试。

  2. 针对 Operator 捆绑包的磁盘路径或捆绑包镜像的名称运行 scorecard:

    $ operator-sdk scorecard <bundle_dir_or_image>

5.10.5. Scorecard 输出

scorecard 命令的 --output 标志指定 scorecard 结果输出格式: textjson

例 5.15. JSON 输出片断示例

{
  "apiVersion": "scorecard.operatorframework.io/v1alpha3",
  "kind": "TestList",
  "items": [
    {
      "kind": "Test",
      "apiVersion": "scorecard.operatorframework.io/v1alpha3",
      "spec": {
        "image": "quay.io/operator-framework/scorecard-test:v1.25.4",
        "entrypoint": [
          "scorecard-test",
          "olm-bundle-validation"
        ],
        "labels": {
          "suite": "olm",
          "test": "olm-bundle-validation-test"
        }
      },
      "status": {
        "results": [
          {
            "name": "olm-bundle-validation",
            "log": "time=\"2020-06-10T19:02:49Z\" level=debug msg=\"Found manifests directory\" name=bundle-test\ntime=\"2020-06-10T19:02:49Z\" level=debug msg=\"Found metadata directory\" name=bundle-test\ntime=\"2020-06-10T19:02:49Z\" level=debug msg=\"Getting mediaType info from manifests directory\" name=bundle-test\ntime=\"2020-06-10T19:02:49Z\" level=info msg=\"Found annotations file\" name=bundle-test\ntime=\"2020-06-10T19:02:49Z\" level=info msg=\"Could not find optional dependencies file\" name=bundle-test\n",
            "state": "pass"
          }
        ]
      }
    }
  ]
}

例 5.16. 文本输出片段示例

--------------------------------------------------------------------------------
Image:      quay.io/operator-framework/scorecard-test:v1.25.4
Entrypoint: [scorecard-test olm-bundle-validation]
Labels:
	"suite":"olm"
	"test":"olm-bundle-validation-test"
Results:
	Name: olm-bundle-validation
	State: pass
	Log:
		time="2020-07-15T03:19:02Z" level=debug msg="Found manifests directory" name=bundle-test
		time="2020-07-15T03:19:02Z" level=debug msg="Found metadata directory" name=bundle-test
		time="2020-07-15T03:19:02Z" level=debug msg="Getting mediaType info from manifests directory" name=bundle-test
		time="2020-07-15T03:19:02Z" level=info msg="Found annotations file" name=bundle-test
		time="2020-07-15T03:19:02Z" level=info msg="Could not find optional dependencies file" name=bundle-test
注意

输出格式 spec 与 Test 类型布局匹配。

5.10.6. 选择测试

Scorecard 测试通过将 --selector CLI 标志设置为一组标签字符串来选择。如果没有提供选择器标志,则运行 scorecard 配置文件中的所有测试。

测试通过 scorecard 聚合并写入标准输出或 stdout 以序列方式运行。

流程

  1. 要选择单个测试(如 basic-check-spec-test),使用 --selector 标志来指定测试:

    $ operator-sdk scorecard <bundle_dir_or_image> \
        -o text \
        --selector=test=basic-check-spec-test
  2. 要选择一组测试(如 olm ),请指定所有 OLM 测试使用的标签:

    $ operator-sdk scorecard <bundle_dir_or_image> \
        -o text \
        --selector=suite=olm
  3. 要选择多个测试,按照以下语法使用 selector 标记指定测试名称:

    $ operator-sdk scorecard <bundle_dir_or_image> \
        -o text \
        --selector='test in (basic-check-spec-test,olm-bundle-validation-test)'

5.10.7. 启用并行测试

作为 Operator 作者,您可以使用 scorecard 配置文件为测试定义独立阶段。阶段会根据配置文件中定义的顺序按照顺序运行。一个阶段(stage)包含测试列表以及一个可配置的 parallel 设置。

默认情况,或当阶段把 parallel 明确设置为 false 时,阶段中的测试会按配置文件中定义的顺序运行。每次只运行一个测试有助于保证两个测试间不会相互交互和冲突。

但是,如果测试被设计为完全隔离,则可以实现并行化。

流程

  • 要并行运行一组隔离测试,在同一个阶段中包括它们,并把 parallel 设置为 true

    apiVersion: scorecard.operatorframework.io/v1alpha3
    kind: Configuration
    metadata:
      name: config
    stages:
    - parallel: true 1
      tests:
      - entrypoint:
        - scorecard-test
        - basic-check-spec
        image: quay.io/operator-framework/scorecard-test:v1.25.4
        labels:
          suite: basic
          test: basic-check-spec-test
      - entrypoint:
        - scorecard-test
        - olm-bundle-validation
        image: quay.io/operator-framework/scorecard-test:v1.25.4
        labels:
          suite: olm
          test: olm-bundle-validation-test
    1
    启用并行测试

    所有并行阶段中的测试都会同时执行,scorecard 会在进入下一阶段前等待所有测试完成。这使得测试可以更快地运行。

5.10.8. 自定义 scorecard 测试

scorecard 工具可按照以下强制约定运行自定义测试:

  • 测试在容器镜像内实施
  • 测试可以接受包含命令和参数的入口点
  • 测试以 JSON 格式生成 v1alpha3 scorecard 输出,在测试输出中没有无关的日志信息
  • 测试可在 /bundle 的共享挂载点获取捆绑包内容
  • 测试可以使用集群内客户端连接访问 Kubernetes API

如果测试镜像遵循上述指南,则可以使用其他编程语言编写自定义测试。

以下示例显示了在 Go 中写入的自定义测试镜像:

例 5.17. 自定义 scorecard 测试示例

// Copyright 2020 The Operator-SDK 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 main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"

	scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
	apimanifests "github.com/operator-framework/api/pkg/manifests"
)

// This is the custom scorecard test example binary
// As with the Redhat scorecard test image, the bundle that is under
// test is expected to be mounted so that tests can inspect the
// bundle contents as part of their test implementations.
// The actual test is to be run is named and that name is passed
// as an argument to this binary.  This argument mechanism allows
// this binary to run various tests all from within a single
// test image.

const PodBundleRoot = "/bundle"

func main() {
	entrypoint := os.Args[1:]
	if len(entrypoint) == 0 {
		log.Fatal("Test name argument is required")
	}

	// Read the pod's untar'd bundle from a well-known path.
	cfg, err := apimanifests.GetBundleFromDir(PodBundleRoot)
	if err != nil {
		log.Fatal(err.Error())
	}

	var result scapiv1alpha3.TestStatus

	// Names of the custom tests which would be passed in the
	// `operator-sdk` command.
	switch entrypoint[0] {
	case CustomTest1Name:
		result = CustomTest1(cfg)
	case CustomTest2Name:
		result = CustomTest2(cfg)
	default:
		result = printValidTests()
	}

	// Convert scapiv1alpha3.TestResult to json.
	prettyJSON, err := json.MarshalIndent(result, "", "    ")
	if err != nil {
		log.Fatal("Failed to generate json", err)
	}
	fmt.Printf("%s\n", string(prettyJSON))

}

// printValidTests will print out full list of test names to give a hint to the end user on what the valid tests are.
func printValidTests() scapiv1alpha3.TestStatus {
	result := scapiv1alpha3.TestResult{}
	result.State = scapiv1alpha3.FailState
	result.Errors = make([]string, 0)
	result.Suggestions = make([]string, 0)

	str := fmt.Sprintf("Valid tests for this image include: %s %s",
		CustomTest1Name,
		CustomTest2Name)
	result.Errors = append(result.Errors, str)
	return scapiv1alpha3.TestStatus{
		Results: []scapiv1alpha3.TestResult{result},
	}
}

const (
	CustomTest1Name = "customtest1"
	CustomTest2Name = "customtest2"
)

// Define any operator specific custom tests here.
// CustomTest1 and CustomTest2 are example test functions. Relevant operator specific
// test logic is to be implemented in similarly.

func CustomTest1(bundle *apimanifests.Bundle) scapiv1alpha3.TestStatus {
	r := scapiv1alpha3.TestResult{}
	r.Name = CustomTest1Name
	r.State = scapiv1alpha3.PassState
	r.Errors = make([]string, 0)
	r.Suggestions = make([]string, 0)
	almExamples := bundle.CSV.GetAnnotations()["alm-examples"]
	if almExamples == "" {
		fmt.Println("no alm-examples in the bundle CSV")
	}

	return wrapResult(r)
}

func CustomTest2(bundle *apimanifests.Bundle) scapiv1alpha3.TestStatus {
	r := scapiv1alpha3.TestResult{}
	r.Name = CustomTest2Name
	r.State = scapiv1alpha3.PassState
	r.Errors = make([]string, 0)
	r.Suggestions = make([]string, 0)
	almExamples := bundle.CSV.GetAnnotations()["alm-examples"]
	if almExamples == "" {
		fmt.Println("no alm-examples in the bundle CSV")
	}
	return wrapResult(r)
}

func wrapResult(r scapiv1alpha3.TestResult) scapiv1alpha3.TestStatus {
	return scapiv1alpha3.TestStatus{
		Results: []scapiv1alpha3.TestResult{r},
	}
}