diff --git a/Makefile b/Makefile index fe0029c..40bd59a 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ lint: golangci_lint .PHONY: gosec gosec: go_sec - $(GO_SEC) ./... + $(GO_SEC) -exclude-dir=hack ./... # go-get-tool will 'go install' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) diff --git a/hack/upgrade-rollouts-script/.gitignore b/hack/upgrade-rollouts-script/.gitignore new file mode 100644 index 0000000..2fd7cf2 --- /dev/null +++ b/hack/upgrade-rollouts-script/.gitignore @@ -0,0 +1,2 @@ +settings.env +argo-rollouts-manager diff --git a/hack/upgrade-rollouts-script/README.md b/hack/upgrade-rollouts-script/README.md new file mode 100644 index 0000000..16cc5f5 --- /dev/null +++ b/hack/upgrade-rollouts-script/README.md @@ -0,0 +1,33 @@ +# Update argo-rollouts-manager to latest release of Argo Rollouts + +The Go code and script this in this directory will automatically open a pull request to update the argo-rollouts-manager to the latest official argo-rollouts release: +- Update container image version in `default.go` +- Update `go.mod` to point to latest module version +- Update CRDs to latest +- Update target Rollouts version in `hack/run-upstream-argo-rollouts-e2e-tests.sh` +- Open Pull Request using 'gh' CLI + +## Instructions + +### Prerequisites +- GitHub CLI (_gh_) installed and on PATH +- Go installed an on PATH +- [Operator-sdk v1.28.0](https://github.com/operator-framework/operator-sdk/releases/tag/v1.28.0) installed (as of January 2024), and on PATH +- You must have your own fork of the [argo-rollouts-manager](https://github.com/argoproj-labs/argo-rollouts-manager) repository (example: `jgwest/argo-rollouts-manager`) +- Your local SSH key registered (e.g. `~/.ssh/id_rsa.pub`) with GitHub to allow git clone via SSH + +### Configure and run the tool + +```bash + +# Set required Environment Variables +export GITHUB_FORK_USERNAME="(your username here)" +export GH_TOKEN="(a GitHub personal access token that can clone/push/open PRs against argo-rollouts-manager repo)" + +# or, you can set these values in the settings.env file: +# cp settings_template.env settings.env +# Then set env vars in settings.env (which is excluded in the .gitignore) + +./init-repo.sh +./go-run.sh +``` diff --git a/hack/upgrade-rollouts-script/go-run.sh b/hack/upgrade-rollouts-script/go-run.sh new file mode 100755 index 0000000..ac34a6a --- /dev/null +++ b/hack/upgrade-rollouts-script/go-run.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +cd $SCRIPT_DIR + +# Read Github Token and Username from settings.env, if it exists +vars_file="$SCRIPT_DIR/settings.env" +if [[ -f "$vars_file" ]]; then + source "$vars_file" +fi + +# Run the upgrade code +go run . diff --git a/hack/upgrade-rollouts-script/go.mod b/hack/upgrade-rollouts-script/go.mod new file mode 100644 index 0000000..7d511ef --- /dev/null +++ b/hack/upgrade-rollouts-script/go.mod @@ -0,0 +1,8 @@ +module github.com/jgwest/argo-rollouts-release-job + +go 1.20 + +require ( + github.com/google/go-github/v58 v58.0.0 + github.com/google/go-querystring v1.1.0 // indirect +) diff --git a/hack/upgrade-rollouts-script/go.sum b/hack/upgrade-rollouts-script/go.sum new file mode 100644 index 0000000..703631d --- /dev/null +++ b/hack/upgrade-rollouts-script/go.sum @@ -0,0 +1,7 @@ +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw= +github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/hack/upgrade-rollouts-script/init-repo.sh b/hack/upgrade-rollouts-script/init-repo.sh new file mode 100755 index 0000000..572512b --- /dev/null +++ b/hack/upgrade-rollouts-script/init-repo.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +cd $SCRIPT_DIR + +# Read Github Token and Username from settings.env, if it exists +vars_file="$SCRIPT_DIR/settings.env" +if [[ -f "$vars_file" ]]; then + source "$vars_file" +fi + +# Clone fork of argo-rollouts-manager repo + +rm -rf "$SCRIPT_DIR/argo-rollouts-manager" || true + +git clone "git@github.com:$GITHUB_FORK_USERNAME/argo-rollouts-manager" +cd argo-rollouts-manager + +# Add a remote back to the original repo + +git remote add parent "git@github.com:argoproj-labs/argo-rollouts-manager" +git fetch parent + diff --git a/hack/upgrade-rollouts-script/main.go b/hack/upgrade-rollouts-script/main.go new file mode 100644 index 0000000..051e553 --- /dev/null +++ b/hack/upgrade-rollouts-script/main.go @@ -0,0 +1,447 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + + "os/exec" + + "github.com/google/go-github/v58/github" +) + +// These can be set while debugging +const ( + skipInitialPRCheck = false // default to false + + // if readOnly is true: + // - PRs will not be opened + // - Git commits will not be pushed to fork + // This is roughly equivalent to a dry run + readOnly = false // default to false + +) + +const ( + PRTitle = "Upgrade to Argo Rollouts " + argoRolloutsRepoOrg = "argoproj" + argoRolloutsRepoName = "argo-rollouts" + + argoprojlabsRepoOrg = "argoproj-labs" + argoRolloutsManagerRepoName = "argo-rollouts-manager" + + controllersDefaultGo = "controllers/default.go" +) + +func main() { + + pathToGitHubRepo := "argo-rollouts-manager" + + gitHubToken := os.Getenv("GH_TOKEN") + if gitHubToken == "" { + exitWithError(fmt.Errorf("missing GH_TOKEN")) + return + } + + client := github.NewClient(nil).WithAuthToken(gitHubToken) + + // 1) Check for existing version update PRs on the repo + + if !skipInitialPRCheck { + prList, _, err := client.PullRequests.List(context.Background(), argoprojlabsRepoOrg, argoRolloutsManagerRepoName, &github.PullRequestListOptions{}) + if err != nil { + exitWithError(err) + return + } + for _, pr := range prList { + if strings.HasPrefix(*pr.Title, PRTitle) { + exitWithError(fmt.Errorf("PR already exists")) + return + } + } + } + + // 2) Pull the latest releases from rollouts repo + + releases, _, err := client.Repositories.ListReleases(context.Background(), argoRolloutsRepoOrg, argoRolloutsRepoName, &github.ListOptions{}) + if err != nil { + exitWithError(err) + return + } + + var firstProperRelease *github.RepositoryRelease + + for _, release := range releases { + + if strings.Contains(*release.TagName, "rc") { + continue + } + firstProperRelease = release + break + } + + if firstProperRelease == nil { + exitWithError(fmt.Errorf("no release found")) + return + } + + newBranchName := "upgrade-to-rollouts-" + *firstProperRelease.TagName + + // 3) Create, commit, and push a new branch + if repoAlreadyUpToDate, err := createNewCommitAndBranch(*firstProperRelease.TagName, "quay.io/argoproj/argo-rollouts", newBranchName, pathToGitHubRepo); err != nil { + + if repoAlreadyUpToDate { + fmt.Println("* Exiting as target repository is already up to date.") + return + } + + exitWithError(err) + return + } + + if !readOnly { + + bodyText := "Update to latest release of Argo Rollouts" + + if firstProperRelease != nil && firstProperRelease.HTMLURL != nil && *firstProperRelease.HTMLURL != "" { + bodyText += ": " + *firstProperRelease.HTMLURL + } + + bodyText += ` +Before merging this PR, ensure you check the Argo Rollouts change logs and release notes: +- ensure there are no changes to the Argo Rollouts install YAML that we need to respond to with changes in the operator +- ensure there are no backwards incompatible API/behaviour changes in the change logs` + + // 4) Create PR if it doesn't exist + if stdout, stderr, err := runCommandWithWorkDir(pathToGitHubRepo, "gh", "pr", "create", + "-R", argoprojlabsRepoOrg+"/"+argoRolloutsManagerRepoName, + "--title", PRTitle+(*firstProperRelease.TagName), "--body", bodyText); err != nil { + fmt.Println(stdout, stderr) + exitWithError(err) + return + } + } + +} + +// return true if the argo-rollouts-manager repo is already up to date +func createNewCommitAndBranch(latestReleaseVersionTag string, latestReleaseVersionImage, newBranchName, pathToGitRepo string) (bool, error) { + + commands := [][]string{ + {"git", "stash"}, + {"git", "fetch", "parent"}, + {"git", "checkout", "main"}, + {"git", "rebase", "parent/main"}, + {"git", "checkout", "-b", newBranchName}, + } + + if err := runCommandListWithWorkDir(pathToGitRepo, commands); err != nil { + return false, err + } + + if repoTargetVersion, err := extractCurrentTargetVersionFromRepo(pathToGitRepo); err != nil { + return false, fmt.Errorf("unable to extract current target version from repo") + } else if repoTargetVersion == latestReleaseVersionTag { + return true, fmt.Errorf("target repository is already on the most recent version") + } + + if err := regenerateControllersDefaultGo(latestReleaseVersionTag, latestReleaseVersionImage, pathToGitRepo); err != nil { + return false, err + } + + if err := regenerateGoMod(latestReleaseVersionTag, pathToGitRepo); err != nil { + return false, err + } + + if err := regenerateArgoRolloutsE2ETestScriptMod(latestReleaseVersionTag, pathToGitRepo); err != nil { + return false, err + } + + if err := copyCRDsFromRolloutsRepo(latestReleaseVersionTag, pathToGitRepo); err != nil { + return false, fmt.Errorf("unable to copy rollouts CRDs: %w", err) + } + + commands = [][]string{ + {"go", "mod", "tidy"}, + {"make", "generate", "manifests"}, + {"make", "bundle"}, + {"make", "fmt"}, + {"git", "add", "--all"}, + {"git", "commit", "-s", "-m", PRTitle + latestReleaseVersionTag}, + } + if err := runCommandListWithWorkDir(pathToGitRepo, commands); err != nil { + return false, err + } + + if !readOnly { + commands = [][]string{ + {"git", "push", "-f", "--set-upstream", "origin", newBranchName}, + } + if err := runCommandListWithWorkDir(pathToGitRepo, commands); err != nil { + return false, err + } + } + + return false, nil + +} + +func copyCRDsFromRolloutsRepo(latestReleaseVersionTag string, pathToGitRepo string) error { + + rolloutsRepoPath, err := checkoutRolloutsRepoIntoTempDir(latestReleaseVersionTag) + if err != nil { + return err + } + + crdPath := filepath.Join(rolloutsRepoPath, "manifests/crds") + crdYamlDirEntries, err := os.ReadDir(crdPath) + if err != nil { + return err + } + + var crdYAMLs []string + for _, crdYamlDirEntry := range crdYamlDirEntries { + + if crdYamlDirEntry.Name() == "kustomization.yaml" { + continue + } + + if !crdYamlDirEntry.IsDir() { + crdYAMLs = append(crdYAMLs, crdYamlDirEntry.Name()) + } + } + + sort.Strings(crdYAMLs) + + // NOTE: If this line fails, check if any new CRDs have been added to Rollouts, and/or if they have changed the filenames. + // - If so, this will require verifying the changes, then updating this list + if !reflect.DeepEqual(crdYAMLs, []string{ + "analysis-run-crd.yaml", + "analysis-template-crd.yaml", + "cluster-analysis-template-crd.yaml", + "experiment-crd.yaml", + "rollout-crd.yaml"}) { + return fmt.Errorf("unexpected CRDs found: %v", crdYAMLs) + } + + destinationPath := filepath.Join(pathToGitRepo, "config/crd/bases") + for _, crdYAML := range crdYAMLs { + + destFile, err := os.Create(filepath.Join(destinationPath, crdYAML)) + if err != nil { + return fmt.Errorf("unable to create file for '%s': %w", crdYAML, err) + } + defer destFile.Close() + + srcFile, err := os.Open(filepath.Join(crdPath, crdYAML)) + if err != nil { + return fmt.Errorf("unable to open source file for '%s': %w", crdYAML, err) + } + defer srcFile.Close() + + _, err = io.Copy(destFile, srcFile) + if err != nil { + return fmt.Errorf("unable to copy file for '%s': %w", crdYAML, err) + } + + } + + return nil +} + +func checkoutRolloutsRepoIntoTempDir(latestReleaseVersionTag string) (string, error) { + + tmpDir, err := os.MkdirTemp("", "argo-rollouts-src") + if err != nil { + return "", err + } + + if _, _, err := runCommandWithWorkDir(tmpDir, "git", "clone", "https://github.com/argoproj/argo-rollouts"); err != nil { + return "", err + } + + newWorkDir := filepath.Join(tmpDir, "argo-rollouts") + + commands := [][]string{ + {"git", "checkout", latestReleaseVersionTag}, + } + + if err := runCommandListWithWorkDir(newWorkDir, commands); err != nil { + return "", err + } + + return newWorkDir, nil +} + +func runCommandListWithWorkDir(workingDir string, commands [][]string) error { + + for _, command := range commands { + + _, _, err := runCommandWithWorkDir(workingDir, command...) + if err != nil { + return err + } + } + return nil +} + +func regenerateGoMod(latestReleaseVersionTag string, pathToGitRepo string) error { + + // Format of string to modify: + // github.com/argoproj/argo-rollouts v1.6.3 + + path := filepath.Join(pathToGitRepo, "go.mod") + + fileBytes, err := os.ReadFile(path) + if err != nil { + return err + } + + var res string + + for _, line := range strings.Split(string(fileBytes), "\n") { + + if strings.Contains(line, "\tgithub.com/argoproj/argo-rollouts v") { + + res += "\tgithub.com/argoproj/argo-rollouts " + latestReleaseVersionTag + "\n" + + } else { + res += line + "\n" + } + + } + + if err := os.WriteFile(path, []byte(res), 0600); err != nil { + return err + } + + return nil + +} + +func regenerateArgoRolloutsE2ETestScriptMod(latestReleaseVersionTag string, pathToGitRepo string) error { + + // Format of string to modify: + // CURRENT_ROLLOUTS_VERSION=v1.6.4 + + path := filepath.Join(pathToGitRepo, "hack/run-upstream-argo-rollouts-e2e-tests.sh") + + fileBytes, err := os.ReadFile(path) + if err != nil { + return err + } + + var res string + + for _, line := range strings.Split(string(fileBytes), "\n") { + + if strings.Contains(line, "CURRENT_ROLLOUTS_VERSION=") { + + res += "CURRENT_ROLLOUTS_VERSION=" + latestReleaseVersionTag + "\n" + + } else { + res += line + "\n" + } + + } + + if err := os.WriteFile(path, []byte(res), 0600); err != nil { + return err + } + + return nil + +} + +// extractCurrentTargetVersionFromRepo read the contents of the argo-rollouts-manager repo and determine which argo-rollouts version is being targeted. +func extractCurrentTargetVersionFromRepo(pathToGitRepo string) (string, error) { + + // Style of text string to parse: + // DefaultArgoRolloutsVersion = "sha256:995450a0a7f7843d68e96d1a7f63422fa29b245c58f7b57dd0cf9cad72b8308f" //v1.4.1 + + path := filepath.Join(pathToGitRepo, controllersDefaultGo) + + fileBytes, err := os.ReadFile(path) + if err != nil { + return "", err + } + + for _, line := range strings.Split(string(fileBytes), "\n") { + if strings.Contains(line, "DefaultArgoRolloutsVersion") { + + indexOfForwardSlash := strings.LastIndex(line, "/") + if indexOfForwardSlash != -1 { + return strings.TrimSpace(line[indexOfForwardSlash+1:]), nil + } + + } + } + + return "", fmt.Errorf("no version found in '" + controllersDefaultGo + "'") +} + +func regenerateControllersDefaultGo(latestReleaseVersionTag string, latestReleaseVersionImage, pathToGitRepo string) error { + + // Style of text string to replace: + // DefaultArgoRolloutsVersion = "sha256:995450a0a7f7843d68e96d1a7f63422fa29b245c58f7b57dd0cf9cad72b8308f" //v1.4.1 + + path := filepath.Join(pathToGitRepo, controllersDefaultGo) + + fileBytes, err := os.ReadFile(path) + if err != nil { + return err + } + + var res string + + for _, line := range strings.Split(string(fileBytes), "\n") { + + if strings.Contains(line, "DefaultArgoRolloutsVersion") { + + res += " DefaultArgoRolloutsVersion = \"" + latestReleaseVersionTag + "\" // " + latestReleaseVersionTag + "\n" + + } else { + res += line + "\n" + } + + } + + if err := os.WriteFile(path, []byte(res), 0600); err != nil { + return err + } + + return nil + +} + +func runCommandWithWorkDir(workingDir string, cmdList ...string) (string, string, error) { + + fmt.Println(cmdList) + + cmd := exec.Command(cmdList[0], cmdList[1:]...) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Dir = workingDir + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + stdoutStr := stdout.String() + stderrStr := stderr.String() + + fmt.Println(stdoutStr, stderrStr) + + return stdoutStr, stderrStr, err + +} + +func exitWithError(err error) { + fmt.Println("ERROR:", err) + os.Exit(1) +} diff --git a/hack/upgrade-rollouts-script/settings_template.env b/hack/upgrade-rollouts-script/settings_template.env new file mode 100644 index 0000000..15aeb2c --- /dev/null +++ b/hack/upgrade-rollouts-script/settings_template.env @@ -0,0 +1,2 @@ +export GITHUB_FORK_USERNAME="(your username here)" +export GH_TOKEN="(a GitHub personal access token that can clone/push/open PRs against argo-rollouts-manager repo)" \ No newline at end of file