Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ad34dd0
feat(vlan): Add VLAN and VLANClaim types.
flynn-nrg Aug 17, 2026
15fd4cf
feat(vlan): Add RBAC definitions
flynn-nrg Aug 17, 2026
e94f461
feat(vlan): Add samples
flynn-nrg Aug 17, 2026
00ee498
feat(vlan): Implement API adapters
flynn-nrg Aug 17, 2026
5d5cca9
feat(vlan): Implement controllers for VLAN and VLANClaim
flynn-nrg Aug 17, 2026
8003b6c
feat(vlan): Wire new functionality into the system
flynn-nrg Aug 17, 2026
895f0e2
tests(vlan): Add tests for the new CRDs.
flynn-nrg Aug 17, 2026
b5e3712
docs: Update documentation.
flynn-nrg Aug 17, 2026
bcb82ee
style: reduce comment verbosity
flynn-nrg Aug 17, 2026
016fbd9
Merge branch 'main' into feat/vpn
flynn-nrg Aug 18, 2026
b9dd419
Merge branch 'main' of github.com:flynn-nrg/netbox-operator into feat…
flynn-nrg Aug 18, 2026
18515d0
Merge branch 'feat/vpn' of github.com:flynn-nrg/netbox-operator into …
flynn-nrg Aug 18, 2026
15e208e
feat(vlangroup): implement vlangroup type.
flynn-nrg Aug 18, 2026
113f85a
feat(vlangroup): implement vlangroup netbox client logic
flynn-nrg Aug 18, 2026
60a74bf
feat(vlangroup): implement controller and its tests.
flynn-nrg Aug 18, 2026
4a7811e
feat(vlangroup): add RBAC artefacts.
flynn-nrg Aug 18, 2026
09f63f4
feat(vlangroup): add sample file
flynn-nrg Aug 18, 2026
3e5eefe
tests: add e2e tests for vlangroup.
flynn-nrg Aug 18, 2026
cc49d58
feat(vlangroup): write everything together.
flynn-nrg Aug 18, 2026
90ea2b3
docs: update documentation.
flynn-nrg Aug 18, 2026
3524ee2
fix(vlan): Use metadata.Name. Vlans are looked up by VID and site.
flynn-nrg Aug 26, 2026
0d21c80
fix(vlan claim): address PR feedback
flynn-nrg Aug 26, 2026
43e1227
fix(vlan claim): address PR feedback
flynn-nrg Aug 26, 2026
de01795
Merge branch 'feat/vpn' into feat/vlangroup
flynn-nrg Aug 27, 2026
0e6a08b
fix(vlangroup): name & decode/encode bug
flynn-nrg Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Note: This requires a running NetBox instance that you can use (e.g. <https://de
- Prepare NetBox (based on the demo NetBox instance):
- Open <https://demo.netbox.dev/plugins/demo/login/> and create any user
- Open <https://demo.netbox.dev/user/api-tokens/> and create a token "0123456789abcdef0123456789abcdef01234567" with default settings
- Open <https://demo.netbox.dev/extras/custom-fields/add/> and create a custom field called "netboxOperatorRestorationHash" for Object types "IPAM > IP Address" and "IPAM > Prefix"
- Open <https://demo.netbox.dev/extras/custom-fields/add/> and create a custom field called "netboxOperatorRestorationHash" for Object types "IPAM > IP Address", "IPAM > Prefix" and "IPAM > VLAN"
- Open a new terminal window and export the following environment variables:
```bash
export NETBOX_HOST="demo.netbox.dev"
Expand All @@ -57,7 +57,7 @@ Note: This requires a running NetBox instance that you can use (e.g. <https://de

## Testing NetBox Operator using samples

In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix and PrefixClaim resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.
In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix, PrefixClaim, Vlan, VlanClaim and VlanGroup resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.

Example of assigning a Prefix using PrefixClaim:

Expand Down
24 changes: 24 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,28 @@ resources:
kind: IpRange
path: github.com/netbox-community/netbox-operator/api/v1
version: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: netbox.dev
kind: VlanClaim
path: github.com/netbox-community/netbox-operator/api/v1
version: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: netbox.dev
kind: Vlan
path: github.com/netbox-community/netbox-operator/api/v1
version: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: netbox.dev
kind: VlanGroup
path: github.com/netbox-community/netbox-operator/api/v1
version: v1
version: "3"
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ To optionally access the NetBox UI:

## Testing NetBox Operator using samples

In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix, and PrefixClaim resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.
In the folder `config/samples/` you can find example manifests to create IpAddress, IpAddressClaim, Prefix, PrefixClaim, Vlan, VlanClaim, and VlanGroup resources. Apply them to the cluster with `kubectl apply -f <file-name>` and use your favorite Kubernetes tools to display.

Example of assigning a Prefix using PrefixClaim:

Expand Down Expand Up @@ -93,17 +93,36 @@ This means that you need to plan your automation carefully. As a rule of thumb:

The same applies if you use parentPrefixSelector with PrefixClaims. The above example is IPv4 based but will be the same with IPv6 equivalents.

# VLAN Management

NetBox Operator supports managing [VLANs](https://github.com/netbox-community/netbox/blob/main/docs/models/ipam/vlan.md) through two custom resources:

- **Vlan**: Represents a single VLAN in NetBox. Similar to an IpAddress, it manages the lifecycle of a specific VLAN (`vid`, `site`, `tenant`, `status`) using the CR's Kubernetes object name as the NetBox VLAN name.
- **VlanClaim**: Claims a VID for a VLAN, either an exact `vid` or the next free one from a `vidRangeStart`/`vidRangeEnd` range. Similar to IpAddressClaim, it creates a child Vlan CR with the assigned VID. VID allocation is scoped to `.spec.site` since VLAN IDs are commonly only unique within a site.

## Example: Claiming a VLAN

1. Apply a VlanClaim: `kubectl apply -f config/samples/netbox_v1_vlanclaim.yaml`
2. Wait for ready condition: `kubectl wait vlanclaim vlanclaim-sample --for=condition=Ready`
3. List VlanClaim and Vlan resources: `kubectl get vlnc,vln`

`vid` and `vidRangeStart`/`vidRangeEnd` are mutually exclusive on `VlanClaim` — set exactly one form. When a range is used, the operator picks the next free VID in NetBox from that range.

Restoration (via `preserveInNetbox: true`) works the same way as for IP Addresses and Prefixes — the VLAN is preserved in NetBox upon CR deletion and can be reclaimed when the VlanClaim is re-created.

A **VlanGroup** resource manages a NetBox VLAN Group (a named container for organizing VLANs, optionally scoped to a Site and constrained to a `vidRangeStart`/`vidRangeEnd`) using the CR's Kubernetes object name as the NetBox VLAN Group name. Unlike `Vlan`, it has no claim counterpart — a VLAN Group is user-named rather than auto-allocated, so there's nothing to claim from a pool.

# Restoration from NetBox

In the case that the cluster containing the NetBox Custom Resources managed by this NetBox Operator is not backed up (e.g. using Velero), we need to be able to restore some information from NetBox. This includes two mechanisms implemented in this NetBox Operator:

- `IpAddressClaim` and `PrefixClaim` have the flag `preserveInNetbox` in their spec. If set to true, the NetBox Operator will not delete the assigned IP Address/Prefix in NetBox when the Kubernetes Custom Resource is deleted
- In NetBox, a custom field (by default `netboxOperatorRestorationHash`) is used to identify an IP Address/Prefix based on data from the IpAddressClaim/PrefixClaim resource
- `IpAddressClaim`, `PrefixClaim`, and `VlanClaim` have the flag `preserveInNetbox` in their spec. If set to true, the NetBox Operator will not delete the assigned IP Address/Prefix/VLAN in NetBox when the Kubernetes Custom Resource is deleted
- In NetBox, a custom field (by default `netboxOperatorRestorationHash`) is used to identify an IP Address/Prefix/VLAN based on data from the IpAddressClaim/PrefixClaim/VlanClaim resource

Use Cases for this Restoration:

- Disaster Recovery: In case the cluster is lost, IP Addresses can be restored with the IPAddressClaim only
- Sticky IPs: Some services do not handle changes to IPs well. This ensures the IP/Prefix assigned to a Custom Resource is always the same.
- Sticky IPs/VIDs: Some services do not handle changes to IPs or VLAN IDs well. This ensures the IP/Prefix/VID assigned to a Custom Resource is always the same.

# `ParentPrefixSelector` in `PrefixClaim`

Expand Down
161 changes: 161 additions & 0 deletions api/v1/vlan_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
Copyright 2026 Swisscom (Schweiz) AG.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// VlanSpec defines the desired state of Vlan
type VlanSpec struct {
// The VLAN ID (VID) to be assigned to this VLAN in NetBox
// Field is immutable, required, range from 1-4094
//+kubebuilder:validation:Required
//+kubebuilder:validation:Minimum=1
//+kubebuilder:validation:Maximum=4094
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'vid' is immutable"
Vid int32 `json:"vid"`

// The NetBox Site to be assigned to this resource in NetBox. Use the `name` value instead of the `slug` value
// Field is immutable, not required
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'site' is immutable"
Site string `json:"site,omitempty"`

// The NetBox Tenant to be assigned to this resource in NetBox. Use the `name` value instead of the `slug` value
// Field is immutable, not required
// Example: "Initech" or "Cyberdyne Systems"
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'tenant' is immutable"
Tenant string `json:"tenant,omitempty"`

// The operational status of the VLAN in NetBox
// Field is mutable, not required, defaults to "active"
//+kubebuilder:validation:Enum=active;reserved;deprecated
Status string `json:"status,omitempty"`

// The NetBox Custom Fields that should be added to the resource in NetBox.
// Note that currently only Text Type is supported (GitHub #129)
// More info on NetBox Custom Fields:
// https://github.com/netbox-community/netbox/blob/main/docs/customization/custom-fields.md
// Field is mutable, not required
// Example:
// customfield1: "Production"
// customfield2: "This is a string"
CustomFields map[string]string `json:"customFields,omitempty"`

// Comment that should be added to the resource in NetBox
// Field is mutable, not required
Comments string `json:"comments,omitempty"`

// Description that should be added to the resource in NetBox
// Field is mutable, not required
Description string `json:"description,omitempty"`

// Defines whether the Resource should be preserved in NetBox when the
// Kubernetes Resource is deleted.
// - When set to true, the resource will not be deleted but preserved in
// NetBox upon CR deletion
// - When set to false, the resource will be cleaned up in NetBox
// upon CR deletion
// Setting preserveInNetbox to true is mandatory if the user wants to restore
// resources from NetBox (e.g. Sticky VIDs even if resources are deleted and
// recreated in Kubernetes)
// Field is mutable, not required
PreserveInNetbox bool `json:"preserveInNetbox,omitempty"`
}

// VlanStatus defines the observed state of Vlan
type VlanStatus struct {
// The ID of the resource in NetBox
VlanId int64 `json:"id,omitempty"`

// Last updated, corresponds to the 'last_updated' returned by NetBox when NetBox Operator updates a resource in NetBox.
// Format: date-time
LastUpdated metav1.Time `json:"lastUpdated,omitempty"`

// The URL to the resource in the NetBox UI. Note that the base of this
// URL depends on the runtime config of NetBox Operator
VlanUrl string `json:"url,omitempty"`

// Conditions represent the latest available observations of an object's state
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion
//+kubebuilder:printcolumn:name="Vid",type=integer,JSONPath=`.spec.vid`
//+kubebuilder:printcolumn:name="Site",type=string,JSONPath=`.spec.site`
//+kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
//+kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id`
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
//+kubebuilder:resource:shortName=vln

// Vlan allows to create a NetBox VLAN. The Kubernetes object name
// (metadata.name) is used as the VLAN's name in NetBox. More info about
// NetBox VLANs: https://github.com/netbox-community/netbox/blob/main/docs/models/ipam/vlan.md
type Vlan struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec VlanSpec `json:"spec,omitempty"`
Status VlanStatus `json:"status,omitempty"`
}

func (v *Vlan) Conditions() *[]metav1.Condition {
return &v.Status.Conditions
}

//+kubebuilder:object:root=true

// VlanList contains a list of Vlan
type VlanList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Vlan `json:"items"`
}

func init() {
register(&Vlan{}, &VlanList{})
}

var ConditionVlanReadyTrue = metav1.Condition{
Type: "Ready",
Status: "True",
Reason: "VLANReservedInNetbox",
Message: "VLAN was reserved/updated in NetBox",
}

var ConditionVlanReadyFalse = metav1.Condition{
Type: "Ready",
Status: "False",
Reason: "FailedToReserveVLANInNetbox",
Message: "Failed to reserve VLAN in NetBox",
}

var ConditionVlanReadyFalseDeletionInProgress = metav1.Condition{
Type: "Ready",
Status: "False",
Reason: "DeletionInProgress",
Message: "VLAN deletion in progress",
}

var ConditionVlanReadyFalseDeletionFailed = metav1.Condition{
Type: "Ready",
Status: "False",
Reason: "FailedToDeleteVLANInNetbox",
Message: "Failed to delete VLAN in NetBox",
}
Loading