Skip to content

WIP: etcd backups#1657

Draft
bhperry wants to merge 2 commits into
openshift:mainfrom
bhperry:etcd-backups
Draft

WIP: etcd backups#1657
bhperry wants to merge 2 commits into
openshift:mainfrom
bhperry:etcd-backups

Conversation

@bhperry

@bhperry bhperry commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added termination-log reporting for backup jobs, including captured completion metadata such as output size.
    • Reworked periodic/scheduled backups to be driven by backup policies (including timezone support) and to create backups per policy with retention by count or size.
    • Enhanced backup requests to require an explicit backup name and to support storage via either a PVC (optionally with sub-path) or a hostPath.
  • Bug Fixes

    • Backups are now marked failed when required PVCs are missing.
    • Improved completion status updates and pruning behavior for policy-managed backups.
  • Tests

    • Updated unit and e2e tests to follow the policy-driven scheduling and storage contract.

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@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

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Automated backups now use EtcdBackupPolicy and EtcdBackup resources instead of legacy configuration and CronJob reconciliation. The change adds PVC/local storage handling, policy scheduling and pruning, termination-log status reporting, updated controller wiring, tests, and dependency pins.

Changes

Automated backup migration

Layer / File(s) Summary
Backup contracts and execution flow
pkg/backuphelpers/*, pkg/cmd/backuprestore/*, pkg/cmd/request-backup/requestbackup.go, bindata/etcd/cluster-backup-job.yaml
Backup policy types, storage flags, termination metadata, snapshot sizing, and generated backup command arguments are updated.
Periodic policy scheduling and retention
pkg/operator/periodicbackupcontroller/*
Policies are queued and scheduled into node-specific EtcdBackup resources, with active-backup checks, retention pruning, schedule status updates, and schedule parsing.
Backup validation and job lifecycle
pkg/operator/backupcontroller/*
Backup validation and job creation support PVC and hostPath storage, node placement, finalizers, termination metadata, and explicit job status references.
Controller startup and lifecycle validation
pkg/operator/starter.go, test/e2e/backup.go, pkg/tnf/internal/testutil/fakeinformer.go
Startup uses policy, backup, node, and pod informers, while tests use operator backup resources and updated lifecycle fields.
Toolchain and dependency pins
go.mod
The Go toolchain and pinned direct and indirect dependencies are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PeriodicBackupController
  participant EtcdBackupPolicy
  participant EtcdBackup
  participant BackupController
  participant BackupJob
  participant BackupPod
  PeriodicBackupController->>EtcdBackupPolicy: read scheduled policy
  PeriodicBackupController->>EtcdBackup: create EtcdBackup per selected node
  BackupController->>EtcdBackup: validate and update backup
  BackupController->>BackupJob: create backup Job
  BackupJob->>BackupPod: run snapshot and write termination log
  BackupPod-->>BackupController: return path and size metadata
  BackupController->>EtcdBackup: record job and file status
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 4 warnings, 1 inconclusive)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error FAIL: getEtcdClient includes os.Environ() in errors that backup() logs, and controller/e2e logs print node names/files, exposing env data and internal hostnames. Remove env dumps from returned errors, and redact/limit node names and file paths in klog lines (backuprestore, backupcontroller, e2e tests).
Docstring Coverage ⚠️ Warning Docstring coverage is 2.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Structure And Quality ⚠️ Warning The e2e Ginkgo tests create PV/PVCs and backups without cleanup in multiple helpers/tests, and several timed polls still call the API with context.Background(), so the test-quality bar isn’t met. Add BeforeEach/AfterEach or t.Cleanup for all created cluster resources (including PV/PVCs and backup CRs), thread the poll ctx into every cluster API call, and add assertion messages to key requires.
Microshift Test Compatibility ⚠️ Warning Six [sig-etcd] Ginkgo e2e tests use operatorv1alpha1, openshift-etcd, and etcd/operator resources, with no MicroShift skip/tag/guard. Add [Skipped:MicroShift] or an appropriate [apigroup:...] tag, or skip at runtime on MicroShift, or restrict the tests to MicroShift-supported APIs.
Topology-Aware Scheduling Compatibility ⚠️ Warning FAIL: executeBackup falls back to node-role.kubernetes.io/master, and cluster-backup-job.yaml still hardcodes the same selector, with no ControlPlaneTopology check; HyperShift has no in-clu... Gate backup node selection on infrastructure.Status.ControlPlaneTopology; avoid master selectors on External, and add topology-aware fallback/lister logic for SNO/TNF/TNA/arbiter clusters.
Title check ❓ Inconclusive The title is too vague and generic; it does not describe the main change beyond a work-in-progress note. Replace it with a concise, specific title that names the primary change, such as adding or refactoring etcd backup support.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.
Stable And Deterministic Test Names ✅ Passed PASS: Modified Ginkgo/t.Run titles are static literals; no pod/node/namespace/timestamp/UUID-derived names were found in the touched tests.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The new e2e tests don’t require >1 node; they only iterate over master nodes and have no SNO skip guard because a single master still works.
Ote Binary Stdout Contract ✅ Passed No changed main/init/suite setup code writes to stdout; new logging uses klog or errOut, and backupserver sets logtostderr=true.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The new Ginkgo e2e tests in test/e2e/backup.go use only cluster-internal services and PVC/HostPath flows; no IPv4 literals, ParseIP/CIDR, or public-host access found.
No-Weak-Crypto ✅ Passed No MD5/SHA1/DES/RC4/3DES/Blowfish/ECB, custom crypto, or secret/token comparisons were introduced in the changed files.
Container-Privileges ✅ Passed No privileged/host* or allowPrivilegeEscalation fields were introduced; the manifest diff only adds --termination-log.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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:
Once this PR has been reviewed and has the lgtm label, please assign benluddy 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

@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: 9

🤖 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 `@go.mod`:
- Line 37: Update the google.golang.org/grpc dependency in go.mod from v1.79.3
to a patched version such as v1.82.1, or remove it if no longer required
transitively; regenerate the corresponding go.sum entries and rerun
vulnerability scanning to confirm the vulnerable version is absent.
- Line 138: Upgrade the go.opentelemetry.io/otel/sdk dependency from v1.40.0 to
v1.43.0 or later, then refresh go.mod, go.sum, and vendor/modules.txt so all
OpenTelemetry module versions remain consistent.

In `@pkg/backuphelpers/backupvars_test.go`:
- Around line 38-46: The retention expectations in TestBackupConfig_ToArgs and
the matching TestBackupConfig_ToArgList cases must be removed because ArgList()
and ArgString() no longer emit retention arguments. In
pkg/backuphelpers/backupvars_test.go lines 38-46, remove the RetentionNumber and
RetentionSize cases; remove the corresponding cases at lines 99-119. At lines
154-159, remove withRetentionSizeOneGB since it is no longer used and constructs
an invalid quantity.

In `@pkg/operator/backupcontroller/backupcontroller.go`:
- Around line 383-403: In the successful-job handling around the pod loop,
update the termination message extraction to use
pod.Status.ContainerStatuses[0].State.Terminated rather than
LastTerminationState.Terminated. Add nil checks for the container status, State,
and Terminated before reading Message, while preserving the existing JSON
parsing and FilePath/FileSize updates.

In `@pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go`:
- Around line 128-130: In the re-fetch flow for EtcdBackupPolicies, add a
require.NoError assertion immediately after the Get call and before accessing
backupPolicy.Status. Keep the existing LastScheduleTime and LastScheduleNodes
assertions unchanged.

In `@pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go`:
- Line 65: Update NewPeriodicBackupController so the Node informer is obtained
from the cluster-scoped informer factory, such as the existing clusterInformers,
rather than calling InformersFor("") on kubeInformersForNamespaces. Ensure the
factory used for Nodes is initialized before the .Core().V1().Nodes() chain
executes.
- Around line 237-239: Update the masterNodes truncation logic in the periodic
backup controller so the slice upper bound never exceeds len(masterNodes). When
backupPolicy.Spec.NodeCount is positive, cap it at the available master-node
count before slicing, while preserving the existing behavior when the requested
count is within bounds.
- Around line 216-222: Update the default selector in the periodic backup
controller’s fallback branch to parse the canonical OpenShift master-node label
key, node-role.kubernetes.io/master, instead of node-role.kubernetes.master.io.
Preserve the existing custom NodeSelector handling and error propagation.

In `@test/e2e/backup.go`:
- Around line 186-190: Update the MaxSize value in the
EtcdBackupPolicyRetentionRule to represent approximately 5GB in bytes, using
resource.NewScaledQuantity with the appropriate giga suffix or an explicitly
scaled byte quantity; do not use resource.NewQuantity(5, "GB"), which stores
only 5 bytes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

Comment thread go.mod
go.uber.org/zap v1.27.1
golang.org/x/sys v0.45.0
google.golang.org/grpc v1.72.2
google.golang.org/grpc v1.79.3

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

curl -sS https://api.osv.dev/v1/query \
  -H 'Content-Type: application/json' \
  --data '{"package":{"name":"google.golang.org/grpc","ecosystem":"Go"},"version":"v1.79.3"}' |
  jq .

Repository: openshift/cluster-etcd-operator

Length of output: 6584


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== go.mod grpc line =="
grep -n 'google.golang.org/grpc' go.mod || true

echo
echo "== go.sum grpc entries =="
grep -n 'google.golang.org/grpc' go.sum || true

echo
echo "== gRPC usage / RBAC/xDS import context =="
rg -n 'google\.golang\.org/grpc|grpc/xds|xds|rbac|Rbac|RequestedServerName|Metadata|SourcedMetadata' -S . --glob '!vendor/**' --glob '!_out/**' | head -200

Repository: openshift/cluster-etcd-operator

Length of output: 4622


Remove or update the vulnerable gRPC dependency.

google.golang.org/grpc v1.79.3 is affected by GHSA-hrxh-6v49-42gf. This module is only referenced as an indirect/transitive dependency now; remove the stale go.mod/go.sum entry or update it to a patched version such as v1.82.1, then regenerate go.sum and rerun vulnerability scanning.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[HIGH] 37-37: google.golang.org/grpc 1.79.3: gRPC-Go: xDS RBAC and HTTP/2 Vulnerabilities

(GHSA-hrxh-6v49-42gf)

🤖 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 `@go.mod` at line 37, Update the google.golang.org/grpc dependency in go.mod
from v1.79.3 to a patched version such as v1.82.1, or remove it if no longer
required transitively; regenerate the corresponding go.sum entries and rerun
vulnerability scanning to confirm the vulnerable version is absent.

Source: Linters/SAST tools

Comment thread go.mod
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

curl -sS https://api.osv.dev/v1/query \
  -H 'Content-Type: application/json' \
  --data '{"package":{"name":"go.opentelemetry.io/otel/sdk","ecosystem":"Go"},"version":"v1.40.0"}' |
  jq .

Repository: openshift/cluster-etcd-operator

Length of output: 3970


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "go.mod entries for OpenTelemetry packages:"
grep -n 'go.opentelemetry.io/otel' go.mod || true

echo
echo "Dependency metadata for go.opentelemetry.io/otel/sdk@v1.40.0:"
go list -m -json go.opentelemetry.io/otel/sdk@v1.40.0 2>&1 || true

echo
echo "Dependency metadata for patched/compatible versions:"
for v in v1.43.0 v1.43.1 v1.44.0; do
  echo "--- go.opentelemetry.io/otel/sdk@${v}"
  go list -m -json "go.opentelemetry.io/otel/sdk@${v}" 2>&1 || true
done

Repository: openshift/cluster-etcd-operator

Length of output: 1437


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Module metadata from go.sum:"
rg 'go.opentelemetry.io/otel/sdk v1\.4[0-9]\.[0-9]+|go.opentelemetry.io/otel/sdk v1\.43\.0|go.opentelemetry.io/otel/sdk v1\.43\.1|go.opentelemetry.io/otel/sdk v1\.44\.0' go.sum || true

echo
echo "Current OpenTelemetry versions in go.mod:"
python3 - <<'PY'
from pathlib import Path
for line in Path("go.mod").read_text().splitlines():
    if "opentelemetry.io/otel" in line:
        print(line.strip())
PY

echo
echo "Vendor manifest module version for SDK if present:"
python3 - <<'PY'
from pathlib import Path
import re
p = Path("vendor/modules.txt")
if p.exists():
    for m in re.findall(r'^## go\.opentelemetry\.io/otel/sdk v[0-9][^\n]*', p.read_text().splitlines()[0:100] + p.read_text().splitlines()):
        pass
    for line in p.read_text().splitlines():
        if line.startswith("## go.opentelemetry.io/otel/sdk "):
            print(line)
else:
    print("vendor/modules.txt not found")
PY

Repository: openshift/cluster-etcd-operator

Length of output: 1124


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re
p = Path("vendor/modules.txt")
if p.exists():
    for line in p.read_text().splitlines():
        if line.startswith("## go.opentelemetry.io/otel/sdk "):
            print(line)
else:
    print("vendor/modules.txt not found")
PY

Repository: openshift/cluster-etcd-operator

Length of output: 169


Upgrade go.opentelemetry.io/otel/sdk to v1.43.0 or later.

v1.40.0 is affected by GHSA-hfvc-g4fc-pqhx / CVE-2026-39883, a high-severity PATH-hijacking issue in the BSD/Solaris resource host ID path. Run go get -u go.opentelemetry.io/otel/sdk@v1.43.0 and refresh go.mod, go.sum, and vendor/modules.txt to keep the OpenTelemetry modules consistent.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[HIGH] 138-138: go.opentelemetry.io/otel/sdk 1.40.0: opentelemetry-go: BSD kenv command not using absolute path enables PATH hijacking

(GHSA-hfvc-g4fc-pqhx)

🤖 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 `@go.mod` at line 138, Upgrade the go.opentelemetry.io/otel/sdk dependency from
v1.40.0 to v1.43.0 or later, then refresh go.mod, go.sum, and vendor/modules.txt
so all OpenTelemetry module versions remain consistent.

Source: Linters/SAST tools

Comment on lines 38 to 46
{
"backup spec with timezone and schedule and retention number",
withRetentionNumberThreeBackups(createEtcdBackupSpec(timezone, schedule)),
withRetentionNumberThreeBackups(createEtcdBackupPolicySpec(timezone, schedule)),
" args:\n - --enabled=true\n - --timezone=GMT\n - --schedule=0 */2 * * *\n - --type=RetentionNumber\n - --maxNumberOfBackups=3",
},
{
"backup spec with timezone and schedule and retention size",
withRetentionSizeOneGB(createEtcdBackupSpec(timezone, schedule)),
withRetentionSizeOneGB(createEtcdBackupPolicySpec(timezone, schedule)),
" args:\n - --enabled=true\n - --timezone=GMT\n - --schedule=0 */2 * * *\n - --type=RetentionSize\n - --maxSizeOfBackupsGb=1",

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

Retention test fixtures/expectations are stale after retention args were removed from ArgList()/ArgString(). The production methods no longer emit --type/--maxNumberOfBackups/--maxSizeOfBackupsGb, so the retention cases fail; the retention helpers also build an incorrect quantity.

  • pkg/backuphelpers/backupvars_test.go#L38-L46: remove the --type=RetentionNumber/--maxNumberOfBackups=3 and RetentionSize retention assertions from TestBackupConfig_ToArgs (and the matching cases in TestBackupConfig_ToArgList, L99-L119).
  • pkg/backuphelpers/backupvars_test.go#L154-L159: drop/repair withRetentionSizeOneGBresource.NewQuantity(10, "GB") uses value 10 and a non-standard Format "GB", inconsistent with the OneGB/=1 intent; remove it if retention args are no longer asserted.
📍 Affects 1 file
  • pkg/backuphelpers/backupvars_test.go#L38-L46 (this comment)
  • pkg/backuphelpers/backupvars_test.go#L154-L159
🤖 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/backuphelpers/backupvars_test.go` around lines 38 - 46, The retention
expectations in TestBackupConfig_ToArgs and the matching
TestBackupConfig_ToArgList cases must be removed because ArgList() and
ArgString() no longer emit retention arguments. In
pkg/backuphelpers/backupvars_test.go lines 38-46, remove the RetentionNumber and
RetentionSize cases; remove the corresponding cases at lines 99-119. At lines
154-159, remove withRetentionSizeOneGB since it is no longer used and constructs
an invalid quantity.

Comment on lines +128 to +130
backupPolicy, err = operatorClient.OperatorV1alpha1().EtcdBackupPolicies().Get(context.TODO(), backupPolicy.Name, v1.GetOptions{})
require.NotNil(t, backupPolicy.Status.LastScheduleTime)
require.Equal(t, backupPolicy.Status.LastScheduleNodes, []string{"master-0"})

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing error assertion on the re-fetched policy.

backupPolicy, err = ...Get(...) is followed by assertions on backupPolicy.Status without a require.NoError(t, err); a failed Get would produce a confusing nil-deref instead of a clear failure.

🤖 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/operator/periodicbackupcontroller/periodicbackupcontroller_test.go`
around lines 128 - 130, In the re-fetch flow for EtcdBackupPolicies, add a
require.NoError assertion immediately after the Get call and before accessing
backupPolicy.Status. Keep the existing LastScheduleTime and LastScheduleNodes
assertions unchanged.

Comment thread pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go Outdated
Comment thread pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go
Comment thread pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go Outdated
Comment thread test/e2e/backup.go

@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: 4

🤖 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 `@go.mod`:
- Around line 5-7: Remove the personal-fork replace directives for
github.com/openshift/api and github.com/openshift/client-go from go.mod. Restore
dependencies to reviewed upstream module versions or an auditable
organization-owned fork, ensuring production builds no longer resolve through
github.com/bhperry.

In `@pkg/backuphelpers/backupmessage.go`:
- Around line 20-26: Update the file lifecycle in the function containing the
termination-log Encode call: defer closing f immediately after opening it so
encoding failures release the descriptor, while preserving explicit close-error
propagation when encoding succeeds. Ensure a prior Encode error remains the
returned error and is not replaced by the deferred close result.

In `@pkg/operator/backupcontroller/backupcontroller.go`:
- Line 471: Update the logging statement in the backup job assignment flow to
stop including backup.Spec.NodeName, which may expose an internal hostname.
Retain only the job identifier in the klog message.
- Around line 383-403: The job-completion handling must not finalize the backup
when no succeeded Pod with parseable termination metadata has been observed. In
the jobState == batchv1.JobComplete path, require finding and successfully
parsing a succeeded Pod’s termination message before allowing the surrounding
flow to persist processed state and finalize the Job; otherwise defer or retry
processing so later Pod cache updates can populate FilePath and FileSize.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

Comment thread go.mod
Comment on lines +5 to +7
replace github.com/openshift/api => github.com/bhperry/openshift-api v0.0.0-20260724171108-0e2863a63be5

replace github.com/openshift/client-go => github.com/bhperry/openshift-client-go v0.0.0-20260724172246-3b02de06ce22

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod relevant lines =="
sed -n '1,30p' go.mod 2>/dev/null || true

echo
echo "== repo occurrences of bhperry and openshift replacements =="
rg -n "github.com/bhperry|github.com/openshift/api|github.com/openshift/client-go" -S . || true

echo
echo "== README/dependency mentions =="
rg -n "openshift|dependency|bhperry|supply|SBOM|sigstore|cosign" -S --glob '!vendor/**' --glob '!dist/**' --glob '!build/**' . || true

Repository: openshift/cluster-etcd-operator

Length of output: 50388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate go-related files =="
git ls-files | rg '(^go\.mod$|^go\.sum$|^Dockerfile$|\.github/workflows/|helm/|Makefile$|vendor/|pkg/|cmd/|openshift|openshift-client-go|openshift-api)' | head -200

echo
echo "== module references in code =="
rg -n '"github.com/(openshift|bhperry)/(api|client-go)|bhperry|openshift-client-go|openshift-api' --glob '*.go' --glob '!vendor/**' . || true

Repository: openshift/cluster-etcd-operator

Length of output: 19739


Remove the personal-fork replace directives before merge.

These module replacements make the build depend on github.com/bhperry/openshift-api and github.com/bhperry/openshift-client-go, so production builds can include unreviewed fork content. Keep the fork only as temporary draft wiring; use reviewed upstream commits or an auditable organization-owned fork before merging.

🤖 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 `@go.mod` around lines 5 - 7, Remove the personal-fork replace directives for
github.com/openshift/api and github.com/openshift/client-go from go.mod. Restore
dependencies to reviewed upstream module versions or an auditable
organization-owned fork, ensuring production builds no longer resolve through
github.com/bhperry.

Comment thread pkg/backuphelpers/backupmessage.go Outdated
Comment on lines +20 to +26
if err := json.NewEncoder(f).Encode(BackupTerminationLog{Path: snapshotFile, Size: size}); err != nil {
return fmt.Errorf("Error writing termination log: %w", err)
}

if err := f.Close(); err != nil {
return fmt.Errorf("Error closing termination log: %w", 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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close the file on encoding failures.

If Encode returns an I/O error, this function returns without closing f, leaking the descriptor and potentially leaving a truncated termination log. Defer cleanup immediately after opening the file, while still propagating a close error when no earlier error occurred.

Proposed fix
-func WriteTerminationLog(terminationLog, snapshotFile string, size int64) error {
+func WriteTerminationLog(terminationLog, snapshotFile string, size int64) (retErr error) {
 	f, err := os.OpenFile(terminationLog, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
 	if err != nil {
 		return fmt.Errorf("Error opening termination log: %w", err)
 	}
+	defer func() {
+		if closeErr := f.Close(); closeErr != nil && retErr == nil {
+			retErr = fmt.Errorf("Error closing termination log: %w", closeErr)
+		}
+	}()

 	if err := json.NewEncoder(f).Encode(BackupTerminationLog{Path: snapshotFile, Size: size}); err != nil {
 		return fmt.Errorf("Error writing termination log: %w", err)
 	}
-
-	if err := f.Close(); err != nil {
-		return fmt.Errorf("Error closing termination log: %w", err)
-	}
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := json.NewEncoder(f).Encode(BackupTerminationLog{Path: snapshotFile, Size: size}); err != nil {
return fmt.Errorf("Error writing termination log: %w", err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("Error closing termination log: %w", err)
}
func WriteTerminationLog(terminationLog, snapshotFile string, size int64) (retErr error) {
f, err := os.OpenFile(terminationLog, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("Error opening termination log: %w", err)
}
defer func() {
if closeErr := f.Close(); closeErr != nil && retErr == nil {
retErr = fmt.Errorf("Error closing termination log: %w", closeErr)
}
}()
if err := json.NewEncoder(f).Encode(BackupTerminationLog{Path: snapshotFile, Size: size}); err != nil {
return fmt.Errorf("Error writing termination log: %w", err)
}
return nil
}
🤖 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/backuphelpers/backupmessage.go` around lines 20 - 26, Update the file
lifecycle in the function containing the termination-log Encode call: defer
closing f immediately after opening it so encoding failures release the
descriptor, while preserving explicit close-error propagation when encoding
succeeds. Ensure a prior Encode error remains the returned error and is not
replaced by the deferred close result.

Comment on lines +383 to +403
if jobState == batchv1.JobComplete {
pods, err := podLister.List(labels.SelectorFromSet(labels.Set{"batch.kubernetes.io/job-name": job.Name}))
if err != nil {
return err
}
for _, pod := range pods {
if pod.Status.Phase == corev1.PodSucceeded && len(pod.Status.ContainerStatuses) > 0 && pod.Status.ContainerStatuses[0].State.Terminated != nil {
terminationMessage := pod.Status.ContainerStatuses[0].State.Terminated.Message
if terminationMessage != "" {
data := backuphelpers.BackupTerminationLog{}
if err := json.Unmarshal([]byte(terminationMessage), &data); err != nil {
klog.V(4).Infof("error reading termination log: %s", err)
} else {
bp.Status.FilePath = data.Path
bp.Status.FileSize = *resource.NewQuantity(data.Size, resource.BinarySI)
}
}
break
}
}
}

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

Do not process the backup before its termination metadata is available.

The Job and Pod caches update independently. If the completed Job arrives first, pods can be empty; this code then persists state=processed and finalizes the Job. Later Pod updates cannot populate FilePath or FileSize because both resources are excluded by the state!=processed selectors. Delay marking the backup/job processed, or retry, until the succeeded Pod has been observed and parsed.

🤖 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/operator/backupcontroller/backupcontroller.go` around lines 383 - 403,
The job-completion handling must not finalize the backup when no succeeded Pod
with parseable termination metadata has been observed. In the jobState ==
batchv1.JobComplete path, require finding and successfully parsing a succeeded
Pod’s termination message before allowing the surrounding flow to persist
processed state and finalize the Job; otherwise defer or retry processing so
later Pod cache updates can populate FilePath and FileSize.

job.Spec.Template.Spec.NodeName = backup.Spec.NodeName
// Remove nodeSelector to avoid conflicts
delete(job.Spec.Template.Spec.NodeSelector, "node-role.kubernetes.io/master")
klog.V(4).Infof("BackupController assigned job [%s] to node [%s]", job.Name, backup.Spec.NodeName)

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid logging the target node name.

backup.Spec.NodeName may be an internal hostname. Omit it from this log entry or log only the Job identifier. As per coding guidelines, flag logging that may expose “internal hostnames.”

🤖 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/operator/backupcontroller/backupcontroller.go` at line 471, Update the
logging statement in the backup job assignment flow to stop including
backup.Spec.NodeName, which may expose an internal hostname. Retain only the job
identifier in the klog message.

Source: Coding guidelines

@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: 1

🤖 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 `@pkg/operator/backupcontroller/backupcontroller.go`:
- Line 473: Update the TTLSecondsAfterFinished assignment in the backup job
setup to use ptr.To(ttlSecondsAfterFinished) instead of
new(ttlSecondsAfterFinished), preserving the intended pointer value and the
existing ptr import.
🪄 Autofix (Beta)

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: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a83fcfe-d2dc-4904-993e-aa1ba088733a

📥 Commits

Reviewing files that changed from the base of the PR and between 4f1b86f and e8b6929.

📒 Files selected for processing (14)
  • bindata/etcd/cluster-backup-job.yaml
  • pkg/backuphelpers/backupmessage.go
  • pkg/backuphelpers/backupvars.go
  • pkg/backuphelpers/backupvars_test.go
  • pkg/cmd/backuprestore/backuprestore.go
  • pkg/cmd/backuprestore/backuputils.go
  • pkg/cmd/backuprestore/etcdclientutils.go
  • pkg/operator/backupcontroller/backupcontroller.go
  • pkg/operator/backupcontroller/backupcontroller_test.go
  • pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go
  • pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go
  • pkg/operator/starter.go
  • pkg/tnf/internal/testutil/fakeinformer.go
  • test/e2e/backup.go
💤 Files with no reviewable changes (1)
  • pkg/backuphelpers/backupvars.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • pkg/cmd/backuprestore/backuputils.go
  • pkg/tnf/internal/testutil/fakeinformer.go
  • pkg/cmd/backuprestore/backuprestore.go
  • pkg/backuphelpers/backupmessage.go
  • pkg/cmd/backuprestore/etcdclientutils.go
  • bindata/etcd/cluster-backup-job.yaml
  • pkg/backuphelpers/backupvars_test.go
  • pkg/operator/starter.go
  • pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go
  • pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go
  • pkg/operator/backupcontroller/backupcontroller_test.go
  • test/e2e/backup.go

Comment thread pkg/operator/backupcontroller/backupcontroller.go Outdated

@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: 4

🤖 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 `@pkg/cmd/backuprestore/etcdclientutils.go`:
- Around line 59-61: Update backup() and saveSnapshot() to accept and propagate
the running command context, replacing context.Background() in the cli.Snapshot
call with a derived context that has an appropriate snapshot timeout. Ensure the
timeout context is cancelled and preserve existing error wrapping while allowing
command cancellation to interrupt snapshot creation or streaming.
- Around line 50-70: Update the snapshot-writing function around cli.Snapshot
and io.Copy to always close rd, including when copying fails, while preserving
the existing copy error. Capture and return any f.Close failure as currently
done, and check the deferred os.RemoveAll cleanup result so removal failures
after successful output are propagated.

In `@pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go`:
- Around line 83-88: Update newFakeSyncContext to accept *testing.T, create the
rate-limiting queue as a local variable, and register t.Cleanup to call
queue.ShutDown before returning the context. Update all three newFakeSyncContext
callers to pass their testing handle while preserving the existing context
setup.
- Around line 40-57: Update createTestController to accept *testing.T, then
check the error returned by each nodeIndexer.Add(node) call and fail the test
via the testing handle when insertion fails. Update all callers of
createTestController to pass their test handle.
🪄 Autofix (Beta)

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: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 81b96d58-2373-4796-a102-b8ec8f5362cc

📥 Commits

Reviewing files that changed from the base of the PR and between e8b6929 and c5f4957.

📒 Files selected for processing (14)
  • bindata/etcd/cluster-backup-job.yaml
  • pkg/backuphelpers/backupmessage.go
  • pkg/backuphelpers/backupvars.go
  • pkg/backuphelpers/backupvars_test.go
  • pkg/cmd/backuprestore/backuprestore.go
  • pkg/cmd/backuprestore/backuputils.go
  • pkg/cmd/backuprestore/etcdclientutils.go
  • pkg/operator/backupcontroller/backupcontroller.go
  • pkg/operator/backupcontroller/backupcontroller_test.go
  • pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go
  • pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go
  • pkg/operator/starter.go
  • pkg/tnf/internal/testutil/fakeinformer.go
  • test/e2e/backup.go
💤 Files with no reviewable changes (1)
  • pkg/backuphelpers/backupvars.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • pkg/backuphelpers/backupmessage.go
  • pkg/tnf/internal/testutil/fakeinformer.go
  • pkg/cmd/backuprestore/backuprestore.go
  • bindata/etcd/cluster-backup-job.yaml
  • pkg/cmd/backuprestore/backuputils.go
  • pkg/backuphelpers/backupvars_test.go
  • pkg/operator/periodicbackupcontroller/periodicbackupcontroller.go
  • pkg/operator/starter.go
  • pkg/operator/backupcontroller/backupcontroller_test.go
  • test/e2e/backup.go

Comment on lines 50 to 70
defer os.RemoveAll(partpath)

f, err := os.OpenFile(partpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("could not open %s (%w)", partpath, err)
return -1, fmt.Errorf("could not open %s (%w)", partpath, err)
}

opBegin := time.Now()
var rd io.ReadCloser
rd, err = cli.Snapshot(context.Background())
if err != nil {
return fmt.Errorf("saveSnapshot failed: %w", err)
return -1, fmt.Errorf("saveSnapshot failed: %w", err)
}

if _, err := io.Copy(f, rd); err != nil {
return fmt.Errorf("saveSnapshot failed: %w", err)
var size int64
if size, err = io.Copy(f, rd); err != nil {
return -1, fmt.Errorf("saveSnapshot failed: %w", err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("saveSnapshot failed: %w", err)
return -1, fmt.Errorf("saveSnapshot failed: %w", 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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline pkg/cmd/backuprestore/etcdclientutils.go --items all
rg -n -C3 'os\.RemoveAll|cli\.Snapshot|io\.Copy|\.Close\(' pkg/cmd/backuprestore/etcdclientutils.go

Repository: openshift/cluster-etcd-operator

Length of output: 1344


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file content =="
cat -n pkg/cmd/backuprestore/etcdclientutils.go

echo
echo "== package/module refs for etcd client =="
rg -n 'go\.etcd\.io/etcd/client/v3|require|module' -g '*.go' -g 'go.mod' -g 'go.sum' .

echo
echo "== snapshot/readcloser close calls in surrounding code =="
rg -n -C2 'Snapshot\(|ReadCloser|\.Close\(' pkg/cmd/backuprestore/etcdclientutils.go pkg/cmd/backuprestore

Repository: openshift/cluster-etcd-operator

Length of output: 50388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== saveSnapshot location =="
cat -n pkg/cmd/backuprestore/etcdclientutils.go | sed -n '1,120p'

echo
echo "== saveSnapshot usages =="
rg -n -C3 'saveSnapshot\(' pkg/cmd/backuprestore

echo
echo "== etcd client/v3 dependency =="
python3 - <<'PY'
from pathlib import Path
for p in Path('.').rglob('go.mod'):
    if 'vendor' in p.parts:
        continue
    text=p.read_text()
    if 'go.etcd.io/etcd/client/v3' in text:
        print(p)
        for line in text.splitlines():
            if 'go.etcd.io/etcd/client/v3' in line or 'require' in line or 'module ' in line:
                print(line)
PY

echo
echo "== go module cached source if present =="
modpath="$(go env GOPATH 2>/dev/null)/pkg/mod"; if [ -n "$modpath" ] && [ -d "$modpath" ]; then
  find "$modpath" -path '*etcd*client*v3*client.go' -o -path '*etcd*client*v3*client*snapshot*' 2>/dev/null | head -50
  if [ -f "$modpath/go.etcd.io/etcd/client/v3@*/client.go" ]; then
    grep -n "func .*Snapshot" "$modpath/go.etcd.io/etcd/client/v3@*/client.go"
  fi
else
  echo "no GOPATH module cache"
fi

Repository: openshift/cluster-etcd-operator

Length of output: 4353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cache="$(go env GOPATH)/pkg/mod/go.etcd.io/etcd/client/v3@v3.6.8"
echo "== client.go outline around Snapshot =="
grep -n "func .*Snapshot\|Snapshot\|ReadCloser\|Close\|grpc.*snapshot" "$cache/client.go"
echo
sed -n '330,430p' "$cache/client.go"

echo
echo "== source search snapshot response handling =="
rg -n -C4 'func .*Snapshot|type .*SnapshotResponse|SnapshotResponse|Close\(\) error|snapshotpb\.Snapshot' "$cache" vendor/go.etcd.io 2>/dev/null | head -200

echo
echo "== local os.RemoveAll callers in file to show return ignored =="
sed -n '80,140p' pkg/cmd/backuprestore/etcdclientutils.go

Repository: openshift/cluster-etcd-operator

Length of output: 21520


Close the snapshot stream and return cleanup file failures.

cli.Snapshot(...) returns a ReaderCloser; io.Copy reads it but does not close it, so the snapshot stream remains open until the file is closed. Also, f.Close() errors are returned, but rd is left open on any io.Copy failure, and the immediate cleanup return from os.RemoveAll is ignored on the success path. Wrap the cleanup return so removal failures after successful output are surfaced.

🤖 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/backuprestore/etcdclientutils.go` around lines 50 - 70, Update the
snapshot-writing function around cli.Snapshot and io.Copy to always close rd,
including when copying fails, while preserving the existing copy error. Capture
and return any f.Close failure as currently done, and check the deferred
os.RemoveAll cleanup result so removal failures after successful output are
propagated.

Source: Path instructions

Comment on lines 59 to +61
rd, err = cli.Snapshot(context.Background())
if err != nil {
return fmt.Errorf("saveSnapshot failed: %w", err)
return -1, fmt.Errorf("saveSnapshot failed: %w", 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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline pkg/cmd/backuprestore --items all
rg -n -C3 '\bsaveSnapshot\s*\(|\.Snapshot\s*\(|context\.Background\(\)' pkg/cmd/backuprestore

Repository: openshift/cluster-etcd-operator

Length of output: 5068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- etcdclientutils.go ---\n'
sed -n '1,120p' pkg/cmd/backuprestore/etcdclientutils.go

printf '\n--- backuputils.go saveSnapshot call site ---\n'
sed -n '1,100p' pkg/cmd/backuprestore/backuputils.go

printf '\n--- backupOptions/flags and command context usage ---\n'
sed -n '1,220p' pkg/cmd/backuprestore/backuprestore.go

Repository: openshift/cluster-etcd-operator

Length of output: 8045


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- backupserver.go timeout policy ---'
sed -n '1,240p' pkg/cmd/backuprestore/backupserver.go

Repository: openshift/cluster-etcd-operator

Length of output: 3659


🌐 Web query:

etcd client/v3 Client Snapshot context.Context timeout cancellation default documentation

💡 Result:

In the etcd v3 Go client, snapshot operations—specifically via the Maintenance interface methods Snapshot and SnapshotWithVersion—do not have a default client-side timeout applied automatically by the etcd library [1]. The context.Context passed to these methods is used to manage the lifecycle and cancellation of the streaming RPC [1]. If the provided context is canceled or reaches its deadline, the etcd client will immediately abort the operation, typically resulting in a context.Canceled or context.DeadlineExceeded error [2][1]. Because there is no internal default timeout, it is standard practice to wrap the context using context.WithTimeout to ensure your application can recover from stalled network requests or unresponsive server nodes [3][4]. Key takeaways: 1. No Default Timeout: The etcd client library does not impose a default timeout on Snapshot calls [1]. If you pass context.Background or any context without a deadline, the operation will wait indefinitely for the server to respond [3][4]. 2. Context Responsibility: You are responsible for providing a context with an appropriate timeout or deadline to prevent resource leaks or long-hanging requests [3][4]. 3. Cancellation Behavior: If the context is canceled or times out, the snapshot stream will error out, and the client will stop processing data [2][1]. 4. Recommended Pattern: Use context.WithTimeout when initiating snapshot requests to ensure deterministic behavior [4]. Example Usage: ctx, cancel:= context.WithTimeout(context.Background, 10 * time.Minute) defer cancel resp, err:= cli.SnapshotWithVersion(ctx) if err!= nil { // Handle context.DeadlineExceeded or context.Canceled }

Citations:


Use a cancellable, bounded context for the etcd snapshot.

cli.Snapshot(context.Background()) has no command-backed cancellation or timeout here, and the etcd client does not apply its own snapshot deadline. This can leave the backup job stuck if snapshot creation/streaming hangs. Thread the running context into backup()/saveSnapshot() and apply an appropriate snapshot timeout before calling cli.Snapshot().

🤖 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/backuprestore/etcdclientutils.go` around lines 59 - 61, Update
backup() and saveSnapshot() to accept and propagate the running command context,
replacing context.Background() in the cli.Snapshot call with a derived context
that has an appropriate snapshot timeout. Ensure the timeout context is
cancelled and preserve existing error wrapping while allowing command
cancellation to interrupt snapshot creation or streaming.

Source: Path instructions

Comment on lines +40 to +57
func createTestController(kubeClient *k8sfakeclient.Clientset, operatorClient *operatorfake.Clientset, fakeOperatorClient v1helpers.StaticPodOperatorClient, nodes []*corev1.Node) *PeriodicBackupController {
fakeClock := testingclock.NewFakeClock(time.Now())
eventRecorder := events.NewInMemoryRecorder("test", fakeClock)

if nodes == nil {
nodes = []*corev1.Node{
{
ObjectMeta: v1.ObjectMeta{
Name: "master-0",
Labels: map[string]string{"node-role.kubernetes.io/master": ""},
},
},
}

}
nodeIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
for _, node := range nodes {
nodeIndexer.Add(node)

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go --items all
rg -n -C2 'createTestController\(|nodeIndexer\.Add\(' pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go

Repository: openshift/cluster-etcd-operator

Length of output: 4240


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant helper and call sites around createTestController.
sed -n '35,65p' pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go
printf '\n--- createTestController call sites ---\n'
rg -n -C2 'createTestController\(' pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go

# Check whether the test file already imports require and whether Node creation is custom or generated.
printf '\n--- imports ---\n'
sed -n '1,35p' pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go

Repository: openshift/cluster-etcd-operator

Length of output: 4213


🌐 Web query:

k8s.io/client-go tools/cache Indexer Add signature and duplicate key behavior

💡 Result:

In the k8s.io/client-go/tools/cache package, the Add method on the Indexer (and Store) interface is defined with the signature: Add(obj interface{}) error [1][2][3]. The behavior of the Add method is to insert an object into the cache [1][2]. It uses a KeyFunc—a function provided when the Indexer is initialized—to determine the storage key for the object [1][3]. If the KeyFunc returns an error, the Add operation fails and returns that error [1][2]. If a transformer is configured for the store, the object is transformed before being stored [1]. Regarding duplicate keys, the underlying storage mechanism (ThreadSafeStore) manages the accumulation of objects [1][3]. When an object is added, if an item with the same key already exists, the new object effectively replaces or updates the existing entry within the accumulator for that key, maintaining a consistent state for the informer's cache [3][4]. The Store interface ensures that operations are performed against the accumulator associated with the object's key [3][4].

Citations:


Assert indexer insertion succeeds.

cache.Indexer.Add can return an error, so ignoring nodeIndexer.Add(node) can silently drop test node setup failures. Pass t into the helper and assert the error.

Proposed fix
-func createTestController(kubeClient *k8sfakeclient.Clientset, operatorClient *operatorfake.Clientset, fakeOperatorClient v1helpers.StaticPodOperatorClient, nodes []*corev1.Node) *PeriodicBackupController {
+func createTestController(t *testing.T, kubeClient *k8sfakeclient.Clientset, operatorClient *operatorfake.Clientset, fakeOperatorClient v1helpers.StaticPodOperatorClient, nodes []*corev1.Node) *PeriodicBackupController {
+	t.Helper()
 	...
 	for _, node := range nodes {
-		nodeIndexer.Add(node)
+		require.NoError(t, nodeIndexer.Add(node))
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func createTestController(kubeClient *k8sfakeclient.Clientset, operatorClient *operatorfake.Clientset, fakeOperatorClient v1helpers.StaticPodOperatorClient, nodes []*corev1.Node) *PeriodicBackupController {
fakeClock := testingclock.NewFakeClock(time.Now())
eventRecorder := events.NewInMemoryRecorder("test", fakeClock)
if nodes == nil {
nodes = []*corev1.Node{
{
ObjectMeta: v1.ObjectMeta{
Name: "master-0",
Labels: map[string]string{"node-role.kubernetes.io/master": ""},
},
},
}
}
nodeIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
for _, node := range nodes {
nodeIndexer.Add(node)
func createTestController(t *testing.T, kubeClient *k8sfakeclient.Clientset, operatorClient *operatorfake.Clientset, fakeOperatorClient v1helpers.StaticPodOperatorClient, nodes []*corev1.Node) *PeriodicBackupController {
t.Helper()
fakeClock := testingclock.NewFakeClock(time.Now())
eventRecorder := events.NewInMemoryRecorder("test", fakeClock)
if nodes == nil {
nodes = []*corev1.Node{
{
ObjectMeta: v1.ObjectMeta{
Name: "master-0",
Labels: map[string]string{"node-role.kubernetes.io/master": ""},
},
},
}
}
nodeIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
for _, node := range nodes {
require.NoError(t, nodeIndexer.Add(node))
}
🤖 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/operator/periodicbackupcontroller/periodicbackupcontroller_test.go`
around lines 40 - 57, Update createTestController to accept *testing.T, then
check the error returned by each nodeIndexer.Add(node) call and fail the test
via the testing handle when insertion fails. Update all callers of
createTestController to pass their test handle.

Source: Path instructions

Comment on lines +83 to +88
func newFakeSyncContext(key string) *fakeSyncContext {
return &fakeSyncContext{
key: key,
recorder: events.NewInMemoryRecorder("test", clock.RealClock{}),
queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go --items all
rg -n -C2 'newFakeSyncContext\(|NewRateLimitingQueue|\.ShutDown\(' pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go

Repository: openshift/cluster-etcd-operator

Length of output: 3766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Go module versions and relevant imports to decide whether workqueue.ShutDown exists/needs import path.
printf '== go.mod deps around k8s/client-go/workqueue ==\n'
rg -n 'k8s\.io/(client-go|apimachinery)|workqueue' go.mod go.sum || true

printf '\n== test helper and callers context ==\n'
sed -n '1,270p' pkg/operator/periodicbackupcontroller/periodicbackupcontroller_test.go

printf '\n== workqueue ShutDown references in repo ==\n'
rg -n 'ShutDown|NewRateLimitingQueue' pkg -g '*.go' || true

Repository: openshift/cluster-etcd-operator

Length of output: 10812


🌐 Web query:

k8s.io/client-go v0.36.2 util/workqueue RateLimitingInterface ShutDown

💡 Result:

In k8s.io/client-go v0.36.2, the ShutDown method on a RateLimitingInterface (which implements the base workqueue.Interface) is used to gracefully signal that the queue should stop accepting new items [1][2][3]. When ShutDown is called: 1. The queue ignores all new items added to it [1][2][3]. 2. Any worker goroutines currently blocked on Get will receive a shutdown signal (shutdown=true), allowing them to exit their processing loops [2][4][3]. 3. If workers are still processing items, they will continue to process them until the queue is empty before receiving the signal, depending on the implementation details and how Get is handled [1][2][3]. Common Usage In Kubernetes controller patterns, ShutDown is typically invoked using the defer statement at the beginning of the Run method to ensure the queue closes when the controller stops [5][6][4]. Example pattern: func (c *Controller) Run(stopCh <-chan struct{}) { defer c.queue.ShutDown //... controller logic } Alternative: ShutDownWithDrain If you require that the queue finishes processing all items currently in the queue before the shutdown signal is fully realized, you should use ShutDownWithDrain instead [1][2][3]. Note that for ShutDownWithDrain to function correctly, workers must call Done on each item after processing; otherwise, the drain operation may block indefinitely [2][3]. Important Version Note As of v0.36.2, RateLimitingInterface is deprecated in favor of TypedRateLimitingInterface[T] [7][1]. It is recommended to migrate to the typed interface to take advantage of Go generics and improved type safety [7][1].

Citations:


Shut down test workqueues.

Each NewRateLimitingQueue creates delayed-queue goroutines, and the helper owns the queue via fakeSyncContext.Queue(). Accept t *testing.T, register t.Cleanup(queue.ShutDown), and update the three newFakeSyncContext(...) callers.

🤖 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/operator/periodicbackupcontroller/periodicbackupcontroller_test.go`
around lines 83 - 88, Update newFakeSyncContext to accept *testing.T, create the
rate-limiting queue as a local variable, and register t.Cleanup to call
queue.ShutDown before returning the context. Update all three newFakeSyncContext
callers to pass their testing handle while preserving the existing context
setup.

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

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant