From f225f44de85de11e51d0b9a7525839dc0e8ec0af Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Fri, 14 Aug 2026 11:20:20 +0000 Subject: [PATCH 1/4] OCPBUGS-109670: Derive documentationBaseURL dynamically from OPERATOR_IMAGE_VERSION Replace the hardcoded DEFAULT_DOC_URL constant in brand_ocp.go with a dynamic function that reads OPERATOR_IMAGE_VERSION at sync time and extracts the major.minor version (e.g., "5.0.3" -> "5.0"). Falls back to "latest" when the env var is empty or unparseable. This eliminates the recurring need for manual version-bump PRs on every OCP release. The OKD build is unchanged (already uses "latest"). User-configured DocumentationBaseURL still takes precedence via the existing merge order. Co-Authored-By: Claude Opus 4.6 --- .../subresource/configmap/brand_ocp.go | 14 ++- .../subresource/configmap/brand_okd.go | 6 + .../subresource/configmap/configmap.go | 2 +- pkg/console/subresource/configmap/doc_url.go | 28 +++++ .../subresource/configmap/doc_url_test.go | 107 ++++++++++++++++++ 5 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 pkg/console/subresource/configmap/doc_url.go create mode 100644 pkg/console/subresource/configmap/doc_url_test.go diff --git a/pkg/console/subresource/configmap/brand_ocp.go b/pkg/console/subresource/configmap/brand_ocp.go index ec62a6a7f4..d9e6b97b1c 100644 --- a/pkg/console/subresource/configmap/brand_ocp.go +++ b/pkg/console/subresource/configmap/brand_ocp.go @@ -3,7 +3,13 @@ package configmap -const ( - DEFAULT_BRAND = "ocp" - DEFAULT_DOC_URL = "https://access.redhat.com/documentation/en-us/openshift_container_platform/5.0/" -) +import "os" + +const DEFAULT_BRAND = "ocp" + +// DefaultDocURL returns the documentation base URL for OCP, dynamically +// deriving the version from the OPERATOR_IMAGE_VERSION environment variable. +// Falls back to "latest" if the version is unavailable or cannot be parsed. +func DefaultDocURL() string { + return formatOCPDocURL(os.Getenv("OPERATOR_IMAGE_VERSION")) +} diff --git a/pkg/console/subresource/configmap/brand_okd.go b/pkg/console/subresource/configmap/brand_okd.go index 24be5c9f73..ee7d65fb66 100644 --- a/pkg/console/subresource/configmap/brand_okd.go +++ b/pkg/console/subresource/configmap/brand_okd.go @@ -7,3 +7,9 @@ const ( DEFAULT_BRAND = "okd" DEFAULT_DOC_URL = "https://docs.okd.io/latest/" ) + +// DefaultDocURL returns the documentation base URL for OKD. +// OKD always uses "latest", so no version derivation is needed. +func DefaultDocURL() string { + return DEFAULT_DOC_URL +} diff --git a/pkg/console/subresource/configmap/configmap.go b/pkg/console/subresource/configmap/configmap.go index eecff0757e..b204a83e4b 100644 --- a/pkg/console/subresource/configmap/configmap.go +++ b/pkg/console/subresource/configmap/configmap.go @@ -61,7 +61,7 @@ func DefaultConfigMap( defaultConfig, err := defaultBuilder.Host(consoleHost). LogoutURL(defaultLogoutURL). Brand(DEFAULT_BRAND). - DocURL(DEFAULT_DOC_URL). + DocURL(DefaultDocURL()). APIServerURL(apiServerURL). Monitoring(monitoringSharedConfig). InactivityTimeout(inactivityTimeoutSeconds). diff --git a/pkg/console/subresource/configmap/doc_url.go b/pkg/console/subresource/configmap/doc_url.go new file mode 100644 index 0000000000..1b8fce6927 --- /dev/null +++ b/pkg/console/subresource/configmap/doc_url.go @@ -0,0 +1,28 @@ +package configmap + +import ( + "fmt" + "strings" +) + +const ocpDocURLFormat = "https://access.redhat.com/documentation/en-us/openshift_container_platform/%s/" + +// formatOCPDocURL returns the OCP documentation base URL for the given version. +// Extracts major.minor from version (e.g., "5.0.3" → "5.0"). Falls back to +// "latest" when the version is empty or cannot be parsed. +func formatOCPDocURL(version string) string { + return fmt.Sprintf(ocpDocURLFormat, extractMajorMinor(version)) +} + +// extractMajorMinor extracts the major.minor portion from a version string. +// Returns "latest" if the version is empty or does not contain a dot separator. +func extractMajorMinor(version string) string { + if version == "" { + return "latest" + } + parts := strings.SplitN(version, ".", 3) + if len(parts) < 2 { + return "latest" + } + return parts[0] + "." + parts[1] +} diff --git a/pkg/console/subresource/configmap/doc_url_test.go b/pkg/console/subresource/configmap/doc_url_test.go new file mode 100644 index 0000000000..ca90fe669a --- /dev/null +++ b/pkg/console/subresource/configmap/doc_url_test.go @@ -0,0 +1,107 @@ +package configmap + +import "testing" + +func TestFormatOCPDocURL(t *testing.T) { + tests := []struct { + name string + version string + want string + }{ + { + name: "full release version", + version: "5.0.3", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/5.0/", + }, + { + name: "two-part version", + version: "4.21", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/4.21/", + }, + { + name: "nightly pre-release version", + version: "5.1.0-0.nightly-2026-01-01-000000", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/5.1/", + }, + { + name: "rc version", + version: "4.19.0-rc.1", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/4.19/", + }, + { + name: "empty version falls back to latest", + version: "", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/latest/", + }, + { + name: "single number without dot falls back to latest", + version: "5", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/latest/", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatOCPDocURL(tt.version) + if got != tt.want { + t.Errorf("formatOCPDocURL(%q) = %q, want %q", tt.version, got, tt.want) + } + }) + } +} + +func TestExtractMajorMinor(t *testing.T) { + tests := []struct { + name string + version string + want string + }{ + { + name: "standard three-part version", + version: "5.0.3", + want: "5.0", + }, + { + name: "two-part version", + version: "4.21", + want: "4.21", + }, + { + name: "nightly build version", + version: "5.1.0-0.nightly-2026-01-01-000000", + want: "5.1", + }, + { + name: "release candidate", + version: "4.19.0-rc.1", + want: "4.19", + }, + { + name: "empty string", + version: "", + want: "latest", + }, + { + name: "single number without dot", + version: "5", + want: "latest", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractMajorMinor(tt.version) + if got != tt.want { + t.Errorf("extractMajorMinor(%q) = %q, want %q", tt.version, got, tt.want) + } + }) + } +} + +func TestDefaultDocURL(t *testing.T) { + // Under OKD build (default test build), DefaultDocURL returns the + // static DEFAULT_DOC_URL constant. Under OCP build, it reads + // OPERATOR_IMAGE_VERSION and formats dynamically. + got := DefaultDocURL() + if got == "" { + t.Error("DefaultDocURL() returned empty string") + } +} From 6c981fb25b058a13f9dd8c944374a35fcd5b58ec Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Fri, 14 Aug 2026 11:27:40 +0000 Subject: [PATCH 2/4] fix(configmap): validate major.minor are decimal in version parser OCPBUGS-109670 Reject malformed dotted versions (e.g. "invalid.version", "5.", "5.x") by requiring both components pass strconv.Atoi before returning major.minor; otherwise fall back to "latest". Adds regression tests. Co-Authored-By: Claude Opus 4.6 --- pkg/console/subresource/configmap/doc_url.go | 14 ++++++- .../subresource/configmap/doc_url_test.go | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/pkg/console/subresource/configmap/doc_url.go b/pkg/console/subresource/configmap/doc_url.go index 1b8fce6927..8623befac8 100644 --- a/pkg/console/subresource/configmap/doc_url.go +++ b/pkg/console/subresource/configmap/doc_url.go @@ -2,6 +2,7 @@ package configmap import ( "fmt" + "strconv" "strings" ) @@ -15,7 +16,8 @@ func formatOCPDocURL(version string) string { } // extractMajorMinor extracts the major.minor portion from a version string. -// Returns "latest" if the version is empty or does not contain a dot separator. +// Returns "latest" if the version is empty, does not contain a dot separator, +// or if the major/minor components are not valid decimal numbers. func extractMajorMinor(version string) string { if version == "" { return "latest" @@ -24,5 +26,13 @@ func extractMajorMinor(version string) string { if len(parts) < 2 { return "latest" } - return parts[0] + "." + parts[1] + major := parts[0] + minor := parts[1] + if _, err := strconv.Atoi(major); err != nil { + return "latest" + } + if _, err := strconv.Atoi(minor); err != nil { + return "latest" + } + return major + "." + minor } diff --git a/pkg/console/subresource/configmap/doc_url_test.go b/pkg/console/subresource/configmap/doc_url_test.go index ca90fe669a..1f9cd95675 100644 --- a/pkg/console/subresource/configmap/doc_url_test.go +++ b/pkg/console/subresource/configmap/doc_url_test.go @@ -38,6 +38,26 @@ func TestFormatOCPDocURL(t *testing.T) { version: "5", want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/latest/", }, + { + name: "non-numeric dotted version falls back to latest", + version: "invalid.version", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/latest/", + }, + { + name: "trailing dot falls back to latest", + version: "5.", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/latest/", + }, + { + name: "non-numeric minor falls back to latest", + version: "5.x", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/latest/", + }, + { + name: "leading dot falls back to latest", + version: ".5", + want: "https://access.redhat.com/documentation/en-us/openshift_container_platform/latest/", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -85,6 +105,26 @@ func TestExtractMajorMinor(t *testing.T) { version: "5", want: "latest", }, + { + name: "non-numeric dotted version", + version: "invalid.version", + want: "latest", + }, + { + name: "trailing dot with empty minor", + version: "5.", + want: "latest", + }, + { + name: "non-numeric minor component", + version: "5.x", + want: "latest", + }, + { + name: "leading dot with empty major", + version: ".5", + want: "latest", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 5d79d12abc29fc6e2706df08dcbe9b452257f1b4 Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Fri, 14 Aug 2026 15:41:32 +0000 Subject: [PATCH 3/4] test(configmap): assert exact URL in TestDefaultDocURL OCPBUGS-109670 Assert the expected DEFAULT_DOC_URL value instead of only checking non-empty. Add subtest verifying OPERATOR_IMAGE_VERSION env var does not leak into OKD build path. Co-Authored-By: Claude Opus 4.6 --- .../subresource/configmap/doc_url_test.go | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/console/subresource/configmap/doc_url_test.go b/pkg/console/subresource/configmap/doc_url_test.go index 1f9cd95675..07f5fa9c07 100644 --- a/pkg/console/subresource/configmap/doc_url_test.go +++ b/pkg/console/subresource/configmap/doc_url_test.go @@ -137,11 +137,21 @@ func TestExtractMajorMinor(t *testing.T) { } func TestDefaultDocURL(t *testing.T) { - // Under OKD build (default test build), DefaultDocURL returns the - // static DEFAULT_DOC_URL constant. Under OCP build, it reads - // OPERATOR_IMAGE_VERSION and formats dynamically. - got := DefaultDocURL() - if got == "" { - t.Error("DefaultDocURL() returned empty string") - } + // Tests run without -tags ocp, so DefaultDocURL uses the OKD + // implementation which returns the static DEFAULT_DOC_URL constant. + // The OCP code path (OPERATOR_IMAGE_VERSION → formatOCPDocURL) is + // covered by TestFormatOCPDocURL and TestExtractMajorMinor above. + t.Run("returns expected OKD documentation URL", func(t *testing.T) { + got := DefaultDocURL() + if got != DEFAULT_DOC_URL { + t.Errorf("DefaultDocURL() = %q, want %q", got, DEFAULT_DOC_URL) + } + }) + t.Run("OPERATOR_IMAGE_VERSION does not affect OKD build", func(t *testing.T) { + t.Setenv("OPERATOR_IMAGE_VERSION", "5.0.3") + got := DefaultDocURL() + if got != DEFAULT_DOC_URL { + t.Errorf("DefaultDocURL() = %q, want %q (env should not affect OKD)", got, DEFAULT_DOC_URL) + } + }) } From cc9568dba8c9665e8dfa1427af2a69999cbdf881 Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Fri, 14 Aug 2026 15:49:32 +0000 Subject: [PATCH 4/4] test(configmap): guard TestDefaultDocURL with !ocp build tag Move TestDefaultDocURL into doc_url_okd_test.go with //go:build !ocp so the test compiles under -tags ocp. Assert against the concrete URL literal instead of the DEFAULT_DOC_URL constant. Refactor to table-driven structure per repo conventions. OCPBUGS-109670 Co-Authored-By: Claude Opus 4.6 --- .../subresource/configmap/doc_url_okd_test.go | 36 +++++++++++++++++++ .../subresource/configmap/doc_url_test.go | 20 ----------- 2 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 pkg/console/subresource/configmap/doc_url_okd_test.go diff --git a/pkg/console/subresource/configmap/doc_url_okd_test.go b/pkg/console/subresource/configmap/doc_url_okd_test.go new file mode 100644 index 0000000000..3757e25490 --- /dev/null +++ b/pkg/console/subresource/configmap/doc_url_okd_test.go @@ -0,0 +1,36 @@ +//go:build !ocp +// +build !ocp + +package configmap + +import "testing" + +func TestDefaultDocURL(t *testing.T) { + // OKD build: DefaultDocURL always returns the static OKD docs URL, + // regardless of OPERATOR_IMAGE_VERSION. + // The OCP code path is covered by TestFormatOCPDocURL and + // TestExtractMajorMinor in doc_url_test.go. + const expectedOKDDocURL = "https://docs.okd.io/latest/" + + tests := []struct { + name string + operatorImageVersion string + }{ + {name: "returns expected OKD documentation URL"}, + { + name: "OPERATOR_IMAGE_VERSION does not affect OKD build", + operatorImageVersion: "5.0.3", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.operatorImageVersion != "" { + t.Setenv("OPERATOR_IMAGE_VERSION", tt.operatorImageVersion) + } + got := DefaultDocURL() + if got != expectedOKDDocURL { + t.Errorf("DefaultDocURL() = %q, want %q", got, expectedOKDDocURL) + } + }) + } +} diff --git a/pkg/console/subresource/configmap/doc_url_test.go b/pkg/console/subresource/configmap/doc_url_test.go index 07f5fa9c07..dc75adfc16 100644 --- a/pkg/console/subresource/configmap/doc_url_test.go +++ b/pkg/console/subresource/configmap/doc_url_test.go @@ -135,23 +135,3 @@ func TestExtractMajorMinor(t *testing.T) { }) } } - -func TestDefaultDocURL(t *testing.T) { - // Tests run without -tags ocp, so DefaultDocURL uses the OKD - // implementation which returns the static DEFAULT_DOC_URL constant. - // The OCP code path (OPERATOR_IMAGE_VERSION → formatOCPDocURL) is - // covered by TestFormatOCPDocURL and TestExtractMajorMinor above. - t.Run("returns expected OKD documentation URL", func(t *testing.T) { - got := DefaultDocURL() - if got != DEFAULT_DOC_URL { - t.Errorf("DefaultDocURL() = %q, want %q", got, DEFAULT_DOC_URL) - } - }) - t.Run("OPERATOR_IMAGE_VERSION does not affect OKD build", func(t *testing.T) { - t.Setenv("OPERATOR_IMAGE_VERSION", "5.0.3") - got := DefaultDocURL() - if got != DEFAULT_DOC_URL { - t.Errorf("DefaultDocURL() = %q, want %q (env should not affect OKD)", got, DEFAULT_DOC_URL) - } - }) -}