Skip to content

Draft: Calico Secondary-NIC L2 & L3 Annotation Support#6936

Draft
aaaaaaaalex wants to merge 3 commits into
kubev2v:mainfrom
aaaaaaaalex:rebase-exp-against-main
Draft

Draft: Calico Secondary-NIC L2 & L3 Annotation Support#6936
aaaaaaaalex wants to merge 3 commits into
kubev2v:mainfrom
aaaaaaaalex:rebase-exp-against-main

Conversation

@aaaaaaaalex

Copy link
Copy Markdown

Augments vSphere builder to preserve NIC addresses for later emission of Calico annotations on VM template.

Adds a (unstructured) client for fetching and sparsely-reading Calico datatypes.

  • Client is unstructured since pinning isn't possible for yet-to-be-released datatypes.

Validates (when a NAD has type: calico) Network mappings make sense.

  • E.G. IPPool fits projectcalico.org/Network, that NIC-referenced NAD is valid, etc.

Unit testing for new codepaths.

@aaaaaaaalex aaaaaaaalex marked this pull request as draft June 5, 2026 13:52
@aaaaaaaalex aaaaaaaalex changed the title Calico Secondary-NIC L2 & L3 Annotation Support Draft: Calico Secondary-NIC L2 & L3 Annotation Support Jun 5, 2026
@aaaaaaaalex aaaaaaaalex force-pushed the rebase-exp-against-main branch from 15911b7 to 66b57ac Compare June 5, 2026 13:54
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Calico network validation for migration plans and preserves Calico MAC and static IP annotations on migrated VMs. It introduces plan-level NAD/network/IPPool validation to detect configuration issues, per-VM checks to ensure static IPs match Calico subnets and IPPools, NAD config parsing for Calico detection, and vSphere builder integration to collect and annotate VMs with Calico network metadata.

Changes

Calico Network Validation and Preservation

