From 784246062faaf182f91948abde8274c524095a91 Mon Sep 17 00:00:00 2001 From: Jonathan West Date: Wed, 15 Oct 2025 11:35:23 -0400 Subject: [PATCH] chore: add script to update argocd dependencies and images Signed-off-by: Jonathan West --- .github/workflows/lint.yaml | 15 +- Makefile | 12 + build/util/Dockerfile | 4 +- common/defaults.go | 2 +- .../crd/bases/argoproj.io_applications.yaml | 2 +- .../bases/argoproj.io_applicationsets.yaml | 2 +- config/crd/bases/argoproj.io_appprojects.yaml | 2 +- controllers/argocd/applicationset_test.go | 4 +- controllers/argocd/dex_test.go | 2 +- go.mod | 1 + hack/update-dependencies-script/README.md | 19 + hack/update-dependencies-script/go.mod | 9 + hack/update-dependencies-script/go.sum | 10 + hack/update-dependencies-script/main.go | 549 ++++++++++++++++++ hack/update-dependencies-script/run.sh | 8 + hack/update-dependencies-script/utils.go | 142 +++++ 16 files changed, 773 insertions(+), 10 deletions(-) create mode 100644 hack/update-dependencies-script/README.md create mode 100644 hack/update-dependencies-script/go.mod create mode 100644 hack/update-dependencies-script/go.sum create mode 100644 hack/update-dependencies-script/main.go create mode 100755 hack/update-dependencies-script/run.sh create mode 100644 hack/update-dependencies-script/utils.go diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 6cbc64c75..8b6d677c8 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,4 +1,4 @@ -name: Code scans +name: "Validation, verifications, and lints" on: pull_request: paths-ignore: @@ -24,3 +24,16 @@ jobs: - name: Ensure there is no diff in code run: | git diff --ignore-matching-lines='.*createdAt:.*' --exit-code -- . + + verify_argo_cd_has_updated_dependencies: + name: Verify Argo CD has updated dependencies, for the given target version + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: "Call update dependencies Makefile target" + run: | + make update-dependencies + - name: Ensure there is no unexpected diff in repository artifacts + run: | + git diff --ignore-matching-lines='.*createdAt:.*' --exit-code -- . diff --git a/Makefile b/Makefile index 01c2f68a3..be6f272b8 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,13 @@ # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) VERSION ?= 0.17.0 +# ARGO_CD_TARGET_VERSION is the target version that argocd-operator will install. +# Update this when you upgrade the Argo CD dependencies of the project. +# After updating, call 'make update-dependencies'. +# Notes: +# - String should NOT begin with 'v' prefix, e.g. 'v3.1.1' +ARGO_CD_TARGET_VERSION ?= 3.1.8 + # Try to detect Docker or Podman CONTAINER_RUNTIME := $(shell command -v docker 2> /dev/null || command -v podman 2> /dev/null) @@ -361,3 +368,8 @@ if [ $$(printf '%s\n' $$requiredver $$currentver | sort -V | head -n1) = $$requi rm -rf $$TMP_DIR ;\ } endef + +# Updates upstream dependencies throughout the repository +.PHONY: update-dependencies +update-dependencies: + "hack/update-dependencies-script/run.sh" diff --git a/build/util/Dockerfile b/build/util/Dockerfile index def97e87c..4402fc377 100644 --- a/build/util/Dockerfile +++ b/build/util/Dockerfile @@ -1,5 +1,5 @@ -# Argo CD v3.1.1 -FROM quay.io/argoproj/argocd@sha256:a36ab0c0860c77159c16e04c7e786e7a282f04889ba9318052f0b8897d6d2040 AS argocd +# Argo CD v3.1.8 +FROM quay.io/argoproj/argocd@sha256:19ba7f44cba487c4a0c98ac336327c4df04383dff84f87ea1a578972eb62dd17 as argocd # Final Image FROM docker.io/library/ubuntu:24.04 diff --git a/common/defaults.go b/common/defaults.go index fe324589c..11749394c 100644 --- a/common/defaults.go +++ b/common/defaults.go @@ -70,7 +70,7 @@ const ( ArgoCDDefaultArgoImage = "quay.io/argoproj/argocd" // ArgoCDDefaultArgoVersion is the Argo CD container image digest to use when version not specified. - ArgoCDDefaultArgoVersion = "sha256:a36ab0c0860c77159c16e04c7e786e7a282f04889ba9318052f0b8897d6d2040" // v3.1.1 + ArgoCDDefaultArgoVersion = "sha256:19ba7f44cba487c4a0c98ac336327c4df04383dff84f87ea1a578972eb62dd17" // v3.1.8 // ArgoCDDefaultBackupKeyLength is the length of the generated default backup key. ArgoCDDefaultBackupKeyLength = 32 diff --git a/config/crd/bases/argoproj.io_applications.yaml b/config/crd/bases/argoproj.io_applications.yaml index c203b11a1..3d644d0e5 100644 --- a/config/crd/bases/argoproj.io_applications.yaml +++ b/config/crd/bases/argoproj.io_applications.yaml @@ -5887,4 +5887,4 @@ spec: type: object served: true storage: true - subresources: {} \ No newline at end of file + subresources: {} diff --git a/config/crd/bases/argoproj.io_applicationsets.yaml b/config/crd/bases/argoproj.io_applicationsets.yaml index 11ab6e42f..8cb86b7b9 100644 --- a/config/crd/bases/argoproj.io_applicationsets.yaml +++ b/config/crd/bases/argoproj.io_applicationsets.yaml @@ -17721,4 +17721,4 @@ spec: served: true storage: true subresources: - status: {} \ No newline at end of file + status: {} diff --git a/config/crd/bases/argoproj.io_appprojects.yaml b/config/crd/bases/argoproj.io_appprojects.yaml index 4324daf07..64b15b9b4 100644 --- a/config/crd/bases/argoproj.io_appprojects.yaml +++ b/config/crd/bases/argoproj.io_appprojects.yaml @@ -363,4 +363,4 @@ spec: - spec type: object served: true - storage: true \ No newline at end of file + storage: true diff --git a/controllers/argocd/applicationset_test.go b/controllers/argocd/applicationset_test.go index 4ef7f5558..e6806cf38 100644 --- a/controllers/argocd/applicationset_test.go +++ b/controllers/argocd/applicationset_test.go @@ -1481,7 +1481,7 @@ func TestGetApplicationSetContainerImage(t *testing.T) { cr.Spec.Image = "" cr.Spec.Version = "" out = getApplicationSetContainerImage(&cr) - assert.Equal(t, "quay.io/argoproj/argocd@sha256:a36ab0c0860c77159c16e04c7e786e7a282f04889ba9318052f0b8897d6d2040", out) + assert.Equal(t, "quay.io/argoproj/argocd@"+common.ArgoCDDefaultArgoVersion, out) // when env var is not set and spec image and version fields are set, spec fields should be returned cr.Spec.Image = "customimage" @@ -1502,5 +1502,5 @@ func TestGetApplicationSetContainerImage(t *testing.T) { cr.Spec.Version = "" os.Setenv(common.ArgoCDImageEnvName, "") out = getApplicationSetContainerImage(&cr) - assert.Equal(t, "customimage@sha256:a36ab0c0860c77159c16e04c7e786e7a282f04889ba9318052f0b8897d6d2040", out) + assert.Equal(t, "customimage@"+common.ArgoCDDefaultArgoVersion, out) } diff --git a/controllers/argocd/dex_test.go b/controllers/argocd/dex_test.go index 289a68d4d..f671bac6a 100644 --- a/controllers/argocd/dex_test.go +++ b/controllers/argocd/dex_test.go @@ -581,7 +581,7 @@ func TestReconcileArgoCD_reconcileDexDeployment_withUpdate(t *testing.T) { Containers: []corev1.Container{ { Name: "dex", - Image: "ghcr.io/dexidp/dex@sha256:b08a58c9731c693b8db02154d7afda798e1888dc76db30d34c4a0d0b8a26d913", + Image: "ghcr.io/dexidp/dex@sha256:b08a58c9731c693b8db02154d7afda798e1888dc76db30d34c4a0d0b8a26d913", // (v2.43.0) NOTE: this value is modified by dependency update script Command: []string{ "/shared/argocd-dex", "rundex", diff --git a/go.mod b/go.mod index 1e69c13fd..ab62fe253 100644 --- a/go.mod +++ b/go.mod @@ -175,6 +175,7 @@ require ( ) replace ( + // This replace block is from Argo CD v3.1.8 go.mod github.com/golang/protobuf => github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/grpc-gateway => github.com/grpc-ecosystem/grpc-gateway v1.16.0 diff --git a/hack/update-dependencies-script/README.md b/hack/update-dependencies-script/README.md new file mode 100644 index 000000000..f04b2f61d --- /dev/null +++ b/hack/update-dependencies-script/README.md @@ -0,0 +1,19 @@ +# update-dependencies-script + +This is a simple go-based script that will upgrade the upstream dependencies in argocd-operator. + +## To run this script: + +In `(root)/Makefile`, modify `ARGO_CD_TARGET_VERSION` to target Argo CD version. + +Example: +``` +ARGO_CD_TARGET_VERSION ?= 3.1.8 +``` + +Then run the script: +``` +make update-dependencies +``` + +See `hack/update-dependencies-script/main.go` for list dependencies that are updated. \ No newline at end of file diff --git a/hack/update-dependencies-script/go.mod b/hack/update-dependencies-script/go.mod new file mode 100644 index 000000000..d01eb4403 --- /dev/null +++ b/hack/update-dependencies-script/go.mod @@ -0,0 +1,9 @@ +module github.com/argoproj-labs/argocd-operator/dependency-upgrade + +go 1.20 + +require ( + github.com/google/go-github/v58 v58.0.0 + github.com/google/go-querystring v1.1.0 // indirect + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/hack/update-dependencies-script/go.sum b/hack/update-dependencies-script/go.sum new file mode 100644 index 000000000..8e0f040a6 --- /dev/null +++ b/hack/update-dependencies-script/go.sum @@ -0,0 +1,10 @@ +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/hack/update-dependencies-script/main.go b/hack/update-dependencies-script/main.go new file mode 100644 index 000000000..5a27a9f4f --- /dev/null +++ b/hack/update-dependencies-script/main.go @@ -0,0 +1,549 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" +) + +func main() { + + wd, err := os.Getwd() + if err != nil { + exitWithError(fmt.Errorf("unable to get working dir: %v", err)) + return + } + + argocdOperatorRoot, err := filepath.Abs(wd + "/../..") + if err != nil { + exitWithError(fmt.Errorf("unable to get absolute dir: %v", err)) + return + } + + // Version variable is in the form of 'v3.1.1', note the 'v' prefix + targetArgoCDVersion := readTargetVersionFromMakefile(argocdOperatorRoot) + + fmt.Println() + fmt.Println("Target Argo CD version from Makefile is", targetArgoCDVersion) + fmt.Println() + + // Clone Argo CD into a temporary directory + argoCDRepoRoot, err := cloneArgoCDRepoIntoTempDir(targetArgoCDVersion) + if err != nil { + exitWithError(fmt.Errorf("unable to checkout Argo CD: %v", err)) + return + } + + fmt.Println("Using Argo CD temporary directory:", argoCDRepoRoot) + + // Sanity test that that we have the correct root path for argocd-operator repo + if rootGoModExists, err := fileExists(filepath.Join(argocdOperatorRoot, "go.mod")); err != nil || !rootGoModExists { + exitWithError(fmt.Errorf("script should be run from 'hack/update-dependencies-script' directory: %v", err)) + return + } + + // update 'common/defaults.go': container images for dex, redis, redis HA, argo-cd, etc + argocdContainerInfo, dexContainerInfo := upgradeCommonDefaultsGo(argoCDRepoRoot, argocdOperatorRoot) + + // update Argo CD crds + updateArgoCDCRDs(argoCDRepoRoot, argocdOperatorRoot) + + // update go.mod and regenerate the manifests/bundle: acquire target argo cd version, and ensures replace block matches upstream + updateGoModAndRegenBundle(targetArgoCDVersion, argocdOperatorRoot, argoCDRepoRoot) + + // update build/util/Dockerfile: update to reference target upstream container image + updateBuildUtilDockerfile(argocdOperatorRoot, argocdContainerInfo) + + // update controllers/argocd/dex_test.go: test references a dex container image + replaceDexImageReferenceInDexTest(argocdOperatorRoot, dexContainerInfo) + + fmt.Println() + fmt.Println("Dependency update is complete:") + fmt.Println("- You may wish to use a comparison tool to compare the 'manifests/install.yaml' file from the old and new versions of argo-cd, to verify no additional operator changes are required. ") + fmt.Println("- You may wish to run 'make test' to verify the dependencies are in a consistent state.") + fmt.Println() + +} + +func readTargetVersionFromMakefile(argocdOperatorRoot string) string { + // Target version in Makefile looks like this: + // ARGO_CD_TARGET_VERSION ?= 3.1.1 + + fileToRead := filepath.Join(argocdOperatorRoot, "Makefile") + + bytes, err := os.ReadFile(fileToRead) + if err != nil { + exitWithError(fmt.Errorf("unable to dex_test.go: %v", err)) + return "" + } + + matches := grepForString(string(bytes), "ARGO_CD_TARGET_VERSION ?= ") + + if len(matches) != 1 { + exitWithError(fmt.Errorf("unexpected number of matches for ARGO_CD_TARGET_VERSION: %v", matches)) + return "" + } + + line := matches[0] + + if strings.Contains(line, "#") { + exitWithError(fmt.Errorf("version line should not contain any comments")) + return "" + } + + setIndex := strings.Index(line, "?=") + + res := strings.TrimSpace(line[setIndex+2:]) + + if !strings.HasPrefix(res, "v") { + res = "v" + res + } + + return res + +} + +func replaceDexImageReferenceInDexTest(argocdOperatorRoot string, dexContainerImage *processedContainerImage) { + fileToUpdate := filepath.Join(argocdOperatorRoot, "controllers", "argocd", "dex_test.go") + + lines := getFileContentsAsLines(fileToUpdate) + + // Entry to replace looks like this: + // Name: "dex", + // Image: "ghcr.io/dexidp/dex@sha256:d5f887574312f606c61e7e188cfb11ddb33ff3bf4bd9f06e6b1458efca75f604", + + newContent := "" + + match := false + + for idx := 0; idx < len(lines); idx++ { + if strings.HasPrefix(lines[idx], " Name: \"dex\",") && + strings.HasPrefix(lines[idx+1], " Image: \"ghcr.io/dexidp/dex@sha256:") { + + newContent += lines[idx] + "\n" // No modification of first line required + newContent += " Image: \"ghcr.io/dexidp/dex@" + dexContainerImage.sha256Digest + "\", // (" + dexContainerImage.version + ") NOTE: this value is modified by dependency update script\n" + + idx++ // Skip to the line after these 2 + match = true + } else { + newContent += lines[idx] + "\n" + } + } + + if !match { + exitWithError(fmt.Errorf("unable to locate reference to dex image in dex_test.go")) + return + } + + newContent = strings.TrimSpace(newContent) + "\n" + + if err := os.WriteFile(fileToUpdate, []byte(newContent), 0600); err != nil { + exitWithError(fmt.Errorf("unable to update build/util/Dockefile: %v", err)) + return + } + +} + +func updateBuildUtilDockerfile(argocdOperatorRoot string, argocdContainerImage *processedContainerImage) { + + fileToUpdate := filepath.Join(argocdOperatorRoot, "build", "util", "Dockerfile") + + lines := getFileContentsAsLines(fileToUpdate) + + // Entry to replace in Dockerfile looks like this: + + // # Argo CD v3.1.0-rc2 + // FROM quay.io/argoproj/argocd@sha256:dc4e00548b9e9fe31b6b2dca99b2278390faabd3610a04f4707dfddf66b5e90d as argocd + + newContent := "" + + match := false + + for idx := 0; idx < len(lines); idx++ { + // Replace the argocd image references in the Dockerfile with new version + if strings.HasPrefix(lines[idx], "# Argo CD v") && strings.HasPrefix(lines[idx+1], "FROM quay.io/argoproj/argocd@sha256") { + newContent += "# Argo CD " + argocdContainerImage.version + "\n" + newContent += "FROM quay.io/argoproj/argocd@" + argocdContainerImage.sha256Digest + " as argocd\n" + idx++ // Skip to the line after these 2 + match = true + } else { + newContent += lines[idx] + "\n" + } + } + + if !match { + exitWithError(fmt.Errorf("unable to locate reference to Argo CD container image in build/util/Dockerfile")) + return + } + + newContent = strings.TrimSpace(newContent) + "\n" + + if err := os.WriteFile(fileToUpdate, []byte(newContent), 0600); err != nil { + exitWithError(fmt.Errorf("unable to update build/util/Dockerfile: %v", err)) + return + } + +} + +// upgrade common/defaults.go +func upgradeCommonDefaultsGo(argoCDRepoRoot string, argocdOperatorRoot string) (*processedContainerImage, *processedContainerImage) { + + // Parse install YAML + fileToParse := filepath.Join(argoCDRepoRoot, "manifests/ha/install.yaml") + + installYamlBytes, err := os.ReadFile(fileToParse) + if err != nil { + exitWithError(fmt.Errorf("unable to read Argo CD install yaml")) + return nil, nil + } + + installYAMLContents := string(installYamlBytes) + + targetDexImageLine := grepForString(installYAMLContents, "ghcr.io/dexidp/dex:") + if len(targetDexImageLine) != 1 { + exitWithError(fmt.Errorf("unexpected target dex image value: %v", targetDexImageLine)) + return nil, nil + } + targetDexImage := stripImagePrefix(targetDexImageLine[0]) + + targetRedisImageLine := removeDuplicateLines(grepForString(installYAMLContents, "public.ecr.aws/docker/library/redis:")) + if len(targetRedisImageLine) != 1 { + exitWithError(fmt.Errorf("unexpected target redis image value: %v", targetRedisImageLine)) + return nil, nil + } + targetRedisImage := stripImagePrefix(targetRedisImageLine[0]) + + targetHAProxyImageLine := removeDuplicateLines(grepForString(installYAMLContents, "public.ecr.aws/docker/library/haproxy:")) + if len(targetHAProxyImageLine) != 1 { + exitWithError(fmt.Errorf("unexpected target haproxy image value: %v", targetHAProxyImageLine)) + return nil, nil + } + targetHAProxyImage := stripImagePrefix(targetHAProxyImageLine[0]) + + targetArgoCDImageLine := removeDuplicateLines(grepForString(installYAMLContents, "quay.io/argoproj/argocd:")) + if len(targetArgoCDImageLine) != 1 { + exitWithError(fmt.Errorf("unexpected target argo cd image value: %v", targetArgoCDImageLine)) + } + targetArgoCDImage := stripImagePrefix(targetArgoCDImageLine[0]) + + fmt.Println() + fmt.Println("Found the following images in Argo CD manifests:") + fmt.Println("-", targetArgoCDImage) + fmt.Println("-", targetDexImage) + fmt.Println("-", targetRedisImage) + fmt.Println("-", targetHAProxyImage) + + dexContainerInfo := retrieveSHA256DigestUsingSkopeo(targetDexImage) + redisContainerInfo := retrieveSHA256DigestUsingSkopeo(targetRedisImage) + haProxyContainerInfo := retrieveSHA256DigestUsingSkopeo(targetHAProxyImage) + argoCDContainerInfo := retrieveSHA256DigestUsingSkopeo(targetArgoCDImage) + + if err := replaceLineInCommonDefaultGo(argocdOperatorRoot, "ArgoCDDefaultArgoVersion", *argoCDContainerInfo); err != nil { + exitWithError(fmt.Errorf("unable to replace dex version: %v", err)) + return nil, nil + } + + if err := replaceLineInCommonDefaultGo(argocdOperatorRoot, "ArgoCDDefaultDexVersion", *dexContainerInfo); err != nil { + exitWithError(fmt.Errorf("unable to replace dex version: %v", err)) + return nil, nil + } + + if err := replaceLineInCommonDefaultGo(argocdOperatorRoot, "ArgoCDDefaultRedisVersionHA", *redisContainerInfo); err != nil { + exitWithError(fmt.Errorf("unable to replace redis HA version: %v", err)) + return nil, nil + } + + if err := replaceLineInCommonDefaultGo(argocdOperatorRoot, "ArgoCDDefaultRedisVersion", *redisContainerInfo); err != nil { + exitWithError(fmt.Errorf("unable to replace redis version: %v", err)) + return nil, nil + } + + if err := replaceLineInCommonDefaultGo(argocdOperatorRoot, "ArgoCDDefaultRedisHAProxyVersion", *haProxyContainerInfo); err != nil { + exitWithError(fmt.Errorf("unable to replace redis HA proxy version: %v", err)) + return nil, nil + } + + return argoCDContainerInfo, dexContainerInfo +} + +func updateArgoCDCRDs(argoCDRepoRoot string, argocdOperatorRoot string) { + + argoCDCRDSourcePath := filepath.Join(argoCDRepoRoot, "manifests", "crds") + + entries, err := os.ReadDir(argoCDCRDSourcePath) + if err != nil { + exitWithError(fmt.Errorf("unable to list Argo CD CRDS: %v", err)) + return + } + + var count int + for _, entry := range entries { + fname := entry.Name() + if entry.IsDir() || fname == "kustomization.yaml" { + continue + } + + if strings.HasSuffix(fname, ".yaml") { + count++ + } + } + + filesToCopy := map[string]string{ + // argo cd repo YAML filename -> argocd-operator YAML filename + "application-crd.yaml": "argoproj.io_applications.yaml", + "applicationset-crd.yaml": "argoproj.io_applicationsets.yaml", + "appproject-crd.yaml": "argoproj.io_appprojects.yaml", + } + + // Sanity test: The CRDs found in Argo CD directory should match the values in the map above + if len(filesToCopy) != count { + exitWithError(fmt.Errorf("unexpected number of YAML files found in Argo CD CRD directory '%s': %d", argoCDCRDSourcePath, count)) + return + } + + for k, v := range filesToCopy { + srcFile := filepath.Join(argoCDCRDSourcePath, k) + destFile := filepath.Join(argocdOperatorRoot, "config", "crd", "bases", v) + + if err := copyFile(srcFile, destFile); err != nil { + exitWithError(fmt.Errorf("unable to copy %s to %s: %v", srcFile, destFile, err)) + return + } + + } + +} + +// readReplaceBlockFromGoMod will read the following block from a go.mod: +// +// module github.com/argoproj-labs/argocd-operator +// +// go 1.24.6 +// require ( +// +// (...) +// +// ) +// replace ( +// +// // <=== The values from inside here are returned +// +// ) +// +// This function expects only one replace block to exist, and will fail if 0 or >1 are found. +func readReplaceBlockFromGoMod(pathToGoMod string) ([]string, int, int) { + lines := getFileContentsAsLines(pathToGoMod) + + replaceBlockStart := -1 + replaceBlockEnd := -1 + for x := 0; x < len(lines); x++ { + + line := lines[x] + if strings.HasPrefix(line, "replace (") { + + if replaceBlockStart != -1 { + exitWithError(fmt.Errorf("multiple replace blocks detected in argocd go.mod")) + return nil, replaceBlockStart, replaceBlockEnd + } + + replaceBlockStart = x + 1 + } + + if replaceBlockStart != -1 && strings.HasPrefix(line, ")") { + replaceBlockEnd = x - 1 + } + } + + if replaceBlockStart == -1 { + exitWithError(fmt.Errorf("replace block start not found")) + return nil, replaceBlockStart, replaceBlockEnd + } + + if replaceBlockEnd == -1 { + exitWithError(fmt.Errorf("replace block end not found")) + return nil, replaceBlockStart, replaceBlockEnd + } + + return lines, replaceBlockStart, replaceBlockEnd +} + +// copyGoModReplaceBlockFromArgoCDToArgoCDOperator will look at the replace ( ... ) block from argocd's go.mod, and copy it, as is, into the replace block of argocd-operator go.mod. +// - This code assumes that only 1 replace ( ... ) block exists in both argocd go.mod and argocd-operator go.mod +// - It also assumes that no other non-argo-cd values have been inserted into argocd-operator go.mold +func copyGoModReplaceBlockFromArgoCDToArgoCDOperator(argoCDRepoRoot string, argocdOperatorRoot string, targetArgoCDVersion string) { + + argoCDRepoRootGoModPath := filepath.Join(argoCDRepoRoot, "go.mod") + + replaceBlockFromArgoCDLines, replaceBlockStart, replaceBlockEnd := readReplaceBlockFromGoMod(argoCDRepoRootGoModPath) + replaceBlockFromArgoCD := replaceBlockFromArgoCDLines[replaceBlockStart:replaceBlockEnd] + + argocdOperatorGoModPath := filepath.Join(argocdOperatorRoot, "go.mod") + + argocdOperatorLines, argocdOperatorReplaceStart, argocdOperatorReplaceEnd := readReplaceBlockFromGoMod(argocdOperatorGoModPath) + + newArgoCDOperatorGoModFileContents := "" + + for x, line := range argocdOperatorLines { + if x == argocdOperatorReplaceStart { + + newArgoCDOperatorGoModFileContents += "\t// This replace block is from Argo CD " + targetArgoCDVersion + " go.mod\n" + + // inject the replace block from argocd's go.mod + for _, replaceBlockFromArgoCDLine := range replaceBlockFromArgoCD { + newArgoCDOperatorGoModFileContents += replaceBlockFromArgoCDLine + "\n" + } + + } else if x > argocdOperatorReplaceStart && x < argocdOperatorReplaceEnd { + // skip existing replace block in argocd-operator's go.mod + continue + } else { + newArgoCDOperatorGoModFileContents += line + "\n" + } + } + if err := os.WriteFile(argocdOperatorGoModPath, ([]byte)(newArgoCDOperatorGoModFileContents), 0600); err != nil { + exitWithError(fmt.Errorf("unable to write to file: %s %v", argocdOperatorGoModPath, err)) + return + } +} + +// updateGoModAndRegenBundle ensures that argocd-operator go.mod is update to date with latest from upstream argocd-operator +func updateGoModAndRegenBundle(targetArgoCDVersion string, argocdOperatorRoot string, argoCDRepoRoot string) { + err := runCommandListWithWorkDir(argocdOperatorRoot, + [][]string{ + {"go", "get", "github.com/argoproj/argo-cd/v3@" + targetArgoCDVersion}, + }) + if err != nil { + exitWithError(fmt.Errorf("unable to update argocd-operator go.mod")) + return + } + + copyGoModReplaceBlockFromArgoCDToArgoCDOperator(argoCDRepoRoot, argocdOperatorRoot, targetArgoCDVersion) + + err = runCommandListWithWorkDir(argocdOperatorRoot, + [][]string{ + {"go", "mod", "tidy"}, + {"rm", "-f", argocdOperatorRoot + "/bin/controller-gen"}, // Erase the controller-gen binary to ensure it is re-downloaded to the correct version + {"make", "generate", "manifests"}, + {"make", "bundle"}, + {"make", "fmt"}, + }) + if err != nil { + exitWithError(fmt.Errorf("unable to update argocd-operator go.mod")) + } + +} + +// replaceLineInCommonDefaultGo updates a line in 'common/defaults.go' beginning with 'variableToReplace' with a different container image digest and version comment +// +// Example: +// - From: +// ArgoCDDefaultArgoVersion = "sha256:dc4e00548b9e9fe31b6b2dca99b2278390faabd3610a04f4707dfddf66b5e90d" // v3.1.0-rc2 +// - To: +// ArgoCDDefaultArgoVersion = "sha256:a36ab0c0860c77159c16e04c7e786e7a282f04889ba9318052f0b8897d6d2040" // v3.1.1 +func replaceLineInCommonDefaultGo(pathToArgoCDGitRepoRoot string, variableToReplace string, toReplace processedContainerImage) error { + + path := filepath.Join(pathToArgoCDGitRepoRoot, "common/defaults.go") + + lines := getFileContentsAsLines(path) + + var res string + + var match bool + for _, line := range lines { + + if strings.Contains(line, "\t"+variableToReplace+" ") { + match = true + + res += "\t" + variableToReplace + " = \"" + toReplace.sha256Digest + "\" // " + toReplace.version + "\n" + + } else { + res += line + "\n" + } + + } + + if !match { + return fmt.Errorf("no match found for '%s'", variableToReplace) + } + + res = strings.TrimSpace(res) + "\n" + + if err := os.WriteFile(path, []byte(res), 0600); err != nil { + return err + } + + return nil + +} + +// cloneArgoCDRepoIntoTempDir clones a specific argo-cd version into a temp dir +func cloneArgoCDRepoIntoTempDir(latestReleaseVersionTag string) (string, error) { + + tmpDir, err := os.MkdirTemp("", "argo-cd-src") + if err != nil { + return "", err + } + + if _, _, err := runCommandWithWorkDir(tmpDir, "git", "clone", "https://github.com/argoproj/argo-cd"); err != nil { + return "", err + } + + newWorkDir := filepath.Join(tmpDir, "argo-cd") + + commands := [][]string{ + {"git", "checkout", latestReleaseVersionTag}, + } + + if err := runCommandListWithWorkDir(newWorkDir, commands); err != nil { + return "", err + } + + return newWorkDir, nil +} + +// retrieveSHA256DigestUsingSkopeo determines the SHA256 digest value for a given container image +func retrieveSHA256DigestUsingSkopeo(url string) *processedContainerImage { + + stdout, _, err := runCommandWithWorkDir("", "skopeo", "inspect", "--no-tags", "docker://"+url) + if err != nil { + exitWithError(fmt.Errorf("unexpected skopeo error: %v", err)) + return nil + } + + // Skopeo inspect output looks like this: + // { + // "Name": "quay.io/argoproj/argocd", + // "Digest": "sha256:a36ab0c0860c77159c16e04c7e786e7a282f04889ba9318052f0b8897d6d2040", + // "RepoTags": [], + // "Created": "2025-08-25T15:56:23.164856076Z", + // # (...) + // "Architecture": "amd64", + // "Os": "linux", + // } + + // Extract Digest value from JSON + var jsonMap map[string]any + if err := json.Unmarshal([]byte(stdout), &jsonMap); err != nil { + exitWithError(fmt.Errorf("unexpected unmarshal error: %v", err)) + return nil + } + digestVal := jsonMap["Digest"] + + if digestVal == "" { + exitWithError(fmt.Errorf("unable to extract digest val for %s", url)) + return nil + } + + return &processedContainerImage{ + version: url[strings.Index(url, ":")+1:], + sha256Digest: digestVal.(string), + } +} + +// processedContainerImage is the return values for processedContainerImage +type processedContainerImage struct { + version string // version string portion of container image URL, e.g. 'v3.1.1' + sha256Digest string // 'sha256:(...)' digest value of container image from skopeo. string includes 'sha256:' prefix +} diff --git a/hack/update-dependencies-script/run.sh b/hack/update-dependencies-script/run.sh new file mode 100755 index 000000000..b22ce19f1 --- /dev/null +++ b/hack/update-dependencies-script/run.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +cd $SCRIPT_DIR + +# Run the upgrade code +go run . diff --git a/hack/update-dependencies-script/utils.go b/hack/update-dependencies-script/utils.go new file mode 100644 index 000000000..45f455c2e --- /dev/null +++ b/hack/update-dependencies-script/utils.go @@ -0,0 +1,142 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +func runCommandListWithWorkDir(workingDir string, commands [][]string) error { + + for _, command := range commands { + + _, _, err := runCommandWithWorkDir(workingDir, command...) + if err != nil { + return err + } + } + return nil +} + +func runCommandWithWorkDir(workingDir string, cmdList ...string) (string, string, error) { + + fmt.Printf("%v:\n", 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) + + fmt.Println() + + return stdoutStr, stderrStr, err + +} + +func exitWithError(err error) { + fmt.Println("ERROR:", err) + os.Exit(1) +} + +func stripImagePrefix(line string) string { + line = strings.TrimSpace(line) + + if !strings.HasPrefix(line, "image:") { + exitWithError(fmt.Errorf("unexpected image format on line: %s", line)) + return "" + } + + return strings.TrimPrefix(line, "image: ") + +} + +func grepForString(contents string, str string) []string { + + var res []string + + for _, line := range strings.Split(contents, "\n") { + + if strings.Contains(line, str) { + + res = append(res, line) + } + + } + + return res +} + +func removeDuplicateLines(in []string) []string { + + mapRes := map[string]any{} + + for _, inVal := range in { + mapRes[inVal] = inVal + } + + var res []string + + for k := range mapRes { + res = append(res, k) + } + + return res +} + +func copyFile(srcParam, dstParam string) error { + srcFile, err := os.Open(srcParam) + if err != nil { + return fmt.Errorf("could not open source file %s: %w", srcParam, err) + } + + destFile, err := os.Create(dstParam) + if err != nil { + return fmt.Errorf("could not create destination file %s: %w", dstParam, err) + } + + _, err = io.Copy(destFile, srcFile) + if err != nil { + return fmt.Errorf("could not copy content from %s to %s: %w", srcParam, dstParam, err) + } + + return nil +} + +func fileExists(filename string) (bool, error) { + info, err := os.Stat(filename) + if err == nil { + if info.IsDir() { + return false, nil + } + return true, nil + } + + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + + return false, err +} + +func getFileContentsAsLines(filePath string) []string { + bytes, err := os.ReadFile(filePath) + if err != nil { + exitWithError(fmt.Errorf("unable to dex_test.go: %v", err)) + return nil + } + + return strings.Split(string(bytes), "\n") + +}