Skip to content

OSAC-2350, OSAC-2482: create MetalLB IPAddressPool at subnet creation and stamp VIP range for DHCP exclusion - #258

Merged
omer-vishlitzky merged 1 commit into
osac-project:mainfrom
ori-amizur:OSAC-2350
Aug 13, 2026
Merged

OSAC-2350, OSAC-2482: create MetalLB IPAddressPool at subnet creation and stamp VIP range for DHCP exclusion#258
omer-vishlitzky merged 1 commit into
osac-project:mainfrom
ori-amizur:OSAC-2350

Conversation

@ori-amizur

@ori-amizur ori-amizur commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

At subnet creation, the operator now resolves vip_prefix_length from the NetworkClass and creates a MetalLB IPAddressPool on the target cluster covering the VIP sub-range of the subnet CIDR. On subnet deletion the IPAddressPool is removed before AAP deprovisioning.

The VIP range is also stamped as an osac.openshift.io/vip-cidr annotation on the Subnet CR so the fabric manager Ansible role can read it and exclude that range from DHCP.

Assisted-by: Claude Code noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Added automatic VIP range calculation and formatting for IPv4 subnets.
    • Added support for resolving NetworkClass VIP settings during subnet provisioning.
    • Added automatic MetalLB IP address pool creation and updates after successful provisioning.
    • Added cleanup of IP address pools during subnet deprovisioning.
  • Bug Fixes

    • Improved validation and error handling for invalid VIP configurations and missing resources.
  • Tests

    • Added coverage for VIP range calculation, validation, and formatting.

@openshift-ci-robot

openshift-ci-robot commented Aug 11, 2026

Copy link
Copy Markdown

@ori-amizur: This pull request references OSAC-2350 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the task to target the "5.0.0" version, but no target version was set.

This pull request references OSAC-2482 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the task to target the "5.0.0" version, but no target version was set.

Details

In response to this:

At subnet creation, the operator now resolves vip_prefix_length from the NetworkClass and creates a MetalLB IPAddressPool on the target cluster covering the VIP sub-range of the subnet CIDR. On subnet deletion the IPAddressPool is removed before AAP deprovisioning.

The VIP range is also stamped as an osac.openshift.io/vip-cidr annotation on the Subnet CR so the fabric manager Ansible role can read it and exclude that range from DHCP.

Assisted-by: Claude Code noreply@anthropic.com

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 openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The operator now calculates VIP ranges from NetworkClass settings, annotates subnets, and manages corresponding MetalLB IPAddressPool resources throughout provisioning and deprovisioning.

Changes

VIP pool provisioning

Layer / File(s) Summary
VIP range calculation and validation
osac-operator/helpers/viprange.go, osac-operator/helpers/viprange_test.go, osac-operator/helpers/helpers_suite_test.go
The helpers calculate and format IPv4 VIP ranges. Tests cover valid ranges, invalid inputs, and formatting.
NetworkClass VIP resolution
osac-operator/internal/controller/subnet_controller.go, osac-operator/internal/controller/subnet_names.go, osac-operator/cmd/main.go
The operator shares an optional NetworkClassesClient with the subnet reconciler. The reconciler resolves VIP CIDR annotations from NetworkClass and subnet data.
IPAddressPool lifecycle and permissions
osac-operator/internal/controller/subnet_controller.go, osac-operator/config/rbac/role.yaml
The subnet reconciler creates, updates, and deletes MetalLB IPAddressPool resources. RBAC grants the required permissions.

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

Sequence Diagram(s)

sequenceDiagram
  participant SubnetReconciler
  participant NetworkClassesClient
  participant VIPRangeHelpers
  participant TargetClusterClient
  participant MetalLB
  SubnetReconciler->>NetworkClassesClient: fetch NetworkClass VIP prefix
  NetworkClassesClient-->>SubnetReconciler: return configuration
  SubnetReconciler->>VIPRangeHelpers: calculate VIP CIDR
  VIPRangeHelpers-->>SubnetReconciler: return VIP range
  SubnetReconciler->>TargetClusterClient: obtain target-cluster client
  SubnetReconciler->>MetalLB: create or update IPAddressPool
  MetalLB-->>SubnetReconciler: return resource state
  SubnetReconciler->>MetalLB: delete IPAddressPool during deprovisioning
Loading

Possibly related PRs

Suggested labels: enhancement, go

Suggested reviewers: jhernand, eliorerz

🚥 Pre-merge checks | ✅ 4 | ❌ 7

❌ Failed checks (1 warning, 6 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
No-Hardcoded-Secrets ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
No-Weak-Crypto ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
No-Injection-Vectors ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Container-Privileges ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
No-Sensitive-Data-In-Logs ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Ai-Attribution ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: creating MetalLB IPAddressPool resources and stamping the VIP range for DHCP exclusion.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:02 AM UTC · Completed 11:21 AM UTC

Commit: 4d3daf7 · View workflow run →

coderabbitai[bot]
coderabbitai Bot previously requested changes Aug 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@osac-operator/internal/controller/subnet_controller.go`:
- Around line 215-219: The error from resolveVIPCIDR in handleUpdate must be
returned so reconciliation retries instead of continuing without VIP
configuration; update osac-operator/internal/controller/subnet_controller.go
lines 215-219 accordingly. Also handle the NestedStringSlice error before
comparing or updating spec.addresses, returning it rather than treating invalid
data as an empty list; update lines 415-421 in the same file.
- Around line 230-232: Update the subnet reconciliation logic around
resolveVIPCIDR to handle an empty vipCIDR by deleting the existing
osacVIPCIDRAnnotation and removing the corresponding MetalLB IPAddressPool for
the configured NetworkClass. Preserve the current annotation update behavior
when vipCIDR is non-empty, and ensure annotationsChanged reflects deletion as
well as modification.
- Around line 430-435: Update deleteMetalLBIPAddressPool to return nil before
calling getTargetClient when subnet.Annotations[osacVIPCIDRAnnotation] is empty,
while preserving the existing r.mgr nil guard and normal target-client cleanup
for Subnets with a VIP annotation.
🪄 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: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a91a76d4-3c21-43d8-b526-0da0fe72e972

📥 Commits

Reviewing files that changed from the base of the PR and between d44f34a and 4d3daf7.

📒 Files selected for processing (6)
  • osac-operator/cmd/main.go
  • osac-operator/config/rbac/role.yaml
  • osac-operator/helpers/helpers_suite_test.go
  • osac-operator/helpers/viprange.go
  • osac-operator/helpers/viprange_test.go
  • osac-operator/internal/controller/subnet_controller.go

Comment on lines +215 to +219
// Resolve VIP prefix length from NetworkClass (if gRPC is available)
vipCIDR := ""
if r.NetworkClassesClient != nil && vnet.Spec.NetworkClass != "" && subnet.Spec.IPv4CIDR != "" {
vipCIDR, _ = r.resolveVIPCIDR(ctx, vnet.Spec.NetworkClass, subnet.Spec.IPv4CIDR)
}

Copy link
Copy Markdown

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

Handle the returned errors.

Line 218 can continue provisioning without the required VIP CIDR and MetalLB pool when NetworkClass lookup or CIDR calculation fails. Line 416 can treat an invalid spec.addresses field as an empty list and overwrite the external resource.

  • osac-operator/internal/controller/subnet_controller.go#L215-L219: Return the resolveVIPCIDR error from handleUpdate so reconciliation retries instead of marking the Subnet ready without VIP configuration.
  • osac-operator/internal/controller/subnet_controller.go#L415-L421: Return the NestedStringSlice error before comparing or updating spec.addresses.

As per path instructions, Go code must never ignore error returns.

📍 Affects 1 file
  • osac-operator/internal/controller/subnet_controller.go#L215-L219 (this comment)
  • osac-operator/internal/controller/subnet_controller.go#L415-L421
🤖 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 `@osac-operator/internal/controller/subnet_controller.go` around lines 215 -
219, The error from resolveVIPCIDR in handleUpdate must be returned so
reconciliation retries instead of continuing without VIP configuration; update
osac-operator/internal/controller/subnet_controller.go lines 215-219
accordingly. Also handle the NestedStringSlice error before comparing or
updating spec.addresses, returning it rather than treating invalid data as an
empty list; update lines 415-421 in the same file.

Source: Path instructions

Comment on lines +230 to +232
if vipCIDR != "" && subnet.Annotations[osacVIPCIDRAnnotation] != vipCIDR {
subnet.Annotations[osacVIPCIDRAnnotation] = vipCIDR
annotationsChanged = true

Copy link
Copy Markdown

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

Remove obsolete VIP state when VIP configuration is absent.

When resolveVIPCIDR returns an empty value because vip_prefix_length is removed, Line 230 preserves the old osac.openshift.io/vip-cidr annotation. The existing MetalLB IPAddressPool then remains configured for a range that the NetworkClass no longer defines.

Delete the annotation and remove the corresponding pool when a configured NetworkClass no longer has a VIP prefix.

🤖 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 `@osac-operator/internal/controller/subnet_controller.go` around lines 230 -
232, Update the subnet reconciliation logic around resolveVIPCIDR to handle an
empty vipCIDR by deleting the existing osacVIPCIDRAnnotation and removing the
corresponding MetalLB IPAddressPool for the configured NetworkClass. Preserve
the current annotation update behavior when vipCIDR is non-empty, and ensure
annotationsChanged reflects deletion as well as modification.

Comment thread osac-operator/internal/controller/subnet_controller.go
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [test-inadequate] osac-operator/internal/controller/subnet_controller.go — No controller-level tests cover the new MetalLB IPAddressPool lifecycle (ensureMetalLBIPAddressPool, deleteMetalLBIPAddressPool, resolveVIPCIDR) or the VIP CIDR annotation stamping logic in handleUpdate. The only new tests are for the helpers.ComputeVIPRange utility. Key untested scenarios include: IPAddressPool creation after provisioning, IPAddressPool update when VIP CIDR changes, IPAddressPool deletion during subnet teardown, and resolveVIPCIDR error path.

  • [missing-documentation] osac-operator/README.md:21 — README describes Subnet as "represents a subnet within a VirtualNetwork" but does not mention MetalLB IPAddressPool management or the new osac.openshift.io/vip-cidr annotation. The Subnet controller now creates, updates, and deletes metallb.io/IPAddressPool resources on the target cluster as part of subnet provisioning.

Low

  • [error-handling] osac-operator/internal/controller/subnet_controller.go:236 — When resolveVIPCIDR fails, the controller logs the error and requeues but does not return an error, meaning it won't surface in controller metrics. This is consistent with how other precondition failures work in this controller.

  • [edge-case] osac-operator/internal/controller/subnet_controller.go — In ensureMetalLBIPAddressPool, SetLabels overwrites all labels on update, which could remove labels added by other operators or manual intervention.

  • [edge-case] osac-operator/helpers/viprange.goComputeVIPRange uses uint32(1) << (32 - subnetOnes). For a /0 subnet (subnetOnes == 0), this overflows to 0. No explicit guard, though /0 subnets are unrealistic in OSAC.

  • [fail-open] osac-operator/internal/controller/subnet_controller.goensureMetalLBIPAddressPool and deleteMetalLBIPAddressPool silently return nil when the VIP CIDR annotation is empty or mgr is nil. This is intentional since VIP pools are optional based on NetworkClass configuration.

  • [input-validation] osac-operator/internal/controller/subnet_controller.govipCIDR from the annotation is passed through to MetalLB verbatim. Risk is limited to operators with Subnet write access, within the expected trust boundary.

  • [scope-coherence] osac-operator/internal/controller/subnet_controller.go — Existing Ready subnets created before this feature will get the VIP CIDR annotation on next reconcile; the IPAddressPool is created after provisioning succeeds. Self-healing via normal reconciliation.

  • [naming-convention] osac-operator/internal/controller/subnet_controller.go:233annotationsChanged naming diverges from the established needsUpdate pattern in other controllers (externalip_controller.go, externalipattachment_controller.go).

  • [code-organization] osac-operator/internal/controller/subnet_controller.go:393ipAddressPoolName function is missing a doc comment, while adjacent functions (ensureMetalLBIPAddressPool, deleteMetalLBIPAddressPool) have doc comments.

  • [missing-documentation] osac-operator/.claude/rules/controller-patterns.md:67 — Controller patterns doc describes the AAP callback pattern, but the Subnet controller now has a post-provisioning step (ensureMetalLBIPAddressPool) that runs outside the RunProvisioningLifecycle callbacks.

Previous run

Review

Findings

Medium

  • [edge-case] osac-operator/internal/controller/subnet_controller.go — When the VIP CIDR annotation is cleared during handleUpdate (e.g., NetworkClass has its vip_prefix_length removed), the code deletes the annotation from the Subnet CR but does not delete the corresponding MetalLB IPAddressPool from the target cluster. Later, during handleDelete, deleteMetalLBIPAddressPool skips cleanup because the annotation is empty (subnet.Annotations[osacVIPCIDRAnnotation] == ""). This leaves an orphaned IPAddressPool on the target cluster.
    Remediation: When removing the VIP CIDR annotation (the vipCIDR == "" && subnet.Annotations[osacVIPCIDRAnnotation] != "" branch), call deleteMetalLBIPAddressPool before clearing the annotation. Alternatively, make deleteMetalLBIPAddressPool unconditionally attempt deletion by name (ipAddressPoolName(subnet.Name)) regardless of the annotation value, and only skip on NotFound.

  • [architectural-coherence] osac-operator/internal/controller/subnet_controller.go — The SubnetReconciler now directly depends on privatev1.NetworkClassesClient (a gRPC client to the fulfillment-service) to fetch the NetworkClass and resolve vip_prefix_length. Other networking controllers (VirtualNetwork, SecurityGroup) rely solely on the dispatcher.Resolver abstraction. The NetworkClassesClient was previously only used by the NetworkClassCapabilitiesReconciler and the dispatcheradapter. Introducing a direct gRPC dependency into a resource controller breaks the pattern where resource controllers interact with external services only through the provisioning provider and the dispatcher.
    Remediation: Consider exposing vip_prefix_length through the existing dispatcher.Resolver or a similar abstraction rather than passing the raw gRPC client.

  • [architectural-coherence] osac-operator/internal/controller/subnet_controller.goresolveVIPCIDR makes a synchronous gRPC call to the fulfillment-service on every subnet reconciliation. A fulfillment-service outage would cause subnet reconciliation to requeue indefinitely (RequeueAfter: defaultPreconditionRequeueInterval). This contradicts the documented pattern that the operator works when the fulfillment-service is down — an existing Ready subnet would stall in the provisioning controller if the fulfillment-service is unreachable.
    Remediation: Consider making the VIP resolution best-effort (proceed without VIP annotation if the gRPC call fails) or pre-resolving the VIP prefix length during the NetworkClass capabilities sync and storing it in a locally accessible form.

Low

  • [control-flow-pattern] osac-operator/internal/controller/subnet_controller.go — The new annotationsChanged block does not return early after r.Update, unlike every other networking controller (VirtualNetwork, SecurityGroup, ExternalIPPool) which returns ctrl.Result{}, nil after annotation updates so the next reconciliation proceeds with persisted annotations.

  • [performance] osac-operator/internal/controller/subnet_controller.goresolveVIPCIDR makes a gRPC call to the fulfillment-service on every reconcile when preconditions are met, including reconciles where provisioning evaluates to Skip. For many subnets, this could generate significant gRPC load.

  • [edge-case] osac-operator/helpers/viprange.go — For subnet CIDRs with very small prefix lengths (e.g., /0), uint32(1) << (32 - subnetOnes) overflows uint32. While /0 CIDRs are not practical for real subnets, the function does not guard against this.

  • [permission-expansion] osac-operator/config/rbac/role.yaml — The metallb.io/ipaddresspools RBAC rule is in the hub cluster's ClusterRole, but ensureMetalLBIPAddressPool and deleteMetalLBIPAddressPool both use getTargetClient. If hub and target are separate clusters, this hub-cluster permission may be over-provisioned.

  • [naming-convention] osac-operator/internal/controller/subnet_names.goosacVIPCIDRAnnotation is declared as const using string concatenation, creating a mixed declaration style in subnet_names.go where the adjacent var block uses fmt.Sprintf.

  • [undocumented-behavioral-change] osac-operator/README.md — The Subnet description does not mention the new MetalLB IPAddressPool lifecycle or the dependency on MetalLB CRDs on the target cluster.

  • [undocumented-behavioral-change] osac-operator/AGENTS.md — The Provisioning section describes the Subnet controller's lifecycle as AAP-based only. It now also performs direct Kubernetes operations (MetalLB IPAddressPool) and fetches NetworkClass data via gRPC.

  • [undocumented-annotation] osac-operator/AGENTS.md — The new osac.openshift.io/vip-cidr annotation stamped on Subnet CRs is not documented. It is a contract between the operator and AAP playbooks for DHCP exclusion.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

Medium

  • [test-adequacy] osac-operator/internal/controller/subnet_controller.go — The new VIP CIDR resolution, MetalLB IPAddressPool creation, and deletion code paths have zero unit test coverage. Existing tests set NetworkClassesClient to nil, so resolveVIPCIDR, ensureMetalLBIPAddressPool, and deleteMetalLBIPAddressPool are never exercised. Consider adding test cases with a mock NetworkClassesClient covering VIP CIDR annotation stamping/clearing, MetalLB pool create/update/delete, and gRPC error handling.

  • [scope-coherence] osac-operator/cmd/main.go:518 — The networkClassesClient is created outside the grpcConn != nil && networkingNamespace != "" guard block, diverging from the established pattern where all networking gRPC usage is gated on both conditions. The client is created even when networkingNamespace is empty. Consider moving the creation inside the existing guard block to maintain consistency.

  • [naming-convention] osac-operator/internal/controller/subnet_controller.go:54NetworkClassesClient is exported (uppercase) while analogous fields (mgr, targetCluster) are unexported. The NetworkClassCapabilitiesReconciler in the same package uses networkClassesClient (lowercase) for the identical type. Consider renaming to networkClassesClient for consistency.

  • [stale-resource-description] osac-operator/README.md:21 — The Subnet resource description says only "represents a subnet within a VirtualNetwork" but the controller now also manages MetalLB IPAddressPool lifecycle and stamps a osac.openshift.io/vip-cidr annotation. Consider updating to mention the MetalLB integration.

  • [stale-resource-description] osac-operator/AGENTS.md:13 — Same gap: the Subnet description does not mention the new MetalLB IPAddressPool lifecycle, VIP CIDR annotation, or the fact that the Subnet controller now directly manages MetalLB resources outside the AAP provisioning flow.

Low

  • [error-handling-gap] osac-operator/internal/controller/subnet_controller.go — A gRPC error in resolveVIPCIDR causes the reconciler to requeue with defaultPreconditionRequeueInterval, delaying provisioning. While this follows the established requeue pattern and only affects subnets where all three conditions hold (NetworkClassesClient != nil, NetworkClass != "", IPv4CIDR != ""), a persistent gRPC outage could delay VIP-eligible subnets.

  • [edge-case] osac-operator/internal/controller/subnet_controller.goensureMetalLBIPAddressPool triggers an update only when addresses differ. When an update IS triggered, it replaces the full spec (resetting autoAssign/avoidBuggyIPs). However, if only autoAssign or avoidBuggyIPs were tampered with while addresses remain unchanged, drift would not be corrected.

  • [edge-case] osac-operator/internal/controller/subnet_controller.godeleteMetalLBIPAddressPool skips deletion when subnet.Annotations[osacVIPCIDRAnnotation] is empty. If the annotation was cleared (e.g., when vip_prefix_length is removed from NetworkClass) but the IPAddressPool was previously created, the pool would be orphaned. Consider always attempting deletion by deterministic name during subnet deletion.

  • [permission-expansion] osac-operator/config/rbac/role.yaml:79 — The manager-role ClusterRole gains create/delete/get/update on metallb.io/ipaddresspools. The RBAC correctly omits list/watch/patch (least-privilege), and autoAssign=false on created pools reduces blast radius. The expansion is tracked by OSAC-2350/OSAC-2482.

  • [input-validation] osac-operator/internal/controller/subnet_controller.goensureMetalLBIPAddressPool reads vipCIDR from the annotation without re-validating it as a well-formed CIDR before constructing the MetalLB object. The annotation is set by the controller itself via resolveVIPCIDR, but an actor with Subnet annotation-write RBAC could inject an arbitrary value.

  • [architectural-coherence] osac-operator/internal/controller/subnet_controller.go:52SubnetReconciler now directly depends on privatev1.NetworkClassesClient rather than using the pkg/dispatcher abstraction. This is justified since the dispatcher resolves managers, not raw spec fields like vip_prefix_length, but creates two parallel paths for NetworkClass data access.

  • [naming-convention] osac-operator/internal/controller/subnet_names.go:31ipAddressPoolGVK is defined in subnet_names.go, but all other _names.go files exclusively declare string labels, finalizers, and annotations. None import schema or declare GroupVersionKind values. Consider moving to subnet_controller.go alongside the MetalLB methods.

  • [label-key-naming] osac-operator/internal/controller/subnet_controller.go — The IPAddressPool label key is defined inline as osacPrefix + "/subnet". Other label keys in the codebase are declared as named variables in _names.go files.

  • [error-message-format] osac-operator/helpers/viprange.go:37 — Error message uses Go parameter name vipPrefixLength in camelCase. The codebase convention uses human-readable lowercase descriptions (e.g., "VIP prefix length").

Previous run (3)

Review

Findings

Medium

  • [logic-error] osac-operator/internal/controller/subnet_controller.godeleteMetalLBIPAddressPool skips deletion when subnet.Annotations[osacVIPCIDRAnnotation] is empty. If the VIP CIDR annotation is cleared before the Subnet is deleted (e.g., handleUpdate clears it when vipCIDR resolves to empty, or an external actor removes it), the MetalLB IPAddressPool will be orphaned on the target cluster. The pool name is deterministic (osac-subnet-{name}) and computable without the annotation, so the guard is unnecessarily restrictive.
    Remediation: Remove the subnet.Annotations[osacVIPCIDRAnnotation] == "" guard from deleteMetalLBIPAddressPool. Always attempt deletion by computed name and rely on the existing IsNotFound handling.

  • [race-condition] osac-operator/internal/controller/subnet_controller.go — The resolved VIP CIDR value (derived from NetworkClass.vip_prefix_length) is not included in the ComputeDesiredConfigVersion hash input. When the NetworkClass's vip_prefix_length changes, the annotation and MetalLB IPAddressPool update correctly, but AAP provisioning is not retriggered because DesiredConfigVersion (hashed from SubnetSpec + ImplementationStrategy) remains unchanged. This means the DHCP exclusion range configured by the AAP playbook (which reads the vip-cidr annotation) will become stale until the next unrelated spec change triggers reprovisioning.
    Remediation: Include the resolved vipCIDR in the struct passed to ComputeDesiredConfigVersion.

  • [naming-convention] osac-operator/internal/controller/subnet_names.go:28ipAddressPoolGVK is a schema.GroupVersionKind struct declared in subnet_names.go. Every other _names.go file in the controller package contains only string constants/vars for labels, annotations, finalizers, and namespace defaults — none import schema or define GVK structs.
    Remediation: Move ipAddressPoolGVK (and its schema import) into subnet_controller.go alongside the consuming code, or into a dedicated file like metallb_types.go.

Low

  • [naming-convention] osac-operator/internal/controller/subnet_names.go:23osacVIPCIDRAnnotation is declared as a const using string concatenation (osacPrefix + "/vip-cidr"). Most annotation/label/finalizer strings in _names.go files use var with fmt.Sprintf. Minor inconsistency; both approaches are functionally correct.

  • [code-organization] osac-operator/internal/controller/subnet_controller.go:53NetworkClassesClient is placed between mgr and NetworkingNamespace in the struct, breaking the field ordering convention used by sibling reconcilers. StorageReconciler places its gRPC client fields after targetCluster.

  • [design-smell] osac-operator/internal/controller/subnet_controller.go:210resolveVIPCIDR makes a gRPC call on every reconciliation, even when the VIP CIDR annotation is already set. Since controller-runtime reconciliation is event-driven (not polling), the per-reconcile cost is acceptable for correctness (ensures annotation stays in sync if NetworkClass changes). Noted as a design observation.

  • [naming-alignment] osac-operator/internal/controller/subnet_names.go:25ipAddressPoolGVK uses MetalLB API version v1beta1. MetalLB graduated IPAddressPool to v1 in version 0.13.10+. If target clusters run a recent MetalLB version, v1 would be the canonical API version.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (4)

Review

Findings

Medium

  • [error-handling-gap] osac-operator/internal/controller/subnet_controller.go:218 — The error returned by resolveVIPCIDR is silently discarded (vipCIDR, _ = r.resolveVIPCIDR(...)). If the gRPC call to the fulfillment-service fails (transient network error, auth failure) or FormatVIPRangeCIDR returns an error, the VIP CIDR annotation will not be stamped and no log message is emitted. The subnet proceeds to provisioning without a VIP range. Since ensureMetalLBIPAddressPool only runs in the OnSuccess callback and checks the annotation, a transient gRPC failure during the first reconcile that reaches handleUpdate results in a Ready subnet with no MetalLB IPAddressPool.
    Remediation: At minimum, log the error at Warning level. Consider returning a RequeueAfter when resolveVIPCIDR fails so the annotation gets stamped before provisioning proceeds.

  • [logic-error] osac-operator/internal/controller/subnet_controller.go:218 — The VIP CIDR annotation (osac.openshift.io/vip-cidr) is only set when vipCIDR != "" but is never removed. If a NetworkClass previously had vip_prefix_length and it is later removed, the stale annotation persists on the Subnet. This causes the MetalLB IPAddressPool to continue reflecting the old VIP range, and the DHCP exclusion range communicated via the annotation to AAP playbooks remains stale.
    Remediation: When vipCIDR is empty and the annotation is already set, explicitly delete the annotation (delete(subnet.Annotations, osacVIPCIDRAnnotation)) and set annotationsChanged = true. Consider also cleaning up the MetalLB IPAddressPool when the annotation is removed.

  • [error-handling-gap] osac-operator/internal/controller/subnet_controller.go:319 — The ensureMetalLBIPAddressPool call inside the OnSuccess callback cannot propagate errors to the reconcile loop. When it fails, the callback sets subnet.Status.Phase = SubnetPhaseFailed and returns, but RunProvisioningLifecycle returns ctrl.Result{}, nil after invoking OnSuccess. On subsequent reconciles, EvaluateAction sees the latest provision job succeeded (the AAP job completed) and returns Skip — the OnSuccess callback never fires again. The subnet remains permanently stuck in Failed state with no automatic retry.
    Remediation: Move MetalLB IPAddressPool creation out of the OnSuccess callback into the main reconciliation flow where errors can be properly returned, or add explicit requeue logic after the callback detects failure.

  • [naming-convention] osac-operator/internal/controller/subnet_controller.go:46 — The new constant metallbNamespace duplicates the existing externalIPDefaultMetalLBNamespace constant in externalip_names.go (also set to "metallb-system"). Both are in the same package, creating a maintenance risk if the namespace ever changes.
    Remediation: Remove metallbNamespace and reuse the existing constant, or extract a shared constant name.

Low

  • [permission-expansion] osac-operator/config/rbac/role.yaml:79 — The new RBAC rule for metallb.io/ipaddresspools includes the patch verb, which is not used by any code path in this diff (the code uses Create, Get, Update, Delete). While the same full verb set is used elsewhere in the ClusterRole, removing unused verbs is a least-privilege improvement.

  • [code-organization] osac-operator/internal/controller/subnet_controller.go:48 — The annotation constant osacVIPCIDRAnnotation is defined inline. The established convention places annotation/label constants in constants_common.go or {resource}_names.go files.

  • [code-organization] osac-operator/internal/controller/subnet_controller.go:51 — Package-level var ipAddressPoolGVK is defined in subnet_controller.go. Other resource-scoped identifiers are placed in {resource}_names.go files for discoverability.

  • [naming-convention] osac-operator/helpers/viprange.go:1 — Missing Apache 2.0 copyright headers in the new helper files viprange.go and viprange_test.go. The co-authored helpers_suite_test.go in this same PR includes the header.

  • [design-smell] osac-operator/cmd/main.go:524 — A new privatev1.NetworkClassesClient is instantiated in setupNetworkingControllers while an identical client is already created in setupNetworkClassCapabilitiesController on the same grpcConn. Consider hoisting the client creation and passing it to both functions.

  • [missing-test] osac-operator/internal/controller/subnet_controller.go:319 — No unit tests exercise the new controller-level code paths (VIP CIDR annotation stamping, ensureMetalLBIPAddressPool, deleteMetalLBIPAddressPool, resolveVIPCIDR). Existing tests construct SubnetReconciler without NetworkClassesClient, making the new behavior a no-op. The helpers/viprange_test.go provides good coverage of the VIP computation logic, but the controller integration is untested.

  • [pattern-inconsistency] osac-operator/helpers/helpers_suite_test.go:1 — Existing unit tests in helpers/ use the standard Go testing package. This PR introduces Ginkgo, creating two testing frameworks in the same package.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 2:00 PM UTC · Ended 2:02 PM UTC

Commit: e42fddd · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 2:03 PM UTC · Completed 2:40 PM UTC

Commit: b17d9ae · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@omer-vishlitzky
omer-vishlitzky dismissed coderabbitai[bot]’s stale review August 12, 2026 10:13

Auto-dismissed: only Prow labels gate merging

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:14 AM UTC · Completed 10:33 AM UTC

Commit: da66657 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:47 AM UTC · Completed 11:05 AM UTC

Commit: b85d30c · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot removed the requires-manual-review Review requires human judgment label Aug 12, 2026
@omer-vishlitzky
omer-vishlitzky dismissed fullsend-ai-review[bot]’s stale review August 12, 2026 11:05

Auto-dismissed: only Prow labels gate merging

… and stamp VIP range for DHCP exclusion

At subnet creation, the operator now resolves vip_prefix_length from the
NetworkClass and creates a MetalLB IPAddressPool on the target cluster
covering the VIP sub-range of the subnet CIDR. On subnet deletion the
IPAddressPool is removed before AAP deprovisioning.

The VIP range is also stamped as an osac.openshift.io/vip-cidr annotation
on the Subnet CR so the fabric manager Ansible role can read it and
exclude that range from DHCP.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Ori Amizur <oamizur@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:40 PM UTC · Completed 1:00 PM UTC

Commit: f73a0b3 · View workflow run →

coderabbitai[bot]
coderabbitai Bot previously requested changes Aug 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
osac-operator/internal/controller/subnet_controller.go (1)

250-262: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Include VIP CIDR changes in the provisioning version.

A NetworkClass VIP prefix change updates the annotation at Line 250. ComputeDesiredConfigVersion only receives subnet.Spec and implementationStrategy. AAP provisioning can therefore remain skipped while DHCP exclusion retains the previous VIP range.

Include the resolved VIP CIDR, or a NetworkClass revision, in the desired configuration input. Add coverage for a changed vip_prefix_length.

🤖 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 `@osac-operator/internal/controller/subnet_controller.go` around lines 250 -
262, Update ComputeDesiredConfigVersion and its call sites to include the
resolved vipCIDR or NetworkClass revision in the desired configuration input, so
VIP prefix changes produce a new provisioning version and trigger AAP
reprovisioning. Add coverage verifying that changing vip_prefix_length changes
the computed version.
🤖 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 `@osac-operator/internal/controller/subnet_controller.go`:
- Around line 229-255: The VIP annotation cleanup currently treats skipped
resolution as an empty result. In the reconciliation flow around resolveVIPCIDR,
track whether VIP resolution completed successfully, and only delete
osacVIPCIDRAnnotation when resolution completed and returned no CIDR; preserve
the existing annotation when networkClassesClient is unavailable or resolution
is skipped.
- Line 258: Remove customer network data from the logging calls in
subnet_controller.go: at lines 258-258, update the log in the annotation update
flow to omit the vipCIDR field; at lines 438-438 and 453-453, update the
corresponding logs to omit the addresses field. Preserve non-sensitive fields
such as implementationStrategy and existing log behavior.

---

Outside diff comments:
In `@osac-operator/internal/controller/subnet_controller.go`:
- Around line 250-262: Update ComputeDesiredConfigVersion and its call sites to
include the resolved vipCIDR or NetworkClass revision in the desired
configuration input, so VIP prefix changes produce a new provisioning version
and trigger AAP reprovisioning. Add coverage verifying that changing
vip_prefix_length changes the computed version.
🪄 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: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5245663f-72cb-4ba3-a2dc-3b6876805bf4

📥 Commits

Reviewing files that changed from the base of the PR and between 4d3daf7 and f73a0b3.

📒 Files selected for processing (6)
  • osac-operator/cmd/main.go
  • osac-operator/config/rbac/role.yaml
  • osac-operator/helpers/viprange.go
  • osac-operator/helpers/viprange_test.go
  • osac-operator/internal/controller/subnet_controller.go
  • osac-operator/internal/controller/subnet_names.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • osac-operator/cmd/main.go
  • osac-operator/helpers/viprange.go

Comment on lines +229 to +255
// Resolve VIP prefix length from NetworkClass (if gRPC is available)
vipCIDR := ""
if r.networkClassesClient != nil && vnet.Spec.NetworkClass != "" && subnet.Spec.IPv4CIDR != "" {
var resolveErr error
vipCIDR, resolveErr = r.resolveVIPCIDR(ctx, vnet.Spec.NetworkClass, subnet.Spec.IPv4CIDR)
if resolveErr != nil {
log.Error(resolveErr, "failed to resolve VIP CIDR from NetworkClass, requeueing",
"networkClass", vnet.Spec.NetworkClass)
return ctrl.Result{RequeueAfter: defaultPreconditionRequeueInterval}, nil
}
}

// Stamp annotations for AAP playbooks (implementation strategy + VIP CIDR)
if subnet.Annotations == nil {
subnet.Annotations = make(map[string]string)
}
annotationsChanged := false
if subnet.Annotations[osacImplementationStrategyAnnotation] != implementationStrategy {
subnet.Annotations[osacImplementationStrategyAnnotation] = implementationStrategy
log.Info("setting implementation-strategy annotation", "strategy", implementationStrategy)
annotationsChanged = true
}
if vipCIDR != "" && subnet.Annotations[osacVIPCIDRAnnotation] != vipCIDR {
subnet.Annotations[osacVIPCIDRAnnotation] = vipCIDR
annotationsChanged = true
} else if vipCIDR == "" && subnet.Annotations[osacVIPCIDRAnnotation] != "" {
delete(subnet.Annotations, osacVIPCIDRAnnotation)
annotationsChanged = true

Copy link
Copy Markdown

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 delete a VIP CIDR when resolution was skipped.

Line 230 initializes vipCIDR as empty. If networkClassesClient is nil, Line 231 skips resolution. Line 253 then deletes an existing VIP annotation without confirming that the NetworkClass removed vip_prefix_length.

Track whether resolution completed. Delete osacVIPCIDRAnnotation only after a successful resolution returns no VIP CIDR.

🤖 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 `@osac-operator/internal/controller/subnet_controller.go` around lines 229 -
255, The VIP annotation cleanup currently treats skipped resolution as an empty
result. In the reconciliation flow around resolveVIPCIDR, track whether VIP
resolution completed successfully, and only delete osacVIPCIDRAnnotation when
resolution completed and returned no CIDR; preserve the existing annotation when
networkClassesClient is unavailable or resolution is skipped.

annotationsChanged = true
}
if annotationsChanged {
log.Info("updating annotations", "strategy", implementationStrategy, "vipCIDR", vipCIDR)

Copy link
Copy Markdown

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

Do not log VIP CIDR values. VIP CIDRs are customer network configuration. Remove the CIDR values from controller logs.

  • osac-operator/internal/controller/subnet_controller.go#L258-L258: Remove the vipCIDR log field.
  • osac-operator/internal/controller/subnet_controller.go#L438-L438: Remove the addresses log field.
  • osac-operator/internal/controller/subnet_controller.go#L453-L453: Remove the addresses log field.

As per coding guidelines, “Do not log ... customer data.”

📍 Affects 1 file
  • osac-operator/internal/controller/subnet_controller.go#L258-L258 (this comment)
  • osac-operator/internal/controller/subnet_controller.go#L438-L438
  • osac-operator/internal/controller/subnet_controller.go#L453-L453
🤖 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 `@osac-operator/internal/controller/subnet_controller.go` at line 258, Remove
customer network data from the logging calls in subnet_controller.go: at lines
258-258, update the log in the annotation update flow to omit the vipCIDR field;
at lines 438-438 and 453-453, update the corresponding logs to omit the
addresses field. Preserve non-sensitive fields such as implementationStrategy
and existing log behavior.

Source: Coding guidelines

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Aug 12, 2026
@omer-vishlitzky
omer-vishlitzky dismissed coderabbitai[bot]’s stale review August 12, 2026 13:00

Auto-dismissed: only Prow labels gate merging

@openshift-ci

openshift-ci Bot commented Aug 12, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: danmanor, ori-amizur

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

The pull request process is described 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

@danmanor

Copy link
Copy Markdown
Contributor

/retest

@github-actions

Copy link
Copy Markdown

Re-triggered failed runs:

  • label-gate (#31599224569)
  • E2E VMaaS Full Install (#31597458072)
  • label-gate (#31597457649)
  • pre-commit (#31597457736)

@omer-vishlitzky
omer-vishlitzky added this pull request to the merge queue Aug 13, 2026
Merged via the queue into osac-project:main with commit 931bcea Aug 13, 2026
92 of 100 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants