diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 884d471..37debce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, macos-latest] go-version: [1.24.0] arch: [amd64, arm64] @@ -54,7 +54,7 @@ jobs: - name: Build binary env: - GOOS: linux + GOOS: ${{ matrix.os == 'macos-latest' && 'darwin' || 'linux' }} GOARCH: ${{ matrix.arch }} CGO_ENABLED: 0 run: | @@ -64,7 +64,11 @@ jobs: . # Create checksum file - shasum -a 256 "dist/${{ env.BINARY_NAME }}-${{ matrix.os }}-${{ matrix.arch }}" > "dist/${{ env.BINARY_NAME }}-${{ matrix.os }}-${{ matrix.arch }}.sha256" + if [[ "${{ matrix.os }}" == "macos-latest" ]]; then + shasum -a 256 "dist/${{ env.BINARY_NAME }}-${{ matrix.os }}-${{ matrix.arch }}" > "dist/${{ env.BINARY_NAME }}-${{ matrix.os }}-${{ matrix.arch }}.sha256" + else + shasum -a 256 "dist/${{ env.BINARY_NAME }}-${{ matrix.os }}-${{ matrix.arch }}" > "dist/${{ env.BINARY_NAME }}-${{ matrix.os }}-${{ matrix.arch }}.sha256" + fi # List files for debugging ls -la dist/ diff --git a/README.md b/README.md index 21ddb3e..bb62b6a 100644 --- a/README.md +++ b/README.md @@ -17,55 +17,47 @@ 3. 安装 operator 4. 运行测试,再到 步骤2 升级到新版本 operator -## 配置文件 +## 运行升级 -```yaml -operatorConfig: - workspace: /Users/mac/Desktop/kychen/alaudadevops/gitlab-operator - namespace: gitlab-ce-operator # operator 部署 ns - name: gitlab-ce-operator # operator 名称 -upgradePaths: # 定义升级路径,可以包含多个 - - name: v17.8 upgrade to v17.11 # 升级名称 - versions: # 定义升级路经 - - name: v17.8 # 版本名称 - testCommand: | # 执行测试指令 - # git checkout feat/upgrade-case-17.8 - # export REPORT=allure - # make prepare - gitlab17.8.test --godog.concurrency=1 --godog.format=allure --godog.tags=@prepare --bdd.cleanup=false - # testSubPath: v17.8.10 - bundleVersion: v17.8.10 # bundle 版本号 - - name: v17.11 # 版本名称 - testCommand: | - # git checkout feat/upgrade-case-17.11 - # export REPORT=allure - # make upgrade - gitlab17.11.test --godog.concurrency=1 --godog.format=allure --godog.tags=@upgrade --bdd.cleanup=false - # testSubPath: v17.11.1 - bundleVersion: v17.11.1 # bundle 版本号 +### 安装升级工具 + +通过源码安装 + +```sh +git clone https://github.com/AlaudaDevops/upgrade-test +cd upgrade-test +go build -o upgrade +``` + +在 [release](https://github.com/AlaudaDevops/upgrade-test/releases) 页面下载对应系统版本的二进制。 + +```bash +wget https://github.com/AlaudaDevops/upgrade-test/releases/download/v0.0.5/upgrade-ubuntu-latest-amd64 +mv upgrade-ubuntu-latest-amd64 upgrade && chmod +x upgrade +./upgrade ``` -## 测试用例的编写 +### 测试用例的编写 测试用例分文两个部分: 1. 数据准备。 - 数据准备多次执行应该具备幂等性,确保可以重复运行。 - - 需要添加 skip-clean-namespace 的 Tag 确保执行后实例不被清理。 + - 需要添加 `skip-clean-namespace` 的 Tag 确保执行后实例不被清理。 2. 检查升级数据。 - 和数据准备相对应,负责升级实例以及检查准备的数据是否丢失。 - - 存在多次升级时,需要添加 skip-clean-namespace 的 Tag 确保执行后实例不被清理。 + - 存在多次升级时,需要添加 `skip-clean-namespace` 的 Tag 确保执行后实例不被清理。 -## 本地运行 +### 编写配置文件 -本地可以使用 git 命令切换分支,达到更换测试case的目的,然后使用 make 命令执行case。配置示例: +将文件保存为 upgrade.yaml ```yaml operatorConfig: - workspace: /Users/mac/Desktop/kychen/alaudadevops/gitlab-operator + workspace: /app/testing/ # test case 执行位置 namespace: gitlab-ce-operator # operator 部署 ns name: gitlab-ce-operator # operator 名称 upgradePaths: # 定义升级路径,可以包含多个 @@ -73,57 +65,20 @@ upgradePaths: # 定义升级路径,可以包含多个 versions: # 定义升级路经 - name: v17.8 # 版本名称 testCommand: | # 执行测试指令 - git checkout feat/upgrade-case-17.8 - export REPORT=allure - make prepare - testSubPath: testing + gitlab17.8.test --godog.concurrency=1 --godog.format=allure --godog.tags=@prepare bundleVersion: v17.8.10 # bundle 版本号 + channel: stable - name: v17.11 # 版本名称 testCommand: | - git checkout feat/upgrade-case-17.11 - export REPORT=allure - make upgrade - testSubPath: testing + gitlab17.11.test --godog.concurrency=1 --godog.format=allure --godog.tags=@upgrade --bdd.cleanup=false bundleVersion: v17.11.1 # bundle 版本号 + channel: stable ``` -## 流水线中运行 - -流水线中运行需要提前构建好执行测试的二进制,以及升级工具。镜像目录结构如下: +### 运行测试 ```sh -├── /bin/ -│   └── gitlab17.11.test # 17.11 用例执行文件 -│   └── gitlab17.8.test # #17.8 用例执行文件 -│   └── upgrade # 升级测试工具 -├── /app/testing -│   └── v17.8 -│   └──── allure-results... # 测试所需文件 -│   └── v17.11 -│   └──── allure-results... # 测试所需文件 -│   └── config.yaml # 测试执行的 yaml 配置 -``` - -config yaml 配置如下: - -```yaml -operatorConfig: - workspace: /app/testing - namespace: gitlab-ce-operator # operator 部署 ns - name: gitlab-ce-operator # operator 名称 -upgradePaths: # 定义升级路径,可以包含多个 - - name: v17.8 upgrade to v17.11 # 升级名称 - versions: # 定义升级路经 - - name: v17.8 # 版本名称 - testCommand: | # 执行测试指令 - gitlab17.8.test --godog.concurrency=1 --godog.format=allure --godog.tags=@prepare --bdd.cleanup=false - testSubPath: v17.8 - bundleVersion: v17.8.10 # bundle 版本号 - testImage: testimage:v17.8.10 - - name: v17.11 # 版本名称 - testCommand: | - gitlab17.11.test --godog.concurrency=1 --godog.format=allure --godog.tags=@upgrade --bdd.cleanup=false - testSubPath: v17.11 - bundleVersion: v17.11.1 # bundle 版本号 - testImage: testimage:v17.8.10 +# 配置链接的集群 +export KUBECONFIG= +./upgrade --config upgrade.yaml ``` diff --git a/cmd/upgrade_command.go b/cmd/upgrade_command.go index c4474c1..82a0309 100644 --- a/cmd/upgrade_command.go +++ b/cmd/upgrade_command.go @@ -137,6 +137,12 @@ func (uc *UpgradeCommand) getKubeconfig() string { uc.kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") } } + + // If KUBECONFIG is not set, set it to the kubeconfig path from config file, which will be inherited by the shell running test commands + if os.Getenv("KUBECONFIG") == "" { + os.Setenv("KUBECONFIG", uc.kubeconfig) + } + return uc.kubeconfig } @@ -197,13 +203,14 @@ func (uc *UpgradeCommand) process(ctx context.Context, path config.UpgradePath) testCommand = version.TestCommand } - if version.TestSubPath == "" { - version.TestSubPath = "testing" + workspace := uc.config.OperatorConfig.Workspace + if version.TestSubPath != "" { + workspace = fmt.Sprintf("%s/%s", uc.config.OperatorConfig.Workspace, version.TestSubPath) } // Execute test commands if err := uc.execCommand(ctx, - fmt.Sprintf("%s/%s", uc.config.OperatorConfig.Workspace, version.TestSubPath), + workspace, testCommand); err != nil { return fmt.Errorf("failed to execute test command: %v", err) } diff --git a/configs/gitlab/config.yaml b/configs/demo.yaml similarity index 91% rename from configs/gitlab/config.yaml rename to configs/demo.yaml index 80c7232..983d573 100644 --- a/configs/gitlab/config.yaml +++ b/configs/demo.yaml @@ -1,5 +1,5 @@ operatorConfig: - workspace: /Users/mac/Desktop/kychen/alaudadevops/gitlab-operator + workspace: /Users/mac/Desktop/kychen/alaudadevops/gitlab-operator/testing namespace: gitlab-ce-operator name: gitlab-ce-operator upgradePaths: # 定义升级路径,可以包含多个 @@ -10,8 +10,10 @@ upgradePaths: # 定义升级路径,可以包含多个 export REPORT=allure TAGS=@prepare-17.8 make test bundleVersion: v17.8.10 + channel: stable - name: v17.11 # 版本名称 testCommand: | TAGS=@upgrade-17.11 GODOG_ARGS="--godog.format=allure --bdd.cleanup=false" make test bundleVersion: v17.11.1 + channel: stable diff --git a/pkg/config/config.go b/pkg/config/config.go index 5278d94..617eea2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,7 @@ package config import ( "os" + "time" "gopkg.in/yaml.v2" ) @@ -38,6 +39,14 @@ type OperatorConfig struct { // workspace is the path to the workspace directory Workspace string `yaml:"workspace,omitempty"` + // artifactPrefix is the prefix of the artifact to use, default is "operatorhub" + ArtifactPrefix string `yaml:"artifactPrefix,omitempty"` + + // interval is the interval to use for the operator, default is 5 seconds + Interval time.Duration `yaml:"interval,omitempty"` + // timeout is the timeout to use for the operator, default is 10 minutes + Timeout time.Duration `yaml:"timeout,omitempty"` + // command for running the operator, just for local operator, default is "make deploy" Command string `yaml:"command,omitempty"` } @@ -61,7 +70,7 @@ type Version struct { // testSubPath is the path to the test sub-directory, default is "testing" TestSubPath string `yaml:"testSubPath,omitempty"` // revision is the revision to use for the version - Revision string `yaml:"revision,omitempty"` + Channel string `yaml:"channel,omitempty"` } // LoadConfig loads the configuration from a YAML file @@ -88,5 +97,16 @@ func defaultConfig(config *Config) *Config { config.OperatorConfig.Type = "operatorhub" } + if config.OperatorConfig.ArtifactPrefix == "" { + config.OperatorConfig.ArtifactPrefix = "operatorhub" + } + + if config.OperatorConfig.Interval == 0 { + config.OperatorConfig.Interval = 5 * time.Second + } + if config.OperatorConfig.Timeout == 0 { + config.OperatorConfig.Timeout = 10 * time.Minute + } + return config } diff --git a/pkg/exec/exec.go b/pkg/exec/exec.go index fc6be2a..18777fa 100644 --- a/pkg/exec/exec.go +++ b/pkg/exec/exec.go @@ -12,6 +12,7 @@ type Command struct { Name string Args []string Dir string + Env []string } // CommandResult represents the result of a command execution @@ -43,6 +44,14 @@ func RunCommand(ctx context.Context, cmd Command) CommandResult { runCmd := exec.CommandContext(ctx, cmd.Name, cmd.Args...) runCmd.Dir = cmd.Dir + // Inherit current process environment variables + runCmd.Env = os.Environ() + + // Add custom environment variables if specified + if len(cmd.Env) > 0 { + runCmd.Env = append(runCmd.Env, cmd.Env...) + } + // Create buffers to capture output var stdoutBuf, stderrBuf bytes.Buffer diff --git a/pkg/operator/factory.go b/pkg/operator/factory.go index b274f82..e721a58 100644 --- a/pkg/operator/factory.go +++ b/pkg/operator/factory.go @@ -40,8 +40,8 @@ func NewOperatorFactory() *OperatorFactory { func (f *OperatorFactory) CreateOperator(operatorType OperatorType, options OperatorOptions) (OperatorInterface, error) { switch operatorType { case OperatorTypeLocal: - return local.NewLocalOperator(options.OperatorConfig.Workspace, options.OperatorConfig.Command) + return local.NewLocalOperator(options.OperatorConfig) default: - return operatorhub.NewOperator(options.Config, options.Namespace, options.Name) + return operatorhub.NewOperator(options.Config, options.OperatorConfig) } } diff --git a/pkg/operator/local/operator.go b/pkg/operator/local/operator.go index 8b36994..545ac27 100644 --- a/pkg/operator/local/operator.go +++ b/pkg/operator/local/operator.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/AlaudaDevops/upgrade-test/pkg/config" "github.com/AlaudaDevops/upgrade-test/pkg/exec" "knative.dev/pkg/logging" ) @@ -14,10 +15,10 @@ type LocalOperator struct { versionRevision map[string]string } -func NewLocalOperator(workDir string, command string) (*LocalOperator, error) { +func NewLocalOperator(options config.OperatorConfig) (*LocalOperator, error) { return &LocalOperator{ - workDir: workDir, - command: command, + workDir: options.Workspace, + command: options.Command, }, nil } diff --git a/pkg/operator/operatorhub/artifact_versiong.go b/pkg/operator/operatorhub/artifact_versiong.go index b48b03b..9adac7e 100644 --- a/pkg/operator/operatorhub/artifact_versiong.go +++ b/pkg/operator/operatorhub/artifact_versiong.go @@ -16,7 +16,7 @@ func (o *Operator) InstallArtifactVersion(ctx context.Context, version string) ( log := logging.FromContext(ctx) log.Infow("installing artifact version", "version", version) - artifact, err := o.GetResource(ctx, fmt.Sprintf("operatorhub-%s", o.name), systemNamespace, artifactGVR) + artifact, err := o.GetResource(ctx, o.artifact, systemNamespace, artifactGVR) if err != nil { return nil, fmt.Errorf("failed to get artifact: %v", err) } @@ -91,7 +91,7 @@ func (o *Operator) createArtifactVersion(ctx context.Context, version string, ar func (o *Operator) waitArtifactVersionPresent(ctx context.Context, name string) (*unstructured.Unstructured, error) { lastResource := &unstructured.Unstructured{} - err := wait.PollUntilContextTimeout(ctx, o.interval, o.timeout, false, func(ctx context.Context) (done bool, err error) { + err := wait.PollUntilContextTimeout(ctx, o.interval, o.timeout, true, func(ctx context.Context) (done bool, err error) { obj, err := o.client.Resource(artifactVersionGVR).Namespace(systemNamespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return false, err diff --git a/pkg/operator/operatorhub/operator.go b/pkg/operator/operatorhub/operator.go index a3a7d7a..1b84e01 100644 --- a/pkg/operator/operatorhub/operator.go +++ b/pkg/operator/operatorhub/operator.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/AlaudaDevops/upgrade-test/pkg/config" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -17,6 +18,7 @@ type Operator struct { client dynamic.Interface namespace string name string + artifact string timeout time.Duration interval time.Duration @@ -68,18 +70,24 @@ var ( ) // NewOperator creates a new Operator instance -func NewOperator(config *rest.Config, namespace, name string) (*Operator, error) { +func NewOperator(config *rest.Config, options config.OperatorConfig) (*Operator, error) { client, err := dynamic.NewForConfig(config) if err != nil { return nil, err } + artifact := options.Artifact + if artifact == "" { + artifact = fmt.Sprintf("%s-%s", options.ArtifactPrefix, options.Name) + } + return &Operator{ client: client, - namespace: namespace, - name: name, - timeout: 10 * time.Minute, - interval: 5 * time.Second, + namespace: options.Namespace, + name: options.Name, + artifact: artifact, + timeout: options.Timeout, + interval: options.Interval, }, nil } diff --git a/pkg/operator/operatorhub/subscription.go b/pkg/operator/operatorhub/subscription.go index f120215..ef47f87 100644 --- a/pkg/operator/operatorhub/subscription.go +++ b/pkg/operator/operatorhub/subscription.go @@ -9,7 +9,9 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" "knative.dev/pkg/logging" ) @@ -21,11 +23,11 @@ func (o *Operator) InstallSubscription(ctx context.Context, csv string) error { log := logging.FromContext(ctx) log.Infow("installing subscription", "csv", csv, "namespace", o.namespace) // Delete the subscription and csv if they exist - if err := o.client.Resource(subscriptionGVR).Namespace(o.namespace).Delete(ctx, o.name, metav1.DeleteOptions{}); err != nil && !errors.IsNotFound(err) { + if err := o.deleteResource(ctx, subscriptionGVR, o.name, o.namespace); err != nil { return fmt.Errorf("failed to delete old subscription: %v", err) } - if err := o.deleteCSV(ctx, csv, o.namespace); err != nil { + if err := o.deleteResource(ctx, csvGVR, csv, o.namespace); err != nil { return fmt.Errorf("failed to delete old csv: %v", err) } @@ -63,6 +65,36 @@ func (o *Operator) InstallSubscription(ctx context.Context, csv string) error { return nil } +func (o *Operator) deleteResource(ctx context.Context, gvr schema.GroupVersionResource, name, namespace string) error { + log := logging.FromContext(ctx) + + log.Infow("deleting old resource", "gvr", gvr, "name", name, "namespace", namespace) + var rsAbled dynamic.ResourceInterface + nsEnabled := o.client.Resource(gvr) + if namespace != "" { + rsAbled = nsEnabled.Namespace(namespace) + } + + err := rsAbled.Delete(ctx, name, metav1.DeleteOptions{}) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to delete resource %s: %v", name, err) + } + + log.Infow("waiting for resource to be deleted", "name", name, "namespace", namespace) + err = wait.PollUntilContextTimeout(ctx, o.interval, o.timeout, true, func(ctx context.Context) (done bool, err error) { + _, err = rsAbled.Get(ctx, name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + log.Infow("resource not found, deleting resource", "name", name, "namespace", namespace) + return true, nil + } + return false, err + }) + if err != nil { + return fmt.Errorf("failed to delete resource %s: %v", name, err) + } + return nil +} + func (o *Operator) createSubscription(ctx context.Context, name, namespace, csv string) (*unstructured.Unstructured, error) { log := logging.FromContext(ctx) @@ -125,28 +157,11 @@ func (o *Operator) createSubscription(ctx context.Context, name, namespace, csv return nil, fmt.Errorf("failed to create subscription after 3 attempts: %v", err) } -func (o *Operator) deleteCSV(ctx context.Context, name, namespace string) error { - o.client.Resource(csvGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) - err := wait.PollUntilContextTimeout(ctx, o.interval, o.timeout, true, func(ctx context.Context) (done bool, err error) { - _, err = o.client.Resource(csvGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if errors.IsNotFound(err) { - return true, nil - } - return false, nil - }) - if err != nil { - return fmt.Errorf("failed to delete csv %s: %v", name, err) - } - return nil -} - // waitInstallPlan waits for the subscription to have an install plan and returns the install plan name func (o *Operator) waitInstallPlan(ctx context.Context, name, namespace string) (string, error) { var installPlanName string - interval := 5 * time.Second - timeout := 10 * time.Minute - err := wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (done bool, err error) { + err := wait.PollUntilContextTimeout(ctx, o.interval, o.timeout, true, func(ctx context.Context) (done bool, err error) { obj, err := o.client.Resource(subscriptionGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil && !errors.IsNotFound(err) { return false, err @@ -186,10 +201,7 @@ func (o *Operator) waitInstallPlan(ctx context.Context, name, namespace string) } func (o *Operator) waitCSVReady(ctx context.Context, name, namespace string) error { - interval := 5 * time.Second - timeout := 10 * time.Minute - - err := wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (done bool, err error) { + err := wait.PollUntilContextTimeout(ctx, o.interval, o.timeout, true, func(ctx context.Context) (done bool, err error) { csv, err := o.client.Resource(csvGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil && !errors.IsNotFound(err) { return false, err