Skip to content

Support a release controller for layered operators - #804

Open
JoelSpeed wants to merge 5 commits into
openshift:mainfrom
JoelSpeed:hypershift-release-controller
Open

Support a release controller for layered operators#804
JoelSpeed wants to merge 5 commits into
openshift:mainfrom
JoelSpeed:hypershift-release-controller

Conversation

@JoelSpeed

@JoelSpeed JoelSpeed commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

This PR adds a new Layered mode for the release controller and teaches the release-payload-controller how to handle a ReleasePayload that doesn't need any build or mirror step.

Important things:

  • The release controller now does not put a release creation or release mirroring config in place
    • This is the signal to the release payload controller that no build needs to happen
    • This seemed like a clean way to handle the hand-off between the two components, but open to something explicit
  • In the release payload controller, the payload creation controller adds a status for the job creation result
    • This bit is slightly hacky? It causes the job creation result controller to short-circuit, but perhaps we want to add logic directly to that controller instead to handle this case
    • We could avoid having to set this status at all if we taught the payload accepted controller how to understand that no payload creation job exists, not sure which is a better architectural decision here
  • Adds a new publishing strategy to allow pushes to external registries
    • Currently uses the existing mirror jobs flow to create a mirror job to publish to the external registry
    • There is currently no reporting or recording of the status of the mirror job, this seems bad?

Summary by CodeRabbit

  • New Features

    • Added support for layered releases throughout release discovery, synchronization, publishing, dashboards, and stream views.
    • Layered releases can be published to external registries with configurable credentials, tags, exclusions, and optional CLI image overrides.
    • Added the --release-namespace option to limit controller monitoring to a specific namespace.
  • Bug Fixes

    • Layered releases now use stable-style navigation and are excluded from delay and upgrade calculations.
    • Pre-created release payloads and images are recognized as immediately successful when no creation or mirroring configuration is required.
    • Improved validation for layered release and external registry settings.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Layered releases now use stable-style discovery, synchronization, readiness, publishing, payload handling, and dashboard rendering. External registry publishing supports configured tag filters and per-tag mirror jobs. Payload controllers recognize pre-created images, and informer factories support namespace-scoped watches.

Changes

Layered release lifecycle

Layer / File(s) Summary
Release modes and publishing contracts
pkg/release-controller/types.go, pkg/release-controller/release.go, cmd/release-controller/layered_mode_test.go
Adds ReleaseConfigModeLayered and external registry publishing fields. Layered configurations can omit to. Validation requires external registry and secret names.
Layered synchronization and external mirroring
cmd/release-controller/sync.go, cmd/release-controller/sync_publish.go, cmd/release-controller/sync_release_payload.go
Layered releases use stable-style readiness and payload tracking. External registry publishing filters tags, creates mirror jobs, selects images, and applies release metadata.
Pre-created payload completion and controller scope
pkg/cmd/release-payload-controller/*
Payload creation and mirror controllers mark pre-created images successful without running jobs. The controller adds optional namespace-scoped informer watches.
Layered release discovery and presentation
cmd/release-controller-api/http.go, cmd/release-controller-api/http_candidate.go, cmd/release-controller-api/http_helper.go
API discovery, links, descriptions, sorting, tag pruning, and delay calculations treat layered releases like stable releases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseController
  participant ReleasePayloadController
  participant KubernetesJobs
  participant ExternalRegistry
  ReleaseController->>ReleasePayloadController: create layered ReleasePayload
  ReleasePayloadController->>ReleasePayloadController: mark pre-created payload successful
  ReleaseController->>KubernetesJobs: create filtered mirror jobs
  KubernetesJobs->>ExternalRegistry: mirror release tags
Loading

Suggested reviewers: bradmwilliams, alexnpavel

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding release controller support for layered operators.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 24, 2026
@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: JoelSpeed
Once this PR has been reviewed and has the lgtm label, please assign hoxhaeris for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@JoelSpeed
JoelSpeed force-pushed the hypershift-release-controller branch from 7496ef7 to c419063 Compare August 6, 2026 16:17
@JoelSpeed
JoelSpeed marked this pull request as ready for review August 7, 2026 13:13
@JoelSpeed JoelSpeed changed the title [WIP] Support a release controller for layered operators Support a release controller for layered operators Aug 7, 2026
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/release-controller/sync_publish.go`:
- Around line 262-265: The release publishing flow must not call GetMirror for
Layered releases when OverrideCLIImage is empty. Validate this configuration
during parsing and reject it, or resolve the CLI image through a
mirror-independent path; preserve existing behavior for non-Layered releases and
add a regression test covering Layered external registry publishing without
overrideCLIImage.
- Around line 239-244: Update Job-name construction around jobName to normalize
tagName and config.Registry into DNS-safe components, then append a stable hash
derived from both original full values before applying the 63-character limit.
Ensure the final name contains only valid DNS characters and truncate in a way
that preserves the hash suffix, preventing distinct mirrors from colliding.

In `@pkg/cmd/release-payload-controller/cmd.go`:
- Line 79: Update the namespace filtering around the release payload
controller’s Job watch so it also includes the namespaces configured by
ReleaseCreationCoordinates.Namespace and ReleaseMirrorCoordinates.Namespace when
they differ from ReleaseNamespace. Alternatively, validate the configuration and
reject mismatched namespaces before starting the controllers; ensure Jobs are
never excluded and subsequently treated as missing.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d224e77f-1093-4918-b93e-d6f9843571d8

📥 Commits

Reviewing files that changed from the base of the PR and between 65da42a and c419063.

📒 Files selected for processing (15)
  • cmd/release-controller-api/http.go
  • cmd/release-controller-api/http_candidate.go
  • cmd/release-controller-api/http_helper.go
  • cmd/release-controller/layered_mode_test.go
  • cmd/release-controller/sync.go
  • cmd/release-controller/sync_publish.go
  • cmd/release-controller/sync_release_payload.go
  • pkg/cmd/release-payload-controller/cmd.go
  • pkg/cmd/release-payload-controller/layered_reference_test.go
  • pkg/cmd/release-payload-controller/payload_creation_controller.go
  • pkg/cmd/release-payload-controller/payload_mirror_controller.go
  • pkg/cmd/release-payload-controller/payload_mirror_controller_test.go
  • pkg/cmd/release-payload-controller/release_creation_job_controller.go
  • pkg/release-controller/release.go
  • pkg/release-controller/types.go

Comment on lines +239 to +244
jobName := fmt.Sprintf("%s-external-mirror-%s", tagName, sanitizeRegistryForJobName(config.Registry))

// Kubernetes limits job names to 63 characters
if len(jobName) > 63 {
jobName = jobName[:63]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'ensureExternalRegistryMirrorJob|sanitizeRegistryForJobName|newReleaseJobBase|ensureJob' \
  cmd/release-controller pkg

rg -n -C 3 'externalRegistry|registry:|tags:|excludeTags:' \
  --glob '*.yaml' --glob '*.yml' --glob '*.json' .

Repository: openshift/release-controller

Length of output: 16607


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- sync_publish.go ---'
sed -n '230,325p' cmd/release-controller/sync_publish.go

printf '%s\n' '--- related naming and configuration symbols ---'
rg -n -C 4 'sanitizeRegistryForJobName|PublishExternalRegistry|ExternalRegistry|finalTags|FindTagReference' \
  --glob '*.go' --glob '*.yaml' --glob '*.yml' --glob '*.json' .

printf '%s\n' '--- tests mentioning external mirror jobs or naming ---'
rg -n -C 5 'external.*mirror|mirror.*job|sanitizeRegistry|PublishExternalRegistry' \
  --glob '*_test.go' .

Repository: openshift/release-controller

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '236,320p' cmd/release-controller/sync_publish.go
printf '%s\n' '--- helper usages and definitions ---'
rg -n -C 3 'sanitizeRegistryForJobName|PublishExternalRegistry|ExternalRegistry' --glob '*.go' .
printf '%s\n' '--- relevant tests ---'
rg -n -C 4 'external.*registry|mirror.*job|sanitizeRegistry|PublishExternalRegistry' --glob '*_test.go' .

Repository: openshift/release-controller

Length of output: 29913


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re

def sanitize(registry):
    result = registry.replace(".", "-").replace("/", "-").replace(":", "-")
    if len(result) > 40:
        result = result[:40]
    return result

def job(tag, registry):
    name = f"{tag}-external-mirror-{sanitize(registry)}"
    return name[:63]

cases = [
    ("release_tag", "quay.io/example/image"),
    ("release_tag", "quay.io/example/image_with_underscore"),
    ("a"*50, "registry.example.com/team/one"),
    ("a"*50, "registry.example.com/team/two"),
]
for tag, registry in cases:
    name = job(tag, registry)
    print({"tag": tag, "registry": registry, "job": name,
           "valid_dns_label": bool(re.fullmatch(r"[a-z0-9]([-a-z0-9]*[a-z0-9])?", name)),
           "length": len(name)})
PY

Repository: openshift/release-controller

Length of output: 956


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n cmd/release-controller/sync_publish.go | sed -n '236,320p'

Repository: openshift/release-controller

Length of output: 3849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '268,380p' cmd/release-controller/sync_release.go

Repository: openshift/release-controller

Length of output: 4201


Use a DNS-safe, collision-resistant Job name.

Normalize tagName and config.Registry to DNS-safe components. Append a stable hash of both full values before enforcing the 63-character limit. The current code preserves _, which Kubernetes rejects. Truncation can also make different destinations share a Job name; ensureJob then returns the existing Job and skips the second mirror.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/release-controller/sync_publish.go` around lines 239 - 244, Update
Job-name construction around jobName to normalize tagName and config.Registry
into DNS-safe components, then append a stable hash derived from both original
full values before applying the 63-character limit. Ensure the final name
contains only valid DNS characters and truncate in a way that preserves the hash
suffix, preventing distinct mirrors from colliding.

Comment on lines +262 to +265
mirror, err := releasecontroller.GetMirror(release, tagName, c.releaseLister)
if err != nil {
return nil, fmt.Errorf("failed to get mirror for %s: %v", tagName, err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require a CLI image for Layered external publishing.

When config.OverrideCLIImage is empty, Line 262 calls GetMirror. Layered releases do not create a release mirror. The external publish step then fails on every reconciliation.

Reject this Layered configuration during parsing, or resolve the CLI image without a mirror. Add a regression test for a Layered external registry publish without overrideCLIImage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/release-controller/sync_publish.go` around lines 262 - 265, The release
publishing flow must not call GetMirror for Layered releases when
OverrideCLIImage is empty. Validate this configuration during parsing and reject
it, or resolve the CLI image through a mirror-independent path; preserve
existing behavior for non-Layered releases and add a regression test covering
Layered external registry publishing without overrideCLIImage.

fs.StringVar(&o.GoogleProjectID, "google-project-id", os.Getenv("GOOGLE_PROJECT_ID"), "Google project name.")
fs.StringVar(&o.GoogleServiceAccountCredentialFile, "google-service-account-credential-file", os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"), "location of a credential file described by https://cloud.google.com/docs/authentication/production")
fs.DurationVar(&o.BigQueryCacheTTL, "bigquery-cache-ttl", o.BigQueryCacheTTL, "TTL for cached BigQuery query results (0 to disable caching)")
fs.StringVar(&o.ReleaseNamespace, "release-namespace", "", "Namespace to watch for releasepayloads. When unset, all namespaces will be watched. Useful for testing locally with a single namespace.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'ReleaseNamespace|WithNamespace|jobNamespace|ReleaseCreationCoordinates|ReleaseMirrorCoordinates' \
  cmd pkg

rg -n -C 3 'release-namespace|job-namespace|jobNamespace' \
  --glob '*.yaml' --glob '*.yml' --glob '*.json' --glob '*.go' .

Repository: openshift/release-controller

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- release-payload-controller files ---'
fd -t f . | rg 'release-payload-controller|release-payload'

printf '%s\n' '--- command implementation ---'
cat -n pkg/cmd/release-payload-controller/cmd.go | sed -n '1,180p'

printf '%s\n' '--- Job informer and status handlers ---'
rg -n -C 5 'JobInformer|Batch\(\)\.V1\(\)\.Jobs|jobLister|ReleaseCreationCoordinates|ReleaseMirrorCoordinates|ReleaseNamespace' \
  pkg/cmd/release-payload-controller pkg --glob '*.go' \
  | head -n 500

Repository: openshift/release-controller

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- status lookup implementations ---'
for f in \
  pkg/cmd/release-payload-controller/release_creation_status_controller.go \
  pkg/cmd/release-payload-controller/release_mirror_job_status_controller.go \
  pkg/cmd/release-payload-controller/release_creation_job_controller.go \
  pkg/cmd/release-payload-controller/release_mirror_job_controller.go; do
  echo "### $f"
  cat -n "$f" | sed -n '1,240p'
done

printf '%s\n' '--- controller deployment arguments ---'
rg -n -C 5 'release-payload-controller|release-namespace|job-namespace' \
  . --glob '*.yaml' --glob '*.yml' --glob '*.json' --glob '*.go' \
  | head -n 400

Repository: openshift/release-controller

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

cmd = Path("pkg/cmd/release-payload-controller/cmd.go").read_text()
creation_status = Path(
    "pkg/cmd/release-payload-controller/release_creation_status_controller.go"
).read_text()
mirror_status = Path(
    "pkg/cmd/release-payload-controller/release_mirror_job_status_controller.go"
).read_text()
payload_builder = Path("cmd/release-controller/sync_release_payload.go").read_text()
release_main = Path("cmd/release-controller/main.go").read_text()

assert 'informers.WithNamespace(o.ReleaseNamespace)' in cmd
assert 'batchJobLister.Jobs(originalReleasePayload.Status.ReleaseCreationJobResult.Coordinates.Namespace)' in creation_status
assert 'batchJobLister.Jobs(originalReleasePayload.Status.ReleaseMirrorJobResult.Coordinates.Namespace)' in mirror_status
assert 'Namespace:              jobNamespace' in payload_builder or 'Namespace:            jobNamespace' in payload_builder
assert 'flagset.StringVar(&opt.JobNamespace, "job-namespace"' in release_main
assert 'flagset.StringSliceVar(&opt.ReleaseNamespaces, "release-namespace"' in release_main

release_namespace = "ocp"
job_namespace = "ci-release"
watched_jobs = {release_namespace}
creation_coordinate_namespace = job_namespace
mirror_coordinate_namespace = job_namespace

assert creation_coordinate_namespace not in watched_jobs
assert mirror_coordinate_namespace not in watched_jobs

print("PASS: a scoped release-payload controller does not cache Jobs in an independent job namespace.")
print("PASS: both status controllers look up Jobs by the payload coordinate namespace.")
print("PASS: release-controller exposes independent --release-namespace and --job-namespace settings.")
PY

Repository: openshift/release-controller

Length of output: 436


Watch Jobs in their coordinate namespaces.

When --release-namespace differs from ReleaseCreationCoordinates.Namespace or ReleaseMirrorCoordinates.Namespace, line 102 excludes the Jobs. The status controllers then treat them as missing. Add a Job namespace scope that covers these namespaces, or reject mismatched configurations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/release-payload-controller/cmd.go` at line 79, Update the namespace
filtering around the release payload controller’s Job watch so it also includes
the namespaces configured by ReleaseCreationCoordinates.Namespace and
ReleaseMirrorCoordinates.Namespace when they differ from ReleaseNamespace.
Alternatively, validate the configuration and reject mismatched namespaces
before starting the controllers; ensure Jobs are never excluded and subsequently
treated as missing.

@openshift-ci

openshift-ci Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@JoelSpeed: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant