netwrangler
netwrangler
Kubernetes operator for managing virtual IP assignments on cluster nodes. Automatically distributes virtual IPs across nodes running service endpoints, and configures each node's network stack with the necessary addresses, routing tables, and nftables rules.
Written in Crystal, deployed via shell-operator.
Overview
The operator consists of two components:
- Controller (Deployment) — Watches Services, EndpointSlices, and custom resources. When a Service is labelled with
netwrangler.dev/ingress: enabled, the controller matches each externalIP to a VipNetwork by CIDR and allocates IPs to nodes that have ready endpoints, creating VipAddress resources. Also watches VipGateway resources and Pods labelled withnetwrangler.dev/gateway: enabled, selecting matching Pods via the gateway'spodSelectorto create VipRoute resources for gateway routing. - Agent (DaemonSet) — Runs on every node. Reads VipAddress and VipNetwork resources assigned to its node and configures IP addresses (
ip addr), policy routing (ip rule,ip route), nftables sets, and firewall mark rules accordingly. Also watches VipGateway and VipRoute resources: installs each gateway's policy rule and fail-closed fallback on all nodes, and routes to the gateway pods, resolved dynamically viaip route get. Cleans up configuration when VipAddresses, VipGateways or VipRoutes are removed.
Prerequisites
- Kubernetes cluster (v1.29+)
- Helm 3
- Nodes must run Linux on amd64 or arm64 (the agent image bundles statically compiled binaries)
Installation
Install the Helm chart from the OCI registry:
helm install netwrangler \
oci://registry-1.docker.io/jochen/netwrangler \
--namespace netwrangler \
--create-namespace
Note: Helm does not update CRDs on chart upgrades. When CRD schemas change, apply them manually:
helm show crds oci://registry-1.docker.io/jochen/netwrangler | kubectl apply --server-side --filename -
Configuration
After installing the chart, create the required custom resources before the controller can allocate IPs.
1. VipConfig
A cluster-scoped singleton resource defining the global firewall mark, route table, and routing policy rule priority configuration. Create one named default:
apiVersion: netwrangler.dev/v1
kind: VipConfig
metadata:
name: default
spec:
firewallMark: 0x01
firewallMarkMask: 0x7F
routeTable: 20001
routingPriority: 20001
maxAddressCount: 127
clusterCIDRs:
- 10.244.0.0/16
- 10.96.0.0/12
| Field | Description |
|---|---|
firewallMark |
Base firewall mark value for per-IP allocation (immutable). Each VipAddress receives a unique mark from the range [firewallMark, firewallMark + maxAddressCount - 1]. |
firewallMarkMask |
Bitmask determining which firewall mark bits are used (immutable) |
routeTable |
Base route table ID for per-IP allocation (immutable, min 1). The controller allocates a unique route table from a range starting at this value for each VipAddress. |
routingPriority |
Base routing preference / ip rule priority for per-IP allocation (immutable). The controller allocates a unique priority from a range starting at this value for each VipAddress. |
maxAddressCount |
Maximum number of virtual IPs supported across all networks |
clusterCIDRs |
Cluster-internal networks (optional). Traffic whose source and destination both lie in these networks is never marked, and a VipGateway whose destination overlaps one is refused. |
Each VipAddress receives its own firewallMark, routeTable, and routingPriority allocated by the controller from the base values defined on the VipConfig. All VipAddresses share the same firewallMarkMask.
Without a VipConfig the controller assigns no virtual IPs. It places at most min(maxAddressCount, 2^popcount(firewallMarkMask) - 1) virtual IPs of one Service on a node; the example's mask 0x7F has 7 bits, so the limit is 127. The limit applies when the controller keeps existing VipAddresses on their nodes, assigns new ones and rebalances.
2. VipNetwork
A cluster-scoped resource defining the network interface, gateway, and network addressing for a set of virtual IPs. Create one per VIP network:
apiVersion: netwrangler.dev/v1
kind: VipNetwork
metadata:
name: public
spec:
interface: eth0
gatewayIP: 192.168.111.254
networkCIDR: 192.168.111.0/24
sourceCIDRs:
- 10.0.0.0/8
| Field | Description |
|---|---|
interface |
Network interface name (immutable) |
gatewayIP |
Gateway address for routing |
networkCIDR |
Network in CIDR notation (e.g., 192.168.111.0/24) |
sourceCIDRs |
Additional source CIDRs for verdict map entries (opt) |
Use sourceCIDRs: ["0.0.0.0/0"] for a catch-all network that marks all traffic regardless of source. When 0.0.0.0/0 is present, other entries are ignored. Within one VipNetwork, an entry that lies inside a broader entry or inside networkCIDR is ignored with a warning. Source CIDRs of different VipNetworks may overlap.
3. Service
Label your Service to opt in to virtual IP management:
apiVersion: v1
kind: Service
metadata:
name: my-service
labels:
netwrangler.dev/ingress: enabled
netwrangler.dev/egress: enabled
spec:
type: ClusterIP
externalIPs:
- 203.0.113.10
- 203.0.113.11
ports:
- port: 80
protocol: TCP
selector:
app: my-app
Requirements:
- Label
netwrangler.dev/ingress: enabledopts the service into virtual IP management - Label
netwrangler.dev/egress: enabledenables egress marking for pods behind this service (optional, default: disabled) externalIPsmust list valid IPv4 addresses — each IP is automatically matched to a VipNetwork by CIDR. IPs may belong to different VipNetworks.- VipNetwork CIDRs must not overlap. Overlapping networks are automatically disabled, preferring to keep networks with existing VipAddresses or more specific (longer prefix) networks.
4. VipGateway
A cluster-scoped resource defining a gateway routing policy. The VipGateway selects Pods directly via podSelector.matchLabels. Pods must have the label netwrangler.dev/gateway: enabled to be watched by the controller. For each matching running Pod with a podIP, the controller creates a VipRoute resource, and the agent installs routes on every node.
apiVersion: netwrangler.dev/v1
kind: VipGateway
metadata:
name: my-gateway
spec:
destinationCIDR: 10.0.0.0/8
routeTable: 200
routingPriority: 100
podSelector:
matchLabels:
app: my-gateway-app
| Field | Description |
|---|---|
destinationCIDR |
Destination network in CIDR notation |
routeTable |
Linux routing table number (immutable, min 1) |
routingPriority |
Policy rule priority for ip rule (min 1) |
podSelector |
Pod selector with matchLabels for gateway pods |
All four fields are required.
Pods selected by a VipGateway must carry the label netwrangler.dev/gateway: enabled so the controller watches them. The podSelector.matchLabels on the VipGateway further narrows which of those Pods are associated with this particular gateway.
The destinationCIDR field specifies the destination network CIDR for the routes created by this gateway. The controller finds matching running Pods and creates one VipRoute per Pod, storing the Pod's IP address.
The agent installs the gateway's policy rule on every node as soon as the VipGateway exists, whether or not a gateway Pod is running:
ip rule add to <destinationCIDR> fwmark 0/<firewallMarkMask> table <routeTable> priority <routingPriority> protocol 110
The rule matches only packets without a VIP firewall mark. For each VipRoute, the agent resolves the route to the pod IP via ip route get <podIP> to find the outgoing interface and source address, and adds a route through the pod to the gateway's table. The resolution runs every 30 seconds, so routes heal themselves when pods reschedule or the CNI reconfigures.
Precedence over egress marking
Traffic from an egress-enabled endpoint to a gateway destination always takes the gateway path, whatever the routing priorities. The agent keeps every gateway destination in the nftables set gateway-destinations, and both egress vmap rules skip packets whose original destination is in that set. These packets carry no VIP mark and are never SNATed to a VIP. Replies to clients that connected to a VIP still carry the mark and follow the VIP's route table.
While no gateway Pod runs, traffic to the destination is unreachable on every node. The gateway table holds unreachable <destinationCIDR> metric 4294967295 next to the pod routes.
The agent refuses a VipGateway on a node when:
- its
destinationCIDRoverlaps a VipConfigclusterCIDRsnetwork or any IPv4 network on a node interface (address and prefix length), so0.0.0.0/0is always refused - its
routeTableis reserved (0, 253, 254 or 255) or is the route table of a VipAddress on the node - its
routingPriorityis 32766 or later, so the main table rule would come first - another VipGateway has the same
destinationCIDRandrouteTable; both are refused
A refused gateway gets no rule, route, set entry or WireGuard allowed IP on that node, and the agent logs an error that names the conflict. The check runs on each node separately.
For an egress-enabled VipAddress, the agent logs an error for each sourceCIDRs entry that overlaps a gateway destination, once while the overlap lasts and at debug level on later runs. The gateway still takes precedence for that traffic.
A gateway whose destination overlaps a VIP network is handled per node. A node that holds a VIP address of that network has the network on an interface, so it refuses the gateway like any other node network. Nodes without an address in that network apply it.
Creating or deleting a VipGateway moves egress traffic to its destination between the VIP path and the gateway path. A connection that is open during the switch keeps the source NAT that conntrack chose for its first packet, and has to reconnect.
Verification
Once configured, the controller creates VipAddress resources automatically:
kubectl get vipaddresses --all-namespaces
NAMESPACE NAME NETWORK NODE VIRTUAL IP TABLE PRIORITY AGE
default my-service-1 public node-1 203.0.113.10/24 20001 20001 5m
default my-service-2 public node-2 203.0.113.11/24 20002 20002 5m
Custom Resources Reference
All custom resources use API group netwrangler.dev/v1.
| Resource | Scope | Short Name | Description |
|---|---|---|---|
| VipConfig | Cluster | vc |
Global firewall mark, route table, and routing policy configuration |
| VipNetwork | Cluster | vn |
Network interface, gateway, and network addressing |
| VipAddress | Namespaced | va |
Individual IP assignment to a node (managed by controller) |
| VipGateway | Cluster | vg |
Gateway routing policy (destination, table, priority, pod selector) |
| VipRoute | Namespaced | vr |
Individual route to a gateway pod (managed by controller) |
VipAddress
VipAddress resources are created and managed by the controller. Every field is immutable; when a value changes, the controller deletes the VipAddress and creates it again.
| Field | Description |
|---|---|
network |
Name of the VipNetwork (required) |
nodeName |
Node the virtual IP is assigned to (required) |
virtualCIDR |
Virtual IP with its network prefix, e.g. 10.0.1.10/24 (required) |
interface |
Network interface, from the VipNetwork (required) |
gatewayIP |
Gateway address, from the VipNetwork |
sourceCIDRs |
Source networks, from the VipNetwork after overlap pruning |
firewallMark |
Firewall mark allocated by the controller |
routeTable |
Route table ID allocated by the controller |
routingPriority |
Policy rule priority allocated by the controller |
egressEnabled |
Whether egress marking is enabled (from the Service label) |
endpointIPs |
Pod IPs of the Service's ready endpoints on the node |
ports |
Service ports with protocol |
VipAddress names follow the pattern {serviceName}-{index} (e.g., my-service-1). The index is a 1-based integer allocated per service, ensuring unique names within a namespace.
Labels set by the controller:
netwrangler.dev/managed-by: netwranglernetwrangler.dev/namespace: <service-namespace>netwrangler.dev/network: <vipnetwork-name>netwrangler.dev/service: <service-name>netwrangler.dev/node-name: <node-name>
Each VipAddress has two owner references: its Service (as controller) and its VipNetwork, both with blockOwnerDeletion. Deleting either one lets Kubernetes garbage-collect the VipAddress.
The controller only manages VipAddresses and VipRoutes whose netwrangler.dev/managed-by label matches its manager name. The name is netwrangler unless the controller's NETWRANGLER_MANAGER_NAME environment variable sets another; the chart does not set it.
VipRoute
VipRoute resources are created and managed by the controller for each matching Pod selected by a VipGateway. One VipRoute is created per Pod.
| Field | Description |
|---|---|
destinationCIDR |
Destination network CIDR (from VipGateway) |
gatewayNode |
Node the gateway pod runs on |
podIP |
Pod IP address of the gateway pod |
routeTable |
Linux routing table number |
routingPriority |
Policy rule priority |
All fields are required and immutable.
VipRoute names follow the pattern {podName}-{gatewayName} (e.g., my-pod-my-gateway).
Labels set by the controller:
netwrangler.dev/managed-by: netwranglernetwrangler.dev/gateway: <gateway-name>
How It Works
Controller
The controller runs as a shell-operator hook, triggered by changes to Services, EndpointSlices, VipNetworks, VipConfigs, VipAddresses, VipGateways, VipRoutes, and Pods labelled netwrangler.dev/gateway: enabled. It also resyncs every 30 seconds.
For each managed Service, the controller:
- Takes the Service's
externalIPsand matches each IP to a VipNetwork by CIDR - Collects nodes with ready endpoints from EndpointSlices
- Claims existing VipAddresses on their current nodes
- Assigns remaining IPs to the most undersupplied nodes (proportional to endpoint count)
- Rebalances IPs across nodes to minimize quota deviation
Gateway Routing
The controller also handles gateway routing for Pods labelled with netwrangler.dev/gateway: enabled:
- For each VipGateway, selects Pods whose labels match the gateway's
podSelector.matchLabels - Filters to Running Pods that have a podIP
- Creates one VipRoute per matching Pod with the VipGateway's destination IP, the pod IP, route table, and routing priority
- Diffs desired VipRoutes against existing ones (filtered by
managed-bylabel) and emits Create/Delete operations
The agent watches all VipGateway and VipRoute resources (not filtered by node) and on every node:
- Installs, for each VipGateway it applies (see the refusal rules above), the policy rule
ip rule add to <dest> fwmark 0/<mask> table <table> priority <priority> protocol 110and the fallbackunreachable <dest> metric 4294967295in its table - For each VipRoute whose destination and table match an applied gateway, resolves the route to the pod via
ip route get <podIP>to discover the outgoing interface, next hop and source address, then installs:ip route replace <dest> via <next_hop> dev <resolved_dev> src <resolved_src> table <table> proto 110. The next hop is the kernel's gateway towards the pod, or the pod IP when the pod is directly reachable - Removes a gateway's rule and fallback when the VipGateway is deleted or refused, and a pod route when its VipRoute is deleted
- Re-resolves pod routes every 30 seconds, so routes self-heal when pods reschedule or the CNI reconfigures
Every policy rule and route the agent adds carries routing protocol 110 (ip rule ... protocol 110, ip route ... proto 110; ip rule show and ip route show print proto 110). The agent treats only rules with that tag as its own, so it never removes rules other software installs, such as WireGuard's fwmark rules or another router's to rules. Rules and routes from agent versions before the tag are replaced while they are still wanted; ones that are no longer wanted stay and have to be removed by hand (for example ip rule del table <table> and ip route flush table <table>).
WireGuard
A gateway pod on another node may be reached through a WireGuard interface. The agent treats the device ip route get <podIP> returns as WireGuard when wg show <dev> accepts it. WireGuard only sends traffic to a destination that a peer lists in its allowed IPs, so the agent adds the gateway's destinationCIDR to the allowed IPs of the peer whose allowed IPs contain the pod IP, and removes it from any other peer on that interface. Without such a peer it logs a warning and changes nothing.
The agent records the destinations it added in /tmp/shell-operator/wireguard-managed.json. When one is no longer wanted, it removes it from every peer on every WireGuard interface. Allowed IPs it did not add stay as they are.
Agent
The agent runs as a shell-operator hook on each node, triggered by changes to VipAddresses assigned to its node, VipNetworks, VipConfigs, VipGateways, and VipRoutes. It also resyncs every 30 seconds. Every run sees all five resource kinds and reconciles the node against them.
The agent reads everything a VIP needs from its VipAddress: interface, virtualCIDR, gatewayIP, sourceCIDRs and the per-IP values (firewallMark, routeTable, routingPriority). It skips a VipAddress whose VipNetwork does not exist or whose interface is not up. The VipConfig provides the firewall mark mask. For each VipAddress, the agent configures:
- Addresses — Adds the virtual IP on the interface with the correct network prefix and a
vip:label prefix. Detects and replaces addresses with mismatched prefix lengths. After adding an address, it sends three gratuitous ARP announcements (arping -U -c 3) on the interface so upstream ARP caches learn the new location at once. An address that is already in place is not announced again. - Routing — Creates a per-IP policy routing rule (
ip rule add fwmark <mark>/<mask> table <table> priority <priority> protocol 110), a connected network route for the VIP's network, and routes for each configured source network via the gateway — all with the virtual IP as preferred source. Stale routes are removed automatically when source networks change. - nftables verdict maps — Manages four verdict maps in the
netwranglertable, used from the base chainsfilter-prerouting(prerouting hook) andmangle-output(output hook, for locally generated replies). Two specific maps (type ipv4_addr . ipv4_addr : verdict; flags interval) pair a target IP with a network CIDR:ingress-marks-ipv4(mapsct original ip daddr . ct original ip saddr) andegress-marks-ipv4(mapsct original ip saddr . ct original ip daddr, only whenegressEnabledis true). Each VIP gets an entry for the network of itsvirtualCIDR, plus one for eachsourceCIDRsCIDR. A VIP withsourceCIDRs: ["0.0.0.0/0"]goes into the two default maps instead (ingress-marks-default-ipv4andegress-marks-default-ipv4,type ipv4_addr : verdict), which are tried after the specific ones. Conntrack matching ensures both directions of a connection are marked. Entriesgotothe per-IP mark chains. - Mark chains — Each VipAddress gets its own nftables chain named after its firewall mark in hex (
mark-{fwmark}, e.g.,mark-00000003) that applies the IP's allocated firewall mark usingmeta mark set meta mark & <inverted_mask> | <mark>. - Reply marking — Locally generated replies, such as ICMP echo replies from the node itself, never pass the prerouting hook. The
mangle-outputchain (type route hook output priority mangle) looks up the ingress maps withip saddr . ip daddr(default maps:ip saddr) and marks them, and itsroutetype makes the kernel route the packet again after the mark changes, so the reply leaves through the VIP's table. - SNAT — Every VipAddress with a firewall mark gets an entry in the
snat-addressesmap (type mark . ifname : ipv4_addr) that pairs its mark and interface with the virtual IP. One rule in thenat-postroutingchain (type nat hook postrouting priority 90, before the usual source NAT at 100) applies it:snat to meta mark & <mask> . oifname map @snat-addresses fully-random. A new connection whose first packet carries a VIP's mark and leaves through that VIP's interface, which in practice is marked egress traffic from the Service's endpoint pods, is source-NATted to the virtual IP.
The agent processes changes in a specific order to ensure dependencies are satisfied:
- Gateways: installs the policy rules and fallbacks of the applied VipGateways
- Removals: stale verdict map entries → mark chains → VIP policy rules and route tables → SNAT entries → addresses
- Additions: addresses → the
gateway-destinationsset → verdict map entries, mark chains and their rules → SNAT entries → thecluster-networksset → VIP policy rules and routes - Gateway routes: WireGuard allowed IPs → removal of stale gateway rules and routes → gateway pod routes
A gateway's rule exists before its destination stops being marked, and a removed destination is marked again before its rule goes. There are no separate deletion handlers: when a VipAddress, VipGateway or VipRoute is removed, the next run removes its mark chain, verdict map entries, SNAT entry, policy rules and routes in the order above.
Helm Values
Key configurable values (see values.yaml for the full reference):
| Value | Default | Description |
|---|---|---|
controller.replicaCount |
1 |
Number of controller replicas |
controller.debug |
false |
Enable debug mode |
controller.logLevel |
info |
debug, info, warn, error |
controller.image.repository |
jochen/netwrangler-controller |
Controller image |
controller.image.tag |
Chart appVersion | Image tag |
controller.image.pullPolicy |
IfNotPresent |
Image pull policy |
controller.resources.requests.cpu |
10m |
CPU request |
controller.resources.requests.memory |
64Mi |
Memory request |
controller.resources.limits.cpu |
100m |
CPU limit |
controller.resources.limits.memory |
128Mi |
Memory limit |
controller.nodeSelector |
Linux control-plane nodes | Nodes the controller may run on |
agent.enabled |
true |
Deploy the agent DaemonSet |
agent.debug |
false |
Enable debug mode |
agent.retry.count |
3 |
Attempts per external tool call (at least 1) |
agent.retry.delaySeconds |
2 |
Seconds between attempts (at least 0) |
agent.image.repository |
jochen/netwrangler-agent |
Agent image |
agent.image.tag |
Chart appVersion | Image tag |
agent.image.pullPolicy |
IfNotPresent |
Image pull policy |
agent.resources.requests.cpu |
10m |
CPU request |
agent.resources.requests.memory |
64Mi |
Memory request |
agent.resources.limits.cpu |
100m |
CPU limit |
agent.resources.limits.memory |
128Mi |
Memory limit |
agent.serviceAccount.create |
true |
Create the agent's account |
agent.serviceAccount.name |
"" |
Override the agent's account |
agent.serviceAccount.automount |
true |
Mount the agent's account token |
agent.serviceAccount.annotations |
{} |
Annotations on the agent's account |
serviceAccount.create |
true |
Create the controller's account |
serviceAccount.name |
"" |
Override the controller's account |
serviceAccount.automount |
true |
Mount the controller's account token |
serviceAccount.annotations |
{} |
Annotations on the controller's account |
imagePullSecrets |
[] |
Pull secrets for both images |
podAnnotations |
{} |
Annotations on the controller and agent pods |
podLabels |
{} |
Labels on the controller and agent pods |
The agent uses its own ServiceAccount, whose ClusterRole can only read the netwrangler resources; write access stays with the controller's account. The agent runs with NET_ADMIN and NET_RAW capabilities and hostNetwork: true. Both the agent and the controller run as non-root (runAsUser: 1000).
controller.nodeSelector defaults to kubernetes.io/os: linux and node-role.kubernetes.io/control-plane: "". Helm merges an override into these maps instead of replacing them, so to run the controller on worker nodes, set the control-plane key to null. The default tolerations let the controller run on tainted control-plane nodes.
agent.retry.count and agent.retry.delaySeconds become the agent's NETWRANGLER_RETRY_COUNT and NETWRANGLER_RETRY_DELAY environment variables. When an agent hook fails, for example because a call to nft, ip, wg or arping still fails after the last attempt, the agent stops its container by signalling the process that supervises it. The kubelet then restarts the pod, so the failure shows as a restart instead of a hook that keeps running with its changes half applied.
Development
Prerequisites
- Crystal >= 1.21.0, < 1.22
- Ruby (for Rake build system and integration tests)
- Bundler (
gem install bundler, thenbundle install) - Docker with buildx (for container image builds)
- kind (for local Kubernetes cluster)
- Helm and Helmfile (for deployment)
Building
# Install Crystal dependencies
rake deps
# Build all targets for the preferred debug profile of this host
rake debug
# Build every profile this host supports, in both modes
rake build
# Build one specific profile and mode
rake build:debug:darwin-arm64-system-dynamic
# List the build profiles available on this host
rake profiles
Build profiles are configured per OS in .project.yaml under crystal.profiles, named <ostype>-<arch>-<libc>-<linkage>. Only the profiles matching the current host are offered as tasks.
Testing
Unit tests (Crystal specs):
rake spec # All unit tests
rake controller:spec # Controller unit tests only
rake agent:spec # Agent unit tests only
rake util:spec # Shared utility unit tests only
Every spec and test task takes the same two optional arguments, files and pattern — quote the whole argument in zsh, or the brackets glob:
# Compile only these spec paths (space-separated for more than one)
rake 'spec[spec/util]'
rake 'spec[spec/util spec/agent]'
# Run only examples whose full name contains the pattern
rake 'spec[,IPv4Network]'
# Both: compile only spec/util, run only its IPv4Network examples
rake 'spec[spec/util,IPv4Network]'
# Same arguments on a component task, confined to its own spec directory
rake 'agent:spec[spec/agent/nftables_updater_spec.cr]'
files narrows what compiles, pattern narrows what runs.
util is a virtual component: it is declared under crystal.components in .project.yaml with virtual: true, so it takes part in the unit-test and coverage tasks without a shards target or a binary of its own.
Integration tests (Ruby/RSpec against a kind cluster):
rake test # Build, deploy, and run all integration tests
rake controller:test # Controller integration tests only
rake agent:test # Agent integration tests only
The files and pattern arguments work here too. files additionally accepts rspec's single-example addressing, <file>:<line> and <file>[1:2]:
rake 'test[test/agent/agent_spec.rb]'
rake 'test[test/agent/agent_spec.rb:42]'
rake 'test[,announces the VIP]'
rake test always delegates to the component tasks, splitting files by the component that owns each path, so the build-and-deploy steps hooked onto <component>:test still run — narrowing the selection never tests a stale deployment. A path under test/ that no component owns (test/support/…) is an error rather than a silent no-op.
Integration tests require a running kind cluster (see below).
Local Development with kind
# Create a 3-node kind cluster
rake kind:start
# Build images, load into kind, and deploy via Helmfile
rake kind:deploy
# Rebuild and restart after code changes
rake kind:restart
# Delete the cluster
rake kind:delete
Rake Tasks
| Task | Description |
|---|---|
rake setup |
Set up the project (install dependencies) |
rake deps |
Install Crystal dependencies |
rake debug |
Build the preferred debug profile for this host |
rake build |
Build every host profile, debug and release |
rake build:debug, rake build:release |
Build every debug or release profile for this host |
rake profiles |
List the build profiles available on this host |
rake dist:package |
Package every built release profile into dist/ |
rake lint |
Run every linter |
rake lint:crystal |
Run Ameba over the Crystal sources |
rake spec |
Run all unit tests |
rake cover |
Run all unit tests with coverage |
rake test |
Run all integration tests |
rake <component>:spec |
Run one component's unit tests (controller, agent, util) |
rake <component>:cover |
Run one component's unit tests with coverage |
rake <component>:test |
Build, deploy, and run one component's integration tests |
rake <component>:run |
Run the controller or agent locally in debug mode |
rake docker:build |
Build every image for all platforms (build cache only) |
rake docker:load |
Build every image for this host and load it into the daemon |
rake docker:push |
Build and push every multi-arch image |
rake <component>:docker:build, :load, :push |
The same for one image |
rake <component>:docker:run |
Run one image in a container |
rake kind:start |
Create the 3-node kind cluster |
rake kind:deploy |
Deploy to the kind cluster |
rake kind:redeploy |
Deploy and restart every workload so reloaded images take effect |
rake kind:restart |
Redeploy and wait for every rollout |
rake kind:sync |
Load every image and sync the release to kind |
rake kind:upload |
Load every image into kind |
rake <component>:kind:upload |
Load one image into kind |
rake kind:kubeconfig |
Export the kind cluster's kubeconfig |
rake kind:crds |
Apply the CRDs to the kind cluster |
rake kind:delete |
Delete the kind cluster |
rake helm:package |
Package the Helm chart into dist/ |
rake helm:push |
Push the Helm chart to the OCI registry |
rake helm:push:metadata |
Push the Artifact Hub metadata to the OCI registry |
rake helm:netwrangler:package, :push, :push:metadata |
The same for the netwrangler chart by name |
rake version:lint |
Report manifests whose version differs from the project's |
rake version:update |
Write the project version into every manifest |
rake changelog |
Update CHANGELOG.md for the manifest's version |
rake release[yes] |
Cut a release: branch from main, tag, bump dev versions |
rake notary:setup |
Store the notarytool keychain profile |
rake clean, rake clobber |
Remove temporary or all generated files |
Project Structure
src/
controller/ # Controller: allocation logic, shell-operator hook
agent/ # Agent: address, routes, and nftables updaters
model/ # Data models for Kubernetes resources
util/ # Shared utilities (binding context, commands)
spec/ # Crystal unit tests
test/ # Ruby integration tests (RSpec)
deploy/
charts/ # Helm chart with CRDs and templates
kind-config.yaml # kind cluster configuration
helmfile.yaml.gotmpl # Helmfile for multi-environment deployment
License
Copyright (C) 2026 Jochen Seeber
netwrangler is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version (AGPL-3.0-or-later). See LICENSE.txt for the full text.
netwrangler
- 0
- 0
- 0
- 0
- 1
- about 6 hours ago
- October 21, 2026
GNU Affero General Public License v3.0
Wed, 21 Oct 2026 07:35:11 GMT