Layer / File(s) Summary
RBAC permissions for Calico resources
operator/.downstream_manifests, operator/.upstream_manifests, operator/config/rbac/forklift-controller_role.yaml
Operator and controller RBAC rules grant get, list, watch permissions for Calico networks and ippools CRDs under projectcalico.org.
Calico validation data structures
pkg/controller/plan/adapter/base/calico_validation.go
Defines ResolvedCalicoNAD (network/VLAN/eligible pools), CalicoValidationCache (map of resolved NADs), CalicoNADIssue (resource-level failures), and CalicoValidationResult (issues + cache) for sharing validation results between plan and VM scopes.
Calico client libraries for Network and IPPool discovery
pkg/lib/client/calico/network.go, pkg/lib/client/calico/network_test.go, pkg/lib/client/calico/ippool.go, pkg/lib/client/calico/ippool_test.go
GetNetwork fetches and parses Calico Network CRs with L2Bridge VLAN/subnet details; ListIPPools discovers IPPools and provides eligibility filters (EligiblePools, EligiblePoolForIP) based on VLAN subnet containment and CIDR membership.
NAD model and parsing infrastructure
pkg/controller/provider/model/ocp/model.go, pkg/controller/provider/model/ocp/model_test.go, pkg/controller/plan/adapter/base/nad.go, pkg/controller/plan/adapter/base/nad_test.go, pkg/controller/provider/web/ocp/base.go
Extends OCP CNI model with CalicoCNIType, Calico fields (Network, VLAN), and ReferencesCalicoNetwork() predicate; ParseNAD unmarshals NAD config with error context; FetchAndParseNAD retrieves NADs from the cluster; web handler uses centralized parsing for NAD discovery.
Calico annotation setter functions
pkg/controller/plan/adapter/base/calico.go, pkg/controller/plan/adapter/base/calico_test.go
SetCalicoMAC and SetCalicoStaticIPs write Calico interface annotations to VM metadata under cni.projectcalico.org/<ifname>.{hwAddr,ipAddrs}; both lazily initialize ObjectMeta.Annotations and handle JSON marshaling for IP arrays.
Validator interface extension for Calico
pkg/controller/plan/adapter/base/doc.go
Extends Validator interface with ValidateCalicoNADs (plan-level NAD/network/IPPool validation) and CalicoVMIssues (per-VM issue generation); introduces CalicoIssueKind enum categorizing failures (network/VLAN/IPPool issues) and CalicoIssue struct capturing issue kind, network name, VLAN ID, and source VM IP.
vSphere Calico network validation implementation
pkg/controller/plan/adapter/vsphere/validator.go, pkg/controller/plan/adapter/vsphere/validator_test.go
ValidateCalicoNADs walks plan NetworkMap, fetches/parses referenced Calico NADs, resolves VLAN entries from Network L2Bridge, checks IPPool eligibility, and builds a cache of validated NADs with aggregated resource-level issues; CalicoVMIssues consumes cache to emit per-VM/per-NIC/per-IP failures when PreserveStaticIPs is enabled; supporting helpers handle NIC resolution, VLAN lookup, and subnet membership checking.
vSphere builder Calico annotation collection and application
pkg/controller/plan/adapter/vsphere/builder.go, pkg/controller/plan/adapter/vsphere/builder_test.go
Refactors findInterfaceIps as standalone helper; extends mapNetworks to detect Calico NAD destinations, cache parsed NAD configs, track interface MACs and static IPs per NIC, and apply collected Calico annotations via SetCalicoMAC/SetCalicoStaticIPs after network construction; includes multi-NIC and conditional static IP preservation test coverage.
Calico validation stubs for non-vSphere providers
pkg/controller/plan/adapter/hyperv/validator.go, pkg/controller/plan/adapter/ocp/validator.go, pkg/controller/plan/adapter/openstack/validator.go, pkg/controller/plan/adapter/ovfbase/validator.go, pkg/controller/plan/adapter/ovirt/validator.go, pkg/provider/ec2/controller/validator/noop.go
HyperV, OCP, OpenStack, OVF, oVirt, and EC2 providers implement ValidateCalicoNADs and CalicoVMIssues as no-ops, indicating non-vSphere/non-OCP providers are not targets for Calico preservation logic.
Plan validation orchestration and VM-level Calico checks
pkg/controller/plan/validation.go
Plan validation now calls validateCalicoNetwork to cache resolved NADs/networks/IPPools at plan scope, passes cache to validateVM for per-VM issue evaluation, creates two new VM-level condition types for Calico subnet/IPPool failures, formats plan-level CalicoNetworkInvalid condition with deduplicated NAD issue details, and integrates validator.CalicoVMIssues into per-VM validation workflow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Suggested reviewers

  • mnecas
  • mrnold
  • solenoci
  • Hazanel

🐰 Calico networks now validated with care,
MAC addresses and IPs preserved everywhere,
vSphere VMs annotated just right,
Plan-level caching keeps validation tight,
Per-VM checks ensure static IPs delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is related to the changeset and explains the key objectives: preserving NIC addresses, adding Calico client support, validating network mappings, and including unit tests.
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.
Title check ✅ Passed The title accurately captures the main feature: adding Calico annotation support for secondary NICs on vSphere, specifically covering both L2 and L3 preservation scenarios.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

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

Caution

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

⚠️ Outside diff range comments (1)
pkg/controller/plan/adapter/vsphere/builder_test.go (1)

1135-1145: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Migrate this test client bootstrap to the shared k8s testutil helpers.

createBuilder currently hand-rolls scheme/client setup; please wire it through pkg/provider/testutil/k8s.go to keep test client construction consistent across the repo.

As per coding guidelines, **/*_test.go: Unit tests should use shared fake client helpers from pkg/provider/testutil/k8s.go including NewScheme() and NewFakeClient(objs...) before hand-rolling custom fakes.

🤖 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/controller/plan/adapter/vsphere/builder_test.go` around lines 1135 -
1145, The test currently hand-rolls a scheme and fake client in createBuilder;
replace that with the shared k8s testutil helpers by calling
testutil.NewScheme() to get a scheme, register the same APIs (v1, core, rbacv1,
k8snet, v1beta1) onto that scheme, then create the fake client via
testutil.NewFakeClient(objs...) (or NewFakeClient(scheme, objs...) if your
helper takes a scheme) and assign it to the client used to construct the Builder
so createBuilder uses testutil.NewScheme() and testutil.NewFakeClient instead of
manual runtime.NewScheme()/fake.NewClientBuilder().
🧹 Nitpick comments (2)
pkg/controller/plan/adapter/vsphere/validator_test.go (1)

693-700: ⚡ Quick win

Use the shared Kubernetes fake-client test helpers instead of hand-rolled setup.

Please switch this local scheme/client construction to pkg/provider/testutil/k8s.go helpers (NewScheme() and NewFakeClient(objs...)) so these tests follow the repo’s standard test harness.

As per coding guidelines, unit tests should use shared fake client helpers from pkg/provider/testutil/k8s.go including NewScheme() and NewFakeClient(objs...) before hand-rolling custom fakes.

🤖 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/controller/plan/adapter/vsphere/validator_test.go` around lines 693 -
700, Replace the hand-rolled scheme/client setup that calls runtime.NewScheme(),
k8snet.AddToScheme, scheme.AddKnownTypeWithName(...
calicoclient.NetworkGVK/IPPoolGVK ...) and
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(k8sObjs...).Build()
with the repository test helpers: call NewScheme() instead of runtime.NewScheme
and use NewFakeClient(k8sObjs...) to obtain the fake client (replace variables
scheme and c accordingly); remove the manual calico/k8snet type registrations
and adjust imports to use pkg/provider/testutil/k8s.go's NewScheme and
NewFakeClient so tests use the shared fake-client harness.
pkg/lib/client/calico/ippool_test.go (1)

42-66: ⚡ Quick win

Add a negative-path test for malformed or missing spec.cidr in ListIPPools.

Current coverage is mostly happy-path; adding explicit malformed spec.cidr cases would harden confidence in parse/error behavior used by Calico validation.

As per coding guidelines, **/*_test.go: Coverage: Strive for high test coverage on critical logic paths.

🤖 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/lib/client/calico/ippool_test.go` around lines 42 - 66, Add a
negative-path unit test for ListIPPools to exercise malformed or missing
spec.cidr: create a new test (e.g., TestListIPPools_MalformedCIDR) that uses
newFakeClientWithIPPools and makeIPPool variants where spec.cidr is empty or
contains invalid CIDR strings, call ListIPPools(context.Background(), c), assert
that the function returns the expected error or skips/filters the bad entries
(matching the package behavior), and validate that valid pools are still
returned correctly; reference the existing TestListIPPools_Multiple,
ListIPPools, newFakeClientWithIPPools, and makeIPPool to model setup and
assertions.
🤖 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/controller/plan/adapter/base/nad_test.go`:
- Around line 15-25: Replace the custom test helper newFakeClientWithNADs with
the shared test utilities: call the shared NewScheme() to build the scheme and
NewFakeClient(objs...) to create the fake client instead of using
runtime.NewScheme(), k8snet.AddToScheme, and fake.NewClientBuilder; update tests
that call newFakeClientWithNADs to pass the NetworkAttachmentDefinition objects
into NewFakeClient and remove the custom builder function so the test uses
pkg/provider/testutil/k8s.go's NewScheme and NewFakeClient helpers.

In `@pkg/controller/plan/adapter/base/nad.go`:
- Around line 18-20: The raw error from the client Get call in the NAD retrieval
must be wrapped with the NAD namespace/name for context: replace the plain
"return nil, err" after "if err := c.Get(ctx, key, nad); err != nil {" with a
wrapped error that includes key.Namespace and key.Name (e.g., using
fmt.Errorf("failed to get NetworkAttachmentDefinition %s/%s: %w", key.Namespace,
key.Name, err) or errors.Wrapf) so callers receive namespace/name context;
ensure the fmt or errors package is imported and maintain the same return
signature (return nil, wrappedErr).

In `@pkg/lib/client/calico/ippool_test.go`:
- Around line 20-29: Replace the hand-rolled fake client in
newFakeClientWithIPPools by using the shared test util helpers: call
pkg/provider/testutil/k8s.NewScheme() to obtain the scheme, register the IPPool
GVK types on that scheme (using IPPoolGVK and
IPPoolGVK.GroupVersion().WithKind("IPPoolList") as needed), then create the fake
client with pkg/provider/testutil/k8s.NewFakeClient(objs...) instead of manual
fake.NewClientBuilder code; update newFakeClientWithIPPools to return the shared
NewFakeClient result (or a builder-compatible object) and remove the manual
runtime.NewScheme()/fake.NewClientBuilder() logic.

In `@pkg/lib/client/calico/network_test.go`:
- Around line 142-144: The test currently checks "if got.L2Bridge == nil ||
len(got.L2Bridge.VLANs) != 3" which still evaluates len(got.L2Bridge.VLANs) when
L2Bridge is nil; change the assertion in the test (where "got" and
"got.L2Bridge.VLANs" are referenced) to first explicitly fail if got.L2Bridge ==
nil (e.g. t.Fatalf("got.L2Bridge is nil")) and only afterwards check
len(got.L2Bridge.VLANs) != 3 and fail with a message including the actual
length; this prevents a nil dereference while preserving the existing failure
messages.
- Around line 25-37: Replace the local test helper newFakeClientWith with the
shared test utilities: call testutil.NewScheme() to obtain the scheme and use
testutil.NewFakeClient(objs...) to create the fake client instead of building a
fake.ClientBuilder manually; ensure NetworkGVK and its list-kind are registered
on the returned scheme if NewScheme() does not already include them, and update
tests to use the returned client from NewFakeClient rather than the builder.

---

Outside diff comments:
In `@pkg/controller/plan/adapter/vsphere/builder_test.go`:
- Around line 1135-1145: The test currently hand-rolls a scheme and fake client
in createBuilder; replace that with the shared k8s testutil helpers by calling
testutil.NewScheme() to get a scheme, register the same APIs (v1, core, rbacv1,
k8snet, v1beta1) onto that scheme, then create the fake client via
testutil.NewFakeClient(objs...) (or NewFakeClient(scheme, objs...) if your
helper takes a scheme) and assign it to the client used to construct the Builder
so createBuilder uses testutil.NewScheme() and testutil.NewFakeClient instead of
manual runtime.NewScheme()/fake.NewClientBuilder().

---

Nitpick comments:
In `@pkg/controller/plan/adapter/vsphere/validator_test.go`:
- Around line 693-700: Replace the hand-rolled scheme/client setup that calls
runtime.NewScheme(), k8snet.AddToScheme, scheme.AddKnownTypeWithName(...
calicoclient.NetworkGVK/IPPoolGVK ...) and
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(k8sObjs...).Build()
with the repository test helpers: call NewScheme() instead of runtime.NewScheme
and use NewFakeClient(k8sObjs...) to obtain the fake client (replace variables
scheme and c accordingly); remove the manual calico/k8snet type registrations
and adjust imports to use pkg/provider/testutil/k8s.go's NewScheme and
NewFakeClient so tests use the shared fake-client harness.

In `@pkg/lib/client/calico/ippool_test.go`:
- Around line 42-66: Add a negative-path unit test for ListIPPools to exercise
malformed or missing spec.cidr: create a new test (e.g.,
TestListIPPools_MalformedCIDR) that uses newFakeClientWithIPPools and makeIPPool
variants where spec.cidr is empty or contains invalid CIDR strings, call
ListIPPools(context.Background(), c), assert that the function returns the
expected error or skips/filters the bad entries (matching the package behavior),
and validate that valid pools are still returned correctly; reference the
existing TestListIPPools_Multiple, ListIPPools, newFakeClientWithIPPools, and
makeIPPool to model setup and assertions.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aad89d37-fe44-4448-b712-986f8bb239ab

📥 Commits

Reviewing files that changed from the base of the PR and between c0113cb and 15911b7.

⛔ Files ignored due to path filters (1)
  • CLAUDE.md is excluded by !**/*.md
📒 Files selected for processing (27)
  • operator/.downstream_manifests
  • operator/.upstream_manifests
  • operator/config/rbac/forklift-controller_role.yaml
  • pkg/controller/plan/adapter/base/calico.go
  • pkg/controller/plan/adapter/base/calico_test.go
  • pkg/controller/plan/adapter/base/calico_validation.go
  • pkg/controller/plan/adapter/base/doc.go
  • pkg/controller/plan/adapter/base/nad.go
  • pkg/controller/plan/adapter/base/nad_test.go
  • pkg/controller/plan/adapter/hyperv/validator.go
  • pkg/controller/plan/adapter/ocp/validator.go
  • pkg/controller/plan/adapter/openstack/validator.go
  • pkg/controller/plan/adapter/ovfbase/validator.go
  • pkg/controller/plan/adapter/ovirt/validator.go
  • pkg/controller/plan/adapter/vsphere/builder.go
  • pkg/controller/plan/adapter/vsphere/builder_test.go
  • pkg/controller/plan/adapter/vsphere/validator.go
  • pkg/controller/plan/adapter/vsphere/validator_test.go
  • pkg/controller/plan/validation.go
  • pkg/controller/provider/model/ocp/model.go
  • pkg/controller/provider/model/ocp/model_test.go
  • pkg/controller/provider/web/ocp/base.go
  • pkg/lib/client/calico/ippool.go
  • pkg/lib/client/calico/ippool_test.go
  • pkg/lib/client/calico/network.go
  • pkg/lib/client/calico/network_test.go
  • pkg/provider/ec2/controller/validator/noop.go
👮 Files not reviewed due to content moderation or server errors (1)
  • operator/.upstream_manifests

Comment on lines +15 to +25
func newFakeClientWithNADs(nads ...*k8snet.NetworkAttachmentDefinition) (*fake.ClientBuilder, error) {
scheme := runtime.NewScheme()
if err := k8snet.AddToScheme(scheme); err != nil {
return nil, err
}
b := fake.NewClientBuilder().WithScheme(scheme)
for _, nad := range nads {
b = b.WithRuntimeObjects(nad)
}
return b, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use shared k8s fake-client test helpers instead of a custom builder helper.

newFakeClientWithNADs hand-rolls scheme/client setup that should come from the shared test utility to keep test scaffolding consistent across adapters.

As per coding guidelines **/*_test.go: "Unit tests should use shared fake client helpers from pkg/provider/testutil/k8s.go including NewScheme() and NewFakeClient(objs...) before hand-rolling custom fakes."

🤖 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/controller/plan/adapter/base/nad_test.go` around lines 15 - 25, Replace
the custom test helper newFakeClientWithNADs with the shared test utilities:
call the shared NewScheme() to build the scheme and NewFakeClient(objs...) to
create the fake client instead of using runtime.NewScheme(), k8snet.AddToScheme,
and fake.NewClientBuilder; update tests that call newFakeClientWithNADs to pass
the NetworkAttachmentDefinition objects into NewFakeClient and remove the custom
builder function so the test uses pkg/provider/testutil/k8s.go's NewScheme and
NewFakeClient helpers.

Comment on lines +18 to +20
if err := c.Get(ctx, key, nad); err != nil {
return nil, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap NAD GET errors with namespace/name context.

Returning the raw client error here hides which NAD failed and makes reconciliation failures harder to triage.

Proposed fix
 import (
 	"context"
+	"fmt"
@@
 	if err := c.Get(ctx, key, nad); err != nil {
-		return nil, err
+		return nil, fmt.Errorf("get NetworkAttachmentDefinition %s/%s: %w", namespace, name, err)
 	}

As per coding guidelines **/*.go: "Errors must be checked, wrapped with context, and propagated."

📝 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 := c.Get(ctx, key, nad); err != nil {
return nil, err
}
if err := c.Get(ctx, key, nad); err != nil {
return nil, fmt.Errorf("get NetworkAttachmentDefinition %s/%s: %w", namespace, name, err)
}
🤖 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/controller/plan/adapter/base/nad.go` around lines 18 - 20, The raw error
from the client Get call in the NAD retrieval must be wrapped with the NAD
namespace/name for context: replace the plain "return nil, err" after "if err :=
c.Get(ctx, key, nad); err != nil {" with a wrapped error that includes
key.Namespace and key.Name (e.g., using fmt.Errorf("failed to get
NetworkAttachmentDefinition %s/%s: %w", key.Namespace, key.Name, err) or
errors.Wrapf) so callers receive namespace/name context; ensure the fmt or
errors package is imported and maintain the same return signature (return nil,
wrappedErr).

Comment on lines +20 to +29
func newFakeClientWithIPPools(objs ...runtime.Object) *fake.ClientBuilder {
scheme := runtime.NewScheme()
scheme.AddKnownTypeWithName(IPPoolGVK, &unstructured.Unstructured{})
scheme.AddKnownTypeWithName(IPPoolGVK.GroupVersion().WithKind("IPPoolList"), &unstructured.UnstructuredList{})
b := fake.NewClientBuilder().WithScheme(scheme)
for _, o := range objs {
b = b.WithRuntimeObjects(o)
}
return b
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace the local fake-client constructor with shared testutil helpers.

This test helper should use pkg/provider/testutil/k8s.go (NewScheme() / NewFakeClient(objs...)) instead of a hand-rolled fake setup.

As per coding guidelines, **/*_test.go: Unit tests should use shared fake client helpers from pkg/provider/testutil/k8s.go including NewScheme() and NewFakeClient(objs...) before hand-rolling custom fakes.

🤖 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/lib/client/calico/ippool_test.go` around lines 20 - 29, Replace the
hand-rolled fake client in newFakeClientWithIPPools by using the shared test
util helpers: call pkg/provider/testutil/k8s.NewScheme() to obtain the scheme,
register the IPPool GVK types on that scheme (using IPPoolGVK and
IPPoolGVK.GroupVersion().WithKind("IPPoolList") as needed), then create the fake
client with pkg/provider/testutil/k8s.NewFakeClient(objs...) instead of manual
fake.NewClientBuilder code; update newFakeClientWithIPPools to return the shared
NewFakeClient result (or a builder-compatible object) and remove the manual
runtime.NewScheme()/fake.NewClientBuilder() logic.

Comment on lines +25 to +37
func newFakeClientWith(objs ...runtime.Object) (*fake.ClientBuilder, error) {
scheme := runtime.NewScheme()
// Register list-kind so the fake client knows how to map GVK -> Go type
// for unstructured Network objects. For Gets without a list-kind, the
// fake client falls back to using the GVK from the object itself.
scheme.AddKnownTypeWithName(NetworkGVK, &unstructured.Unstructured{})
scheme.AddKnownTypeWithName(NetworkGVK.GroupVersion().WithKind("NetworkList"), &unstructured.UnstructuredList{})
b := fake.NewClientBuilder().WithScheme(scheme)
for _, o := range objs {
b = b.WithRuntimeObjects(o)
}
return b, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use shared fake-client test utilities instead of a local builder helper.

Please switch this helper to pkg/provider/testutil/k8s.go (NewScheme() / NewFakeClient(objs...)) for consistency with the repository’s test harness conventions.

As per coding guidelines, **/*_test.go: Unit tests should use shared fake client helpers from pkg/provider/testutil/k8s.go including NewScheme() and NewFakeClient(objs...) before hand-rolling custom fakes.

🤖 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/lib/client/calico/network_test.go` around lines 25 - 37, Replace the
local test helper newFakeClientWith with the shared test utilities: call
testutil.NewScheme() to obtain the scheme and use
testutil.NewFakeClient(objs...) to create the fake client instead of building a
fake.ClientBuilder manually; ensure NetworkGVK and its list-kind are registered
on the returned scheme if NewScheme() does not already include them, and update
tests to use the returned client from NewFakeClient rather than the builder.

Comment thread pkg/lib/client/calico/network_test.go Outdated
Comment on lines +142 to +144
if got.L2Bridge == nil || len(got.L2Bridge.VLANs) != 3 {
t.Fatalf("VLANs len = %d, want 3", len(got.L2Bridge.VLANs))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid nil dereference in the failure assertion path.

When got.L2Bridge == nil, the current t.Fatalf still evaluates len(got.L2Bridge.VLANs), which can panic and hide the real regression.

Suggested fix
-	if got.L2Bridge == nil || len(got.L2Bridge.VLANs) != 3 {
-		t.Fatalf("VLANs len = %d, want 3", len(got.L2Bridge.VLANs))
-	}
+	if got.L2Bridge == nil {
+		t.Fatal("L2Bridge = nil, want non-nil")
+	}
+	if len(got.L2Bridge.VLANs) != 3 {
+		t.Fatalf("VLANs len = %d, want 3", len(got.L2Bridge.VLANs))
+	}
📝 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 got.L2Bridge == nil || len(got.L2Bridge.VLANs) != 3 {
t.Fatalf("VLANs len = %d, want 3", len(got.L2Bridge.VLANs))
}
if got.L2Bridge == nil {
t.Fatal("L2Bridge = nil, want non-nil")
}
if len(got.L2Bridge.VLANs) != 3 {
t.Fatalf("VLANs len = %d, want 3", len(got.L2Bridge.VLANs))
}
🤖 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/lib/client/calico/network_test.go` around lines 142 - 144, The test
currently checks "if got.L2Bridge == nil || len(got.L2Bridge.VLANs) != 3" which
still evaluates len(got.L2Bridge.VLANs) when L2Bridge is nil; change the
assertion in the test (where "got" and "got.L2Bridge.VLANs" are referenced) to
first explicitly fail if got.L2Bridge == nil (e.g. t.Fatalf("got.L2Bridge is
nil")) and only afterwards check len(got.L2Bridge.VLANs) != 3 and fail with a
message including the actual length; this prevents a nil dereference while
preserving the existing failure messages.

@aaaaaaaalex aaaaaaaalex force-pushed the rebase-exp-against-main branch from 66b57ac to 21b64ce Compare June 8, 2026 13:54
@mrnold

mrnold commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Linking to #6203

The core of this seems straightforward enough, setting some annotations for a particular network type. I have read only a little bit about Calico though, and couldn't quite figure out how it works with forklift. Is the idea to do a regular migration from VMware to a Kubernetes cluster with Calico installed, and then having these annotations enables Calico to do something special with the migrated VMs? Or does Calico work with VMware/Kubernetes in some other way?

@aaaaaaaalex

Copy link
Copy Markdown
Author

Hiya @mrnold, thanks for having a look, and sorry for the delay in replying.

Some of the features I referenced in this code (e.g. NIC-specific annotations) are still unreleased so documentation is scarce at the moment, sorry if there was any confusion! Yes this PR would aim to facilitate migrations into Calico clusters with Kubevirt as the virtualization orchestrator, and Multus for multi-NIC support.

Calico has support for requesting a particular IP address with our cni.projectcalico.org/ipAddrs (and a hwAddr counterpart also) and we're adding more features to support multiple NICs across Calico and Calico Enterprise (including the upcoming Network CRD that this PR is querying).

I'm still very much deep in WIP territory, but with this PR we'd like to detect the variant of Calico running, and depending on the answer, do one / many / all of the following:

  • Detect a VM's primary IP and MAC, instruct Calico to preserve and perform IPAM for them during migration.
  • Detect a VM's secondary NIC addresses and do the same.
  • Detect Network CRs are present and valid in the case where a NIC should be on a particular VLAN and must be hooked up to the host's trunk by Calico CNI.

I haven't addressed all of these points in this PR yet (only naively programmed per-interface annotations), and will hold the PR in draft until they're all addressed.

aaaaaaaalex and others added 3 commits June 22, 2026 11:41
… expected behaviour

Emit Calico annotations for VM template in vSphere builder

use ipAddrs annotation rather than ipAddrsNoIpam. + update some docstrings

validation+testing for calico in VSphere, + stubs in other providers.

* When implicit-VLAN is used, infer the correct VLAN from the l2Bridge spec, rather than reporting VLAN=0.
* Richer error reporting when JSON formatting fails on fetching NAD.

adds new CalicoIssue when there IS an l2Bridge spec, but no VLAN inside

Factors-out NetworkConfig fetch&parse where duplicated code existed
Note:
pkg/controller/plan/validation.go seemed to have a bug where, if the last
NAD in the list failed to parse, the resultant error would not follow
the same (log&continue) codepath as the prior NADs. Instead, the
err would bubble out of the for-loop and get retured by `validateUserDefinedNetwork`.
This commit makes the error handling consistent for all iterations.

separate pure-Calico misconfigurations from VM config mismatches

*Caches fetched NADs in mapNetworks to avoid repeated fetches for the same NAD.
*Adds new Calico Issue "NAD unreadable"
*Increased UT test-case coverage

Claude summary of this branch

Grant forklift-controller RBAC for Calico Network/IPPool reads

Signed-off-by: Alex O'Regan <alex.oregan@tigera.io>
A NAD whose Spec.Config is just {"type": "calico"} (no "network" field) is
a valid Calico-CNI configuration — it requests legacy L3 IPAM mode. But
Forklift's identity-preservation work only fires for L2-attached NADs
(gated on ReferencesCalicoNetwork() == "type==calico" AND "network != \"\""),
so a user opting into this without an L2 reference silently loses MAC/IP
preservation at migration time.

This commit surfaces that gap as a Warn-class Plan condition instead of
silently skipping the NAD.

Changes:
* New CalicoIssueKind: NADMissingNetwork.
* New CalicoValidationResult.Warnings slice, distinct from .Issues so the
  dispatcher can render Critical and Warn classes independently.
* vSphere ValidateCalicoNADs: emit the warning when type==calico and
  network is empty, between NAD parse and the existing non-Calico skip.
* New Plan condition type CalicoNetworkWarning (Category: Warn) alongside
  CalicoNetworkInvalid (Category: Critical). Dispatcher logic deduplicated
  via a shared buildCalicoNADCondition helper so both classes go through
  the same itemize/format path.
* Unit tests: warn-in-isolation case + Critical+Warn coexistence case to
  lock in the dispatcher's independence between the two classes.

Stale-condition cleanup is handled by the existing BeginStaging /
EndStaging pattern in controller.go — when the user fixes the NAD the
warning is naturally pruned on the next reconcile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Alex O'Regan <alex.oregan@tigera.io>
* pkg/controller/plan/adapter/base/nad.go: wrap the NAD GET error with
  namespace/name context using fmt.Errorf("%w"). Downstream k8serr.IsNotFound
  and meta.IsNoMatchError both unwrap through %w, so the validator's
  CalicoNetworkNotFound / NoMatchError disambiguation continues to work.

* pkg/lib/client/calico/network_test.go: split the combined
  "if got.L2Bridge == nil || len(got.L2Bridge.VLANs) != 3" check into two
  distinct t.Fatal calls. The original code would have panicked on a nil
  L2Bridge because the t.Fatalf args still dereferenced through it, masking
  the real regression with a panic stack trace.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Alex O'Regan <alex.oregan@tigera.io>
@aaaaaaaalex aaaaaaaalex force-pushed the rebase-exp-against-main branch from 21b64ce to 3feff37 Compare June 22, 2026 10:42
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants