From e38ec826511a8c18005768187c95fc62fb5e336a Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:12:43 +0100 Subject: [PATCH 01/28] add demo environment set up --- .gitignore | 3 + docs/examples/set-up/README.md | 11 ++ docs/examples/set-up/cluster-cfg.yaml | 14 +++ docs/examples/set-up/create-kind-clusters.sh | 48 +++++++ docs/examples/set-up/kustomization.yaml | 18 +++ .../set-up/netbox-l2advertisement.yaml | 25 ++++ docs/examples/set-up/netbox-svc.yaml | 26 ++++ docs/examples/set-up/prepare-demo-env.sh | 21 ++++ kind/deploy-netbox.sh | 118 ++++++++++++++++++ kind/local-env.sh | 106 +--------------- 10 files changed, 285 insertions(+), 105 deletions(-) create mode 100644 docs/examples/set-up/README.md create mode 100644 docs/examples/set-up/cluster-cfg.yaml create mode 100755 docs/examples/set-up/create-kind-clusters.sh create mode 100644 docs/examples/set-up/kustomization.yaml create mode 100644 docs/examples/set-up/netbox-l2advertisement.yaml create mode 100644 docs/examples/set-up/netbox-svc.yaml create mode 100755 docs/examples/set-up/prepare-demo-env.sh create mode 100755 kind/deploy-netbox.sh diff --git a/.gitignore b/.gitignore index 45297fe5..36661b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ go.work *~ .drawio.dtmp .DS_Store + +# Temporary files +tmp \ No newline at end of file diff --git a/docs/examples/set-up/README.md b/docs/examples/set-up/README.md new file mode 100644 index 00000000..6be7d1a3 --- /dev/null +++ b/docs/examples/set-up/README.md @@ -0,0 +1,11 @@ +# NetBox Operator Examples + +This folder shows some examples how the NetBox Operator can be used. The demo environment can be prepared with the 'docs/examples/set-up/prepare-demo-env.sh' script, which creates two kind clusters with NetBox Operator installed. One one of the clusters a NetBox instance is installed which is available to both NetBox Operator deployments. + +Prerequisites: +- go version v1.24.0+ +- docker image netbox-operatore:build-local +- kustomize version v5.5.0+ +- kubectl version v1.32.2+ +- kind v0.27.0 +- docker cli diff --git a/docs/examples/set-up/cluster-cfg.yaml b/docs/examples/set-up/cluster-cfg.yaml new file mode 100644 index 00000000..5a4429ce --- /dev/null +++ b/docs/examples/set-up/cluster-cfg.yaml @@ -0,0 +1,14 @@ +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +networking: + serviceSubnet: "10.96.0.0/20" # until 10.96.15.255 + apiServerAddress: "127.0.0.1" + apiServerPort: 6443 +nodes: +- role: control-plane + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" \ No newline at end of file diff --git a/docs/examples/set-up/create-kind-clusters.sh b/docs/examples/set-up/create-kind-clusters.sh new file mode 100755 index 00000000..481e3789 --- /dev/null +++ b/docs/examples/set-up/create-kind-clusters.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# This script creates the specified number of kind clusters with MetalLB +set -e +# Define colors +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Check if cluster names are provided +if [ $# -eq 0 ]; then + echo -e "${RED}Error: No cluster names provided${NC}" + echo "Usage: $0 ... " + exit 1 +fi + +number_of_clusters=$1 +clustername=${2:-dns} # Set default cluster name to 'dns' if not provided +mkdir -p tmp +i=0 + +# Loop to create the specified number of clusters +for clustername in "$@"; do + config_file="docs/examples/set-up/cluster-cfg.yaml" + temp_config="tmp/cluster-$clustername-cfg.yaml" + i=$((i + 1)) + + if [ -f "$config_file" ]; then + # Make a temporary copy of the configuration file + cp "$config_file" "$temp_config" + + # Modify apiServerPort in the copied config file + sed -i'' -e "s/apiServerPort: 6443/apiServerPort: $((6443 + i))/g" "$temp_config" + rm "$temp_config"-e + + # check if cluster exists + if kind get clusters | grep -q "^${clustername}$"; then + echo "Cluster ${clustername} already exists. Skipping creation." + continue + fi + kind create cluster --name $clustername --config $temp_config || { echo -e "${RED}Error: Failed to create cluster ${clustername}${NC}"; rm -f "$temp_config"; exit 1; } + + # install MetalLB + kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml + else + echo -e "${RED}Error: Configuration file $config_file not found${NC}" + exit 1 + fi +done diff --git a/docs/examples/set-up/kustomization.yaml b/docs/examples/set-up/kustomization.yaml new file mode 100644 index 00000000..fe91fc3c --- /dev/null +++ b/docs/examples/set-up/kustomization.yaml @@ -0,0 +1,18 @@ +resources: + - ../../../kind + +patches: + - patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: controller-manager + namespace: system + spec: + template: + spec: + containers: + - name: manager + env: + - name: NETBOX_HOST + value: "172.18.1.2" diff --git a/docs/examples/set-up/netbox-l2advertisement.yaml b/docs/examples/set-up/netbox-l2advertisement.yaml new file mode 100644 index 00000000..d04339de --- /dev/null +++ b/docs/examples/set-up/netbox-l2advertisement.yaml @@ -0,0 +1,25 @@ +--- +apiVersion: metallb.io/v1beta1 +kind: L2Advertisement +metadata: + namespace: metallb-system + name: output-l2-advertisement +spec: +--- +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: db-ipaddresspool + namespace: metallb-system +spec: + addresses: + - 172.18.1.1/32 +--- +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: netbox-ipaddresspool + namespace: metallb-system +spec: + addresses: + - 172.18.1.2/32 diff --git a/docs/examples/set-up/netbox-svc.yaml b/docs/examples/set-up/netbox-svc.yaml new file mode 100644 index 00000000..ddba7fc5 --- /dev/null +++ b/docs/examples/set-up/netbox-svc.yaml @@ -0,0 +1,26 @@ + +apiVersion: v1 +kind: Service +metadata: + name: netbox + namespace: default + annotations: + metallb.universe.tf/address-pool: netbox-ipaddresspool +spec: + allocateLoadBalancerNodePorts: true + externalTrafficPolicy: Cluster + type: LoadBalancer + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/component: netbox + app.kubernetes.io/instance: netbox + app.kubernetes.io/name: netbox + sessionAffinity: None \ No newline at end of file diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/set-up/prepare-demo-env.sh new file mode 100755 index 00000000..fc547179 --- /dev/null +++ b/docs/examples/set-up/prepare-demo-env.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e +# create the kind clusters netbox, prod and dev +./docs/examples/set-up/create-kind-clusters.sh prod dev + +# install netbox in the netbox cluster and load demo data +kubectl config use-context kind-prod +./kind/deploy-netbox.sh prod "4.1.8" default +kubectl apply -f docs/examples/set-up/netbox-svc.yaml +kubectl apply -f docs/examples/set-up/netbox-l2advertisement.yaml + + +# install NetBox Operator +kubectl config use-context kind-prod +kind load docker-image netbox-operator:build-local --name prod +kustomize build docs/examples/set-up/ | kubectl apply -f - + + +kubectl config use-context kind-dev +kind load docker-image netbox-operator:build-local --name dev +kustomize build docs/examples/set-up/ | kubectl apply -f - diff --git a/kind/deploy-netbox.sh b/kind/deploy-netbox.sh new file mode 100755 index 00000000..e2ef7423 --- /dev/null +++ b/kind/deploy-netbox.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -e -u -o pipefail + +NETBOX_HELM_CHART="https://github.com/netbox-community/netbox-chart/releases/download/netbox-5.0.0-beta.169/netbox-5.0.0-beta.169.tgz" # default value + +if [[ $# -ne 3 ]]; then + echo "Usage: $0 " + exit 1 +fi + +CLUSTER=$1 +VERSION=$2 +NAMESPACE=$3 + +# load remote images +if [[ "${VERSION}" == "3.7.8" ]] ;then + echo "Using version ${VERSION}" + # need to align with netbox-chart otherwise the creation of the cluster will hang + declare -a Remote_Images=( \ + "busybox:1.36.1" \ + "docker.io/bitnami/redis:7.2.4-debian-12-r9" \ + "docker.io/netboxcommunity/netbox:v3.7.8" \ + "ghcr.io/zalando/postgres-operator:v1.12.2" \ + "ghcr.io/zalando/spilo-16:3.2-p3" \ + ) + NETBOX_HELM_CHART="https://github.com/netbox-community/netbox-chart/releases/download/netbox-5.0.0-beta5/netbox-5.0.0-beta5.tgz" + + # patch load-data.sh + sed 's/netbox-demo-v4.1.sql/netbox-demo-v3.7.sql/g' $(dirname "$0")/load-data-job/load-data.orig.sh > $(dirname "$0")/load-data-job/load-data.sh && chmod +x $(dirname "$0")/load-data-job/load-data.sh + + # patch dockerfile (See README at https://github.com/netbox-community/pynetbox for the supported version matrix) + sed 's/RUN pip install -Iv pynetbox==7.4.1/RUN pip install -Iv pynetbox==7.3.4/g' $(dirname "$0")/load-data-job/dockerfile.orig > $(dirname "$0")/load-data-job/dockerfile +elif [[ "${VERSION}" == "4.0.11" ]] ;then + echo "Using version ${VERSION}" + # need to align with netbox-chart otherwise the creation of the cluster will hang + declare -a Remote_Images=( \ + "busybox:1.36.1" \ + "docker.io/bitnami/redis:7.4.0-debian-12-r2" \ + "ghcr.io/netbox-community/netbox:v4.0.11" \ + "ghcr.io/zalando/postgres-operator:v1.12.2" \ + "ghcr.io/zalando/spilo-16:3.2-p3" \ + ) + NETBOX_HELM_CHART="https://github.com/netbox-community/netbox-chart/releases/download/netbox-5.0.0-beta.84/netbox-5.0.0-beta.84.tgz" + + # patch load-data.sh + sed 's/netbox-demo-v4.1.sql/netbox-demo-v4.0.sql/g' $(dirname "$0")/load-data-job/load-data.orig.sh > $(dirname "$0")/load-data-job/load-data.sh && chmod +x $(dirname "$0")/load-data-job/load-data.sh + + cp $(dirname "$0")/load-data-job/dockerfile.orig $(dirname "$0")/load-data-job/dockerfile +elif [[ "${VERSION}" == "4.1.8" ]] ;then + echo "Using version ${VERSION}" + # need to align with netbox-chart otherwise the creation of the cluster will hang + declare -a Remote_Images=( \ + "busybox:1.37.0" \ + "docker.io/bitnami/redis:7.4.1-debian-12-r2" \ + "ghcr.io/netbox-community/netbox:v4.1.8" \ + "ghcr.io/zalando/postgres-operator:v1.12.2" \ + "ghcr.io/zalando/spilo-16:3.2-p3" \ + ) + + # create load-data.sh + cp $(dirname "$0")/load-data-job/load-data.orig.sh $(dirname "$0")/load-data-job/load-data.sh + + cp $(dirname "$0")/load-data-job/dockerfile.orig $(dirname "$0")/load-data-job/dockerfile +else + echo "Unknown version ${VERSION}" + exit 1 +fi + +for img in "${Remote_Images[@]}"; do + docker pull "$img" + kind load docker-image "$img" --name "${CLUSTER}" +done + +# build image for loading local data via NetBox API +cd ./kind/load-data-job && docker build -t netbox-load-local-data:1.0 --load --no-cache --progress=plain -f ./dockerfile . && cd - + +# load local images +declare -a Local_Images=( \ +"netbox-load-local-data:1.0" \ +) +for img in "${Local_Images[@]}"; do + kind load docker-image "$img" --name "${CLUSTER}" +done + +# install helm charts +helm upgrade --install --namespace="${NAMESPACE}" postgres-operator \ +https://opensource.zalando.com/postgres-operator/charts/postgres-operator/postgres-operator-1.12.2.tgz + +kubectl apply --namespace="${NAMESPACE}" -f "$(dirname "$0")/netbox-db.yaml" +kubectl wait --namespace="${NAMESPACE}" --timeout=600s --for=jsonpath='{.status.PostgresClusterStatus}'=Running postgresql/netbox-db + +kubectl create configmap --namespace="${NAMESPACE}" netbox-demo-data-load-job-scripts --from-file="$(dirname "$0")/load-data-job" -o yaml --dry-run=client | kubectl apply -f - +kubectl apply --namespace="${NAMESPACE}" -f "$(dirname "$0")/load-data-job.yaml" +kubectl wait --namespace="${NAMESPACE}" --timeout=600s --for=condition=complete job/netbox-demo-data-load-job +kubectl delete configmap --namespace="${NAMESPACE}" netbox-demo-data-load-job-scripts + +helm upgrade --install --namespace="${NAMESPACE}" netbox \ + --set postgresql.enabled="false" \ + --set externalDatabase.host="netbox-db.${NAMESPACE}.svc.cluster.local" \ + --set externalDatabase.existingSecretName="netbox.netbox-db.credentials.postgresql.acid.zalan.do" \ + --set externalDatabase.existingSecretKey="password" \ + --set redis.auth.password="password" \ + --set resources.requests.cpu="500m" \ + --set resources.requests.memory="512Mi" \ + --set resources.limits.cpu="2000m" \ + --set resources.limits.memory="2Gi" \ + ${NETBOX_HELM_CHART} + +kubectl rollout status --namespace="${NAMESPACE}" deployment netbox + +# load local data +kubectl create job netbox-load-local-data --image=netbox-load-local-data:1.0 +kubectl wait --namespace="${NAMESPACE}" --timeout=600s --for=condition=complete job/netbox-load-local-data +docker rmi netbox-load-local-data:1.0 + +# clean up +rm $(dirname "$0")/load-data-job/load-data.sh +rm $(dirname "$0")/load-data-job/dockerfile diff --git a/kind/local-env.sh b/kind/local-env.sh index c3c2f932..795b53f7 100755 --- a/kind/local-env.sh +++ b/kind/local-env.sh @@ -3,7 +3,6 @@ set -e -u -o pipefail NAMESPACE="" VERSION="4.1.8" # default value -NETBOX_HELM_CHART="https://github.com/netbox-community/netbox-chart/releases/download/netbox-5.0.0-beta.169/netbox-5.0.0-beta.169.tgz" # default value while [[ $# -gt 0 ]]; do case $1 in -n|--namespace) @@ -40,107 +39,4 @@ fi kind create cluster || echo "cluster already exists, continuing..." kubectl wait --for=jsonpath='{.status.phase}'=Active --timeout=1s namespace/${NAMESPACE} -# load remote images -if [[ "${VERSION}" == "3.7.8" ]] ;then - echo "Using version ${VERSION}" - # need to align with netbox-chart otherwise the creation of the cluster will hang - declare -a Remote_Images=( \ - "busybox:1.36.1" \ - "docker.io/bitnami/redis:7.2.4-debian-12-r9" \ - "docker.io/netboxcommunity/netbox:v3.7.8" \ - "ghcr.io/zalando/postgres-operator:v1.12.2" \ - "ghcr.io/zalando/spilo-16:3.2-p3" \ - ) - NETBOX_HELM_CHART="https://github.com/netbox-community/netbox-chart/releases/download/netbox-5.0.0-beta5/netbox-5.0.0-beta5.tgz" - - # patch load-data.sh - sed 's/netbox-demo-v4.1.sql/netbox-demo-v3.7.sql/g' $(dirname "$0")/load-data-job/load-data.orig.sh > $(dirname "$0")/load-data-job/load-data.sh && chmod +x $(dirname "$0")/load-data-job/load-data.sh - - # patch dockerfile (See README at https://github.com/netbox-community/pynetbox for the supported version matrix) - sed 's/RUN pip install -Iv pynetbox==7.4.1/RUN pip install -Iv pynetbox==7.3.4/g' $(dirname "$0")/load-data-job/dockerfile.orig > $(dirname "$0")/load-data-job/dockerfile -elif [[ "${VERSION}" == "4.0.11" ]] ;then - echo "Using version ${VERSION}" - # need to align with netbox-chart otherwise the creation of the cluster will hang - declare -a Remote_Images=( \ - "busybox:1.36.1" \ - "docker.io/bitnami/redis:7.4.0-debian-12-r2" \ - "ghcr.io/netbox-community/netbox:v4.0.11" \ - "ghcr.io/zalando/postgres-operator:v1.12.2" \ - "ghcr.io/zalando/spilo-16:3.2-p3" \ - ) - NETBOX_HELM_CHART="https://github.com/netbox-community/netbox-chart/releases/download/netbox-5.0.0-beta.84/netbox-5.0.0-beta.84.tgz" - - # patch load-data.sh - sed 's/netbox-demo-v4.1.sql/netbox-demo-v4.0.sql/g' $(dirname "$0")/load-data-job/load-data.orig.sh > $(dirname "$0")/load-data-job/load-data.sh && chmod +x $(dirname "$0")/load-data-job/load-data.sh - - cp $(dirname "$0")/load-data-job/dockerfile.orig $(dirname "$0")/load-data-job/dockerfile -elif [[ "${VERSION}" == "4.1.8" ]] ;then - echo "Using version ${VERSION}" - # need to align with netbox-chart otherwise the creation of the cluster will hang - declare -a Remote_Images=( \ - "busybox:1.37.0" \ - "docker.io/bitnami/redis:7.4.1-debian-12-r2" \ - "ghcr.io/netbox-community/netbox:v4.1.8" \ - "ghcr.io/zalando/postgres-operator:v1.12.2" \ - "ghcr.io/zalando/spilo-16:3.2-p3" \ - ) - - # create load-data.sh - cp $(dirname "$0")/load-data-job/load-data.orig.sh $(dirname "$0")/load-data-job/load-data.sh - - cp $(dirname "$0")/load-data-job/dockerfile.orig $(dirname "$0")/load-data-job/dockerfile -else - echo "Unknown version ${VERSION}" - exit 1 -fi - -for img in "${Remote_Images[@]}"; do - docker pull "$img" - kind load docker-image "$img" -done - -# build image for loading local data via NetBox API -cd ./kind/load-data-job && docker build -t netbox-load-local-data:1.0 --load --no-cache --progress=plain -f ./dockerfile . && cd - - -# load local images -declare -a Local_Images=( \ -"netbox-load-local-data:1.0" \ -) -for img in "${Local_Images[@]}"; do - kind load docker-image "$img" -done - -# install helm charts -helm upgrade --install --namespace="${NAMESPACE}" postgres-operator \ -https://opensource.zalando.com/postgres-operator/charts/postgres-operator/postgres-operator-1.12.2.tgz - -kubectl apply --namespace="${NAMESPACE}" -f "$(dirname "$0")/netbox-db.yaml" -kubectl wait --namespace="${NAMESPACE}" --timeout=600s --for=jsonpath='{.status.PostgresClusterStatus}'=Running postgresql/netbox-db - -kubectl create configmap --namespace="${NAMESPACE}" netbox-demo-data-load-job-scripts --from-file="$(dirname "$0")/load-data-job" -o yaml --dry-run=client | kubectl apply -f - -kubectl apply --namespace="${NAMESPACE}" -f "$(dirname "$0")/load-data-job.yaml" -kubectl wait --namespace="${NAMESPACE}" --timeout=600s --for=condition=complete job/netbox-demo-data-load-job -kubectl delete configmap --namespace="${NAMESPACE}" netbox-demo-data-load-job-scripts - -helm upgrade --install --namespace="${NAMESPACE}" netbox \ - --set postgresql.enabled="false" \ - --set externalDatabase.host="netbox-db.${NAMESPACE}.svc.cluster.local" \ - --set externalDatabase.existingSecretName="netbox.netbox-db.credentials.postgresql.acid.zalan.do" \ - --set externalDatabase.existingSecretKey="password" \ - --set redis.auth.password="password" \ - --set resources.requests.cpu="500m" \ - --set resources.requests.memory="512Mi" \ - --set resources.limits.cpu="2000m" \ - --set resources.limits.memory="2Gi" \ - ${NETBOX_HELM_CHART} - -kubectl rollout status --namespace="${NAMESPACE}" deployment netbox - -# load local data -kubectl create job netbox-load-local-data --image=netbox-load-local-data:1.0 -kubectl wait --namespace="${NAMESPACE}" --timeout=600s --for=condition=complete job/netbox-load-local-data -docker rmi netbox-load-local-data:1.0 - -# clean up -rm $(dirname "$0")/load-data-job/load-data.sh -rm $(dirname "$0")/load-data-job/dockerfile +./kind/deploy-netbox.sh kind $VERSION $NAMESPACE \ No newline at end of file From 26b08fd0d7c8d97aafebf235f88dce47145008e6 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Thu, 6 Mar 2025 14:47:04 +0100 Subject: [PATCH 02/28] add examples --- docs/examples/README.md | 54 +++++++++++++++++++ .../example1/netbox_v1_prefixclaim.yaml | 15 ++++++ docs/examples/example2/kustomization.yaml | 16 ++++++ .../podinfo-podinfo-ipaddresspool.yaml | 13 +++++ docs/examples/example3/kustomization.yaml | 14 +++++ .../example3/podinfo-int-ipaddresspool.yaml | 17 ++++++ .../example4/instance1/kustomization.yaml | 14 +++++ .../instance1/podinfo-int-ipaddresspool.yaml | 17 ++++++ .../example4/instance2/kustomization.yaml | 14 +++++ .../instance2/podinfo-int-ipaddresspool.yaml | 17 ++++++ docs/examples/set-up/README.md | 11 ---- docs/examples/set-up/create-kind-clusters.sh | 6 +++ ...ddress-pool-from-netbox-parent-prefix.yaml | 45 ++++++++++++++++ .../metallb-ip-address-pool-from-netbox.yaml | 49 +++++++++++++++++ docs/examples/set-up/prepare-demo-env.sh | 6 +++ 15 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 docs/examples/README.md create mode 100644 docs/examples/example1/netbox_v1_prefixclaim.yaml create mode 100644 docs/examples/example2/kustomization.yaml create mode 100644 docs/examples/example2/podinfo-podinfo-ipaddresspool.yaml create mode 100644 docs/examples/example3/kustomization.yaml create mode 100644 docs/examples/example3/podinfo-int-ipaddresspool.yaml create mode 100644 docs/examples/example4/instance1/kustomization.yaml create mode 100644 docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml create mode 100644 docs/examples/example4/instance2/kustomization.yaml create mode 100644 docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml delete mode 100644 docs/examples/set-up/README.md create mode 100644 docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml create mode 100644 docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 00000000..fb4c2014 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,54 @@ +# NetBox Operator Examples + +This folder shows some examples how the NetBox Operator can be used. The demo environment can be prepared with the 'docs/examples/set-up/prepare-demo-env.sh' script, which creates two kind clusters with NetBox Operator and [kro] installed. One one of the clusters a NetBox instance is installed which is available to both NetBox Operator deployments. + +[kro]: https://github.com/kro-run/kro/ + +Prerequisites: +- go version v1.24.0+ +- docker image netbox-operatore:build-local +- kustomize version v5.5.0+ +- kubectl version v1.32.2+ +- kind v0.27.0 +- docker cli + +The following chapters show some examples which depend on each other. + +# 0. Manually Create a Prefix in NetBox + +Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. + +1. Point your kubeconfig to the kind cluster 'kind-prod' `kubectl config use-context kind-prod` +2. Port-forward NetBox: `kubectl port-forward deploy/netbox 8080:8080 -n default` +3. Open in your favorite browser and log in with the username `admin` and password `admin` +4. Create a new prefix '3.0.4.8/29' with custom fields `environment: "Production", poolName: "Pool 1"`,tenant `MY_TENANT` and site `DM-Akron` + +# 1. Claim a Prefix + +1. Point your kubeconfig to the kind cluster 'kind-dev' `kubectl config use-context kind-dev` +2. Apply the manifest defining the prefix claim `kubectl apply -f docs/examples/example1/netbox_v1_prefixclaim.yaml` +3. Check that the prefix claim CR got a prefix addigned `kubectl get pxc` + +# 2. Claim a Prefix for a Podinfo Deployment and Create a MetalLB IPAddressPool + +This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. + +1. create the namespace where podinfo should be deployed `kubectl create ns test` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example2 -n test` +3. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n test` + +# 3. Dynamically Claim a Prefix with a Parent Prefix Selector for a Podinfo Deployment and Create a MetalLB IPAddressPool + +1. create the namespace where podinfo should be deployed `kubectl create ns int` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example3 -n int` +3. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n int` + +# 4. Mutli cluster set up with source of truth in netbox + +1. Point your kubeconfig to the kind cluster 'kind-dev' `kubectl config use-context kind-prod` +2. create the namespace where podinfo should be deployed `kubectl create ns instance1` +3. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example4/instance1 -n instance1` +4. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n instance1` +5. create the namespace where podinfo should be deployed `kubectl create ns instance2` +6. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example4/instance2 -n instance2` +7. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n instance2` \ No newline at end of file diff --git a/docs/examples/example1/netbox_v1_prefixclaim.yaml b/docs/examples/example1/netbox_v1_prefixclaim.yaml new file mode 100644 index 00000000..d60e1953 --- /dev/null +++ b/docs/examples/example1/netbox_v1_prefixclaim.yaml @@ -0,0 +1,15 @@ +apiVersion: netbox.dev/v1 +kind: PrefixClaim +metadata: + labels: + app.kubernetes.io/name: netbox-operator + app.kubernetes.io/managed-by: kustomize + name: prefixclaim-sample +spec: + tenant: "MY_TENANT" + site: "DM-Akron" + description: "some description" + comments: "your comments" + preserveInNetbox: true + parentPrefix: 3.0.4.8/29 + prefixLength: "/32" diff --git a/docs/examples/example2/kustomization.yaml b/docs/examples/example2/kustomization.yaml new file mode 100644 index 00000000..bb88a271 --- /dev/null +++ b/docs/examples/example2/kustomization.yaml @@ -0,0 +1,16 @@ +metadata: + namespace: test +resources: + - ../../../../podinfo/kustomize + - podinfo-podinfo-ipaddresspool.yaml + +patches: + - patch: |- + apiVersion: v1 + kind: Service + metadata: + name: podinfo + annotations: + metallb.universe.tf/allow-shared-ip: podinfo-test + spec: + type: LoadBalancer diff --git a/docs/examples/example2/podinfo-podinfo-ipaddresspool.yaml b/docs/examples/example2/podinfo-podinfo-ipaddresspool.yaml new file mode 100644 index 00000000..b387621d --- /dev/null +++ b/docs/examples/example2/podinfo-podinfo-ipaddresspool.yaml @@ -0,0 +1,13 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolFromNetBoxParentPrefix +metadata: + name: podinfo-test +spec: + name: podinfo-test + tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value + site: "DM-Akron" # Use the `name` value instead of the `slug` value + description: "podinfo" + comments: "podinfo in podinfo namespace" + preserveInNetbox: true + prefixLength: "/32" + parentPrefix: 3.0.4.8/29 \ No newline at end of file diff --git a/docs/examples/example3/kustomization.yaml b/docs/examples/example3/kustomization.yaml new file mode 100644 index 00000000..84670460 --- /dev/null +++ b/docs/examples/example3/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - ../../../../podinfo/kustomize + - podinfo-int-ipaddresspool.yaml + +patches: + - patch: |- + apiVersion: v1 + kind: Service + metadata: + name: podinfo + annotations: + metallb.universe.tf/allow-shared-ip: podinfo-int + spec: + type: LoadBalancer diff --git a/docs/examples/example3/podinfo-int-ipaddresspool.yaml b/docs/examples/example3/podinfo-int-ipaddresspool.yaml new file mode 100644 index 00000000..ddb8dd31 --- /dev/null +++ b/docs/examples/example3/podinfo-int-ipaddresspool.yaml @@ -0,0 +1,17 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector +metadata: + name: podinfo-int +spec: + name: podinfo-int + tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value + site: "DM-Akron" # Use the `name` value instead of the `slug` value + description: "int" + comments: "podinfo in pss namespace" + preserveInNetbox: true + prefixLength: "/32" + parentPrefixSelector: + tenant: MY_TENANT + site: DM-Akron + environment: "Production" + poolName: "Pool 1" diff --git a/docs/examples/example4/instance1/kustomization.yaml b/docs/examples/example4/instance1/kustomization.yaml new file mode 100644 index 00000000..7ce6dd7f --- /dev/null +++ b/docs/examples/example4/instance1/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - ../../../../../podinfo/kustomize + - podinfo-int-ipaddresspool.yaml + +patches: + - patch: |- + apiVersion: v1 + kind: Service + metadata: + name: podinfo + annotations: + metallb.universe.tf/allow-shared-ip: podinfo1 + spec: + type: LoadBalancer diff --git a/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml b/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml new file mode 100644 index 00000000..96a181d3 --- /dev/null +++ b/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml @@ -0,0 +1,17 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector +metadata: + name: podinfo1 +spec: + name: podinfo1 + tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value + site: "DM-Akron" # Use the `name` value instead of the `slug` value + description: "instance1" + comments: "podinfo in pss namespace" + preserveInNetbox: true + prefixLength: "/32" + parentPrefixSelector: + tenant: MY_TENANT + site: DM-Akron + environment: "Production" + poolName: "Pool 1" diff --git a/docs/examples/example4/instance2/kustomization.yaml b/docs/examples/example4/instance2/kustomization.yaml new file mode 100644 index 00000000..f71509bd --- /dev/null +++ b/docs/examples/example4/instance2/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - ../../../../../podinfo/kustomize + - podinfo-int-ipaddresspool.yaml + +patches: + - patch: |- + apiVersion: v1 + kind: Service + metadata: + name: podinfo + annotations: + metallb.universe.tf/allow-shared-ip: podinfo2 + spec: + type: LoadBalancer diff --git a/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml b/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml new file mode 100644 index 00000000..f3a4f1fc --- /dev/null +++ b/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml @@ -0,0 +1,17 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector +metadata: + name: podinfo2 +spec: + name: podinfo2 + tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value + site: "DM-Akron" # Use the `name` value instead of the `slug` value + description: "instance2" + comments: "podinfo in pss namespace" + preserveInNetbox: true + prefixLength: "/32" + parentPrefixSelector: + tenant: MY_TENANT + site: DM-Akron + environment: "Production" + poolName: "Pool 1" diff --git a/docs/examples/set-up/README.md b/docs/examples/set-up/README.md deleted file mode 100644 index 6be7d1a3..00000000 --- a/docs/examples/set-up/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# NetBox Operator Examples - -This folder shows some examples how the NetBox Operator can be used. The demo environment can be prepared with the 'docs/examples/set-up/prepare-demo-env.sh' script, which creates two kind clusters with NetBox Operator installed. One one of the clusters a NetBox instance is installed which is available to both NetBox Operator deployments. - -Prerequisites: -- go version v1.24.0+ -- docker image netbox-operatore:build-local -- kustomize version v5.5.0+ -- kubectl version v1.32.2+ -- kind v0.27.0 -- docker cli diff --git a/docs/examples/set-up/create-kind-clusters.sh b/docs/examples/set-up/create-kind-clusters.sh index 481e3789..bcf6d58f 100755 --- a/docs/examples/set-up/create-kind-clusters.sh +++ b/docs/examples/set-up/create-kind-clusters.sh @@ -41,6 +41,12 @@ for clustername in "$@"; do # install MetalLB kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml + + # install kro + helm install kro oci://ghcr.io/kro-run/kro/kro \ + --namespace kro \ + --create-namespace \ + --version=0.2.1 else echo -e "${RED}Error: Configuration file $config_file not found${NC}" exit 1 diff --git a/docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml b/docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml new file mode 100644 index 00000000..7b07a705 --- /dev/null +++ b/docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml @@ -0,0 +1,45 @@ +apiVersion: kro.run/v1alpha1 +kind: ResourceGraphDefinition +metadata: + name: metallb-ip-address-pool-from-netbox-parent-prefix +spec: + schema: + apiVersion: v1alpha1 + kind: MetalLBIPAddressPoolFromNetBoxParentPrefix + spec: + name: string + tenant: string + site: string + description: string + comments: string + preserveInNetbox: boolean + prefixLength: string + parentPrefix: string + status: + + # Define the resources this API will manage. + resources: + - id: prefixclaim + template: + apiVersion: netbox.dev/v1 + kind: PrefixClaim + metadata: + name: ${schema.spec.name} # Use the name provided by user + spec: + tenant: ${schema.spec.tenant} + description: ${schema.spec.description} + comments: ${schema.spec.comments} + preserveInNetbox: ${schema.spec.preserveInNetbox} + parentPrefix: ${schema.spec.parentPrefix} + prefixLength: ${schema.spec.prefixLength} + + - id: ipaddresspool + template: + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool + metadata: + name: ${schema.spec.name} + namespace: metallb-system + spec: + addresses: + - ${prefixclaim.status.prefix} diff --git a/docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml b/docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml new file mode 100644 index 00000000..b9b1e1fa --- /dev/null +++ b/docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml @@ -0,0 +1,49 @@ +apiVersion: kro.run/v1alpha1 +kind: ResourceGraphDefinition +metadata: + name: metallb-ip-address-pool-from-netbox-parent-prefix-selector +spec: + schema: + apiVersion: v1alpha1 + kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector + spec: + name: string + tenant: string + site: string + description: string + comments: string + preserveInNetbox: boolean + prefixLength: string + parentPrefixSelector: + environment: string + poolName: string + site: string + tenant: string + status: + + # Define the resources this API will manage. + resources: + - id: prefixclaim + template: + apiVersion: netbox.dev/v1 + kind: PrefixClaim + metadata: + name: ${schema.spec.name} # Use the name provided by user + spec: + tenant: ${schema.spec.tenant} + description: ${schema.spec.description} + comments: ${schema.spec.comments} + preserveInNetbox: ${schema.spec.preserveInNetbox} + prefixLength: ${schema.spec.prefixLength} + parentPrefixSelector: ${schema.spec.parentPrefixSelector} + + - id: ipaddresspool + template: + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool + metadata: + name: ${schema.spec.name} + namespace: metallb-system + spec: + addresses: + - ${prefixclaim.status.prefix} diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/set-up/prepare-demo-env.sh index fc547179..d124d76b 100755 --- a/docs/examples/set-up/prepare-demo-env.sh +++ b/docs/examples/set-up/prepare-demo-env.sh @@ -14,8 +14,14 @@ kubectl apply -f docs/examples/set-up/netbox-l2advertisement.yaml kubectl config use-context kind-prod kind load docker-image netbox-operator:build-local --name prod kustomize build docs/examples/set-up/ | kubectl apply -f - +# install resource graph defintions +kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml +kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml kubectl config use-context kind-dev kind load docker-image netbox-operator:build-local --name dev kustomize build docs/examples/set-up/ | kubectl apply -f - +# install resource graph defintions +kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml +kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml From 35c4082672eb80bacf3c3b012145a2b2f37897aa Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 10 Mar 2025 09:42:19 +0100 Subject: [PATCH 03/28] Fix podman image load --- docs/examples/set-up/prepare-demo-env.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/set-up/prepare-demo-env.sh index d124d76b..a633e116 100755 --- a/docs/examples/set-up/prepare-demo-env.sh +++ b/docs/examples/set-up/prepare-demo-env.sh @@ -13,6 +13,7 @@ kubectl apply -f docs/examples/set-up/netbox-l2advertisement.yaml # install NetBox Operator kubectl config use-context kind-prod kind load docker-image netbox-operator:build-local --name prod +kind load docker-image netbox-operator:build-local --name prod # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - # install resource graph defintions kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml @@ -21,6 +22,7 @@ kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml kubectl config use-context kind-dev kind load docker-image netbox-operator:build-local --name dev +kind load docker-image netbox-operator:build-local --name dev # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - # install resource graph defintions kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml From 291d7db0e55ee4abec8adf26e2a91ab729e6db46 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 10 Mar 2025 09:42:28 +0100 Subject: [PATCH 04/28] Add example5 --- .../netbox_v1_prefixclaim.yaml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml diff --git a/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml b/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml new file mode 100644 index 00000000..5219fb06 --- /dev/null +++ b/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: netbox.dev/v1 +kind: PrefixClaim +metadata: + labels: + app.kubernetes.io/name: netbox-operator + app.kubernetes.io/managed-by: kustomize + name: prefixclaim-exhaustion-sample-1 +spec: + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: dev + prefixLength: "/25" +--- +apiVersion: netbox.dev/v1 +kind: PrefixClaim +metadata: + labels: + app.kubernetes.io/name: netbox-operator + app.kubernetes.io/managed-by: kustomize + name: prefixclaim-exhaustion-sample-2 +spec: + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: dev + prefixLength: "/25" +--- +apiVersion: netbox.dev/v1 +kind: PrefixClaim +metadata: + labels: + app.kubernetes.io/name: netbox-operator + app.kubernetes.io/managed-by: kustomize + name: prefixclaim-exhaustion-sample-3 +spec: + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: dev + prefixLength: "/25" From c8f47f13dbed50a1a8c64c1739205c424f305775 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:24:03 +0100 Subject: [PATCH 05/28] rename example clusters and simlify examples --- docs/examples/README.md | 39 +- docs/examples/example1/example1.drawio.svg | 463 ++++++++++++++++++ .../example3/podinfo-int-ipaddresspool.yaml | 5 +- .../instance1/podinfo-int-ipaddresspool.yaml | 5 +- .../instance2/podinfo-int-ipaddresspool.yaml | 5 +- .../netbox_v1_prefixclaim.yaml | 3 + docs/examples/set-up/prepare-demo-env.sh | 22 +- 7 files changed, 499 insertions(+), 43 deletions(-) create mode 100644 docs/examples/example1/example1.drawio.svg diff --git a/docs/examples/README.md b/docs/examples/README.md index fb4c2014..2c0dda90 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -18,37 +18,36 @@ The following chapters show some examples which depend on each other. Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. -1. Point your kubeconfig to the kind cluster 'kind-prod' `kubectl config use-context kind-prod` -2. Port-forward NetBox: `kubectl port-forward deploy/netbox 8080:8080 -n default` -3. Open in your favorite browser and log in with the username `admin` and password `admin` -4. Create a new prefix '3.0.4.8/29' with custom fields `environment: "Production", poolName: "Pool 1"`,tenant `MY_TENANT` and site `DM-Akron` +1. Port-forward NetBox: `kubectl port-forward --context kind-london deploy/netbox 8080:8080 -n default` +2. Open in your favorite browser and log in with the username `admin` and password `admin` +3. Create a new prefix '3.0.4.8/29' with custom fields`environment: prod # 1. Claim a Prefix -1. Point your kubeconfig to the kind cluster 'kind-dev' `kubectl config use-context kind-dev` -2. Apply the manifest defining the prefix claim `kubectl apply -f docs/examples/example1/netbox_v1_prefixclaim.yaml` -3. Check that the prefix claim CR got a prefix addigned `kubectl get pxc` +1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1/netbox_v1_prefixclaim.yaml` +2. Check that the prefix claim CR got a prefix addigned `kubectl get --context kind-zurich pxc` + +![Example 1](example1/example1.drawio.svg) # 2. Claim a Prefix for a Podinfo Deployment and Create a MetalLB IPAddressPool This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. -1. create the namespace where podinfo should be deployed `kubectl create ns test` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example2 -n test` -3. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n test` +1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns test` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -k docs/examples/example2 -n test` +3. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc podinfo -n test` # 3. Dynamically Claim a Prefix with a Parent Prefix Selector for a Podinfo Deployment and Create a MetalLB IPAddressPool -1. create the namespace where podinfo should be deployed `kubectl create ns int` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example3 -n int` -3. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n int` +1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns int` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -k docs/examples/example3 -n int` +3. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc podinfo -n int` # 4. Mutli cluster set up with source of truth in netbox -1. Point your kubeconfig to the kind cluster 'kind-dev' `kubectl config use-context kind-prod` -2. create the namespace where podinfo should be deployed `kubectl create ns instance1` -3. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example4/instance1 -n instance1` -4. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n instance1` -5. create the namespace where podinfo should be deployed `kubectl create ns instance2` -6. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply -k docs/examples/example4/instance2 -n instance2` -7. check if the frontend service got an external ip address assigned `kubectl get svc podinfo -n instance2` \ No newline at end of file +1. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance1` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance1 -n instance1` +3. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance1` +4. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance2` +5. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance2 -n instance2` +6. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance2` \ No newline at end of file diff --git a/docs/examples/example1/example1.drawio.svg b/docs/examples/example1/example1.drawio.svg new file mode 100644 index 00000000..8a424179 --- /dev/null +++ b/docs/examples/example1/example1.drawio.svg @@ -0,0 +1,463 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.0/16 +
+
+ prefixLength: /28 +
+
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/example3/podinfo-int-ipaddresspool.yaml b/docs/examples/example3/podinfo-int-ipaddresspool.yaml index ddb8dd31..0c681f59 100644 --- a/docs/examples/example3/podinfo-int-ipaddresspool.yaml +++ b/docs/examples/example3/podinfo-int-ipaddresspool.yaml @@ -11,7 +11,4 @@ spec: preserveInNetbox: true prefixLength: "/32" parentPrefixSelector: - tenant: MY_TENANT - site: DM-Akron - environment: "Production" - poolName: "Pool 1" + environment: prod diff --git a/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml b/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml index 96a181d3..5421eaa6 100644 --- a/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml +++ b/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml @@ -11,7 +11,4 @@ spec: preserveInNetbox: true prefixLength: "/32" parentPrefixSelector: - tenant: MY_TENANT - site: DM-Akron - environment: "Production" - poolName: "Pool 1" + environment: prod diff --git a/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml b/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml index f3a4f1fc..c11648d8 100644 --- a/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml +++ b/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml @@ -11,7 +11,4 @@ spec: preserveInNetbox: true prefixLength: "/32" parentPrefixSelector: - tenant: MY_TENANT - site: DM-Akron - environment: "Production" - poolName: "Pool 1" + environment: prod diff --git a/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml b/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml index 5219fb06..1a3ca1f0 100644 --- a/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml +++ b/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml @@ -6,6 +6,7 @@ metadata: app.kubernetes.io/name: netbox-operator app.kubernetes.io/managed-by: kustomize name: prefixclaim-exhaustion-sample-1 + namespace: restore spec: tenant: "MY_TENANT" preserveInNetbox: true @@ -20,6 +21,7 @@ metadata: app.kubernetes.io/name: netbox-operator app.kubernetes.io/managed-by: kustomize name: prefixclaim-exhaustion-sample-2 + namespace: restore spec: tenant: "MY_TENANT" preserveInNetbox: true @@ -34,6 +36,7 @@ metadata: app.kubernetes.io/name: netbox-operator app.kubernetes.io/managed-by: kustomize name: prefixclaim-exhaustion-sample-3 + namespace: restore spec: tenant: "MY_TENANT" preserveInNetbox: true diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/set-up/prepare-demo-env.sh index a633e116..eb773073 100755 --- a/docs/examples/set-up/prepare-demo-env.sh +++ b/docs/examples/set-up/prepare-demo-env.sh @@ -1,28 +1,28 @@ #!/bin/bash set -e -# create the kind clusters netbox, prod and dev -./docs/examples/set-up/create-kind-clusters.sh prod dev +# create the kind clusters zurich and london +./docs/examples/set-up/create-kind-clusters.sh zurich london -# install netbox in the netbox cluster and load demo data -kubectl config use-context kind-prod -./kind/deploy-netbox.sh prod "4.1.8" default +# install netbox in the london cluster and load demo data +kubectl config use-context kind-london +./kind/deploy-netbox.sh london "4.1.8" default kubectl apply -f docs/examples/set-up/netbox-svc.yaml kubectl apply -f docs/examples/set-up/netbox-l2advertisement.yaml # install NetBox Operator -kubectl config use-context kind-prod -kind load docker-image netbox-operator:build-local --name prod -kind load docker-image netbox-operator:build-local --name prod # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image +kubectl config use-context kind-london +kind load docker-image netbox-operator:build-local --name london +kind load docker-image netbox-operator:build-local --name london # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - # install resource graph defintions kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml -kubectl config use-context kind-dev -kind load docker-image netbox-operator:build-local --name dev -kind load docker-image netbox-operator:build-local --name dev # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image +kubectl config use-context kind-zurich +kind load docker-image netbox-operator:build-local --name zurich +kind load docker-image netbox-operator:build-local --name zurich # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - # install resource graph defintions kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml From 45269a4f58a48215c0cfbd8ed6ba0facba152963 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Wed, 12 Mar 2025 13:54:36 +0100 Subject: [PATCH 06/28] Add drawio backup to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 36661b3e..6f27a999 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,8 @@ go.work *.swo *~ .drawio.dtmp +drawio.svg.bkp .DS_Store # Temporary files -tmp \ No newline at end of file +tmp From 2b61c6deb9ef7c6e6e7a421de4e14a32145e53e4 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Wed, 12 Mar 2025 13:55:14 +0100 Subject: [PATCH 07/28] Add drawio backup to gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6f27a999..68434da5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,8 @@ go.work *.swp *.swo *~ -.drawio.dtmp -drawio.svg.bkp +*.drawio.dtmp +*drawio.svg.bkp .DS_Store # Temporary files From 4849b1e276b37bfc83d1a66cd9e9f7dac74371e6 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Wed, 12 Mar 2025 13:55:40 +0100 Subject: [PATCH 08/28] Restructure examples folder and add Instructions and diagrams for Example 3 --- docs/examples/example1-simple/README.md | 1 + .../example1/example1.drawio.svg | 0 .../example1/netbox_v1_prefixclaim.yaml | 0 .../example2/kustomization.yaml | 0 .../podinfo-podinfo-ipaddresspool.yaml | 0 .../example3/kustomization.yaml | 0 .../example3/podinfo-int-ipaddresspool.yaml | 0 docs/examples/example2-multicluster/README.md | 1 + .../instance1/kustomization.yaml | 0 .../instance1/podinfo-int-ipaddresspool.yaml | 0 .../instance2/kustomization.yaml | 0 .../instance2/podinfo-int-ipaddresspool.yaml | 0 .../example3-advanced-features/README.md | 97 +++++ .../exhaustion-1-starting-point.drawio.svg | 4 + .../exhaustion-2-prefix-exhausted.drawio.svg | 4 + .../exhaustion-3-after-fix.drawio.svg | 4 + .../netbox_v1_prefixclaim.yaml | 29 +- .../restore.drawio.svg | 4 + ...-with-netbox-running-in-cluster.drawio.svg | 395 +----------------- 19 files changed, 133 insertions(+), 406 deletions(-) create mode 100644 docs/examples/example1-simple/README.md rename docs/examples/{ => example1-simple}/example1/example1.drawio.svg (100%) rename docs/examples/{ => example1-simple}/example1/netbox_v1_prefixclaim.yaml (100%) rename docs/examples/{ => example1-simple}/example2/kustomization.yaml (100%) rename docs/examples/{ => example1-simple}/example2/podinfo-podinfo-ipaddresspool.yaml (100%) rename docs/examples/{ => example1-simple}/example3/kustomization.yaml (100%) rename docs/examples/{ => example1-simple}/example3/podinfo-int-ipaddresspool.yaml (100%) create mode 100644 docs/examples/example2-multicluster/README.md rename docs/examples/{example4 => example2-multicluster}/instance1/kustomization.yaml (100%) rename docs/examples/{example4 => example2-multicluster}/instance1/podinfo-int-ipaddresspool.yaml (100%) rename docs/examples/{example4 => example2-multicluster}/instance2/kustomization.yaml (100%) rename docs/examples/{example4 => example2-multicluster}/instance2/podinfo-int-ipaddresspool.yaml (100%) create mode 100644 docs/examples/example3-advanced-features/README.md create mode 100644 docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg create mode 100644 docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg create mode 100644 docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg rename docs/examples/{example5_exhaustion => example3-advanced-features}/netbox_v1_prefixclaim.yaml (58%) create mode 100644 docs/examples/example3-advanced-features/restore.drawio.svg diff --git a/docs/examples/example1-simple/README.md b/docs/examples/example1-simple/README.md new file mode 100644 index 00000000..22980694 --- /dev/null +++ b/docs/examples/example1-simple/README.md @@ -0,0 +1 @@ +# Example 1: Simple diff --git a/docs/examples/example1/example1.drawio.svg b/docs/examples/example1-simple/example1/example1.drawio.svg similarity index 100% rename from docs/examples/example1/example1.drawio.svg rename to docs/examples/example1-simple/example1/example1.drawio.svg diff --git a/docs/examples/example1/netbox_v1_prefixclaim.yaml b/docs/examples/example1-simple/example1/netbox_v1_prefixclaim.yaml similarity index 100% rename from docs/examples/example1/netbox_v1_prefixclaim.yaml rename to docs/examples/example1-simple/example1/netbox_v1_prefixclaim.yaml diff --git a/docs/examples/example2/kustomization.yaml b/docs/examples/example1-simple/example2/kustomization.yaml similarity index 100% rename from docs/examples/example2/kustomization.yaml rename to docs/examples/example1-simple/example2/kustomization.yaml diff --git a/docs/examples/example2/podinfo-podinfo-ipaddresspool.yaml b/docs/examples/example1-simple/example2/podinfo-podinfo-ipaddresspool.yaml similarity index 100% rename from docs/examples/example2/podinfo-podinfo-ipaddresspool.yaml rename to docs/examples/example1-simple/example2/podinfo-podinfo-ipaddresspool.yaml diff --git a/docs/examples/example3/kustomization.yaml b/docs/examples/example1-simple/example3/kustomization.yaml similarity index 100% rename from docs/examples/example3/kustomization.yaml rename to docs/examples/example1-simple/example3/kustomization.yaml diff --git a/docs/examples/example3/podinfo-int-ipaddresspool.yaml b/docs/examples/example1-simple/example3/podinfo-int-ipaddresspool.yaml similarity index 100% rename from docs/examples/example3/podinfo-int-ipaddresspool.yaml rename to docs/examples/example1-simple/example3/podinfo-int-ipaddresspool.yaml diff --git a/docs/examples/example2-multicluster/README.md b/docs/examples/example2-multicluster/README.md new file mode 100644 index 00000000..d80980fd --- /dev/null +++ b/docs/examples/example2-multicluster/README.md @@ -0,0 +1 @@ +# Example 3: Multi Cluster diff --git a/docs/examples/example4/instance1/kustomization.yaml b/docs/examples/example2-multicluster/instance1/kustomization.yaml similarity index 100% rename from docs/examples/example4/instance1/kustomization.yaml rename to docs/examples/example2-multicluster/instance1/kustomization.yaml diff --git a/docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml b/docs/examples/example2-multicluster/instance1/podinfo-int-ipaddresspool.yaml similarity index 100% rename from docs/examples/example4/instance1/podinfo-int-ipaddresspool.yaml rename to docs/examples/example2-multicluster/instance1/podinfo-int-ipaddresspool.yaml diff --git a/docs/examples/example4/instance2/kustomization.yaml b/docs/examples/example2-multicluster/instance2/kustomization.yaml similarity index 100% rename from docs/examples/example4/instance2/kustomization.yaml rename to docs/examples/example2-multicluster/instance2/kustomization.yaml diff --git a/docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml b/docs/examples/example2-multicluster/instance2/podinfo-int-ipaddresspool.yaml similarity index 100% rename from docs/examples/example4/instance2/podinfo-int-ipaddresspool.yaml rename to docs/examples/example2-multicluster/instance2/podinfo-int-ipaddresspool.yaml diff --git a/docs/examples/example3-advanced-features/README.md b/docs/examples/example3-advanced-features/README.md new file mode 100644 index 00000000..8df1f659 --- /dev/null +++ b/docs/examples/example3-advanced-features/README.md @@ -0,0 +1,97 @@ +# Example 3: Advanced Features + +## Description + +This demo showcases the two following cases: + +- Example 3a: Automatic Reconcilation in case of Prefix Exhaustion. When a Prefix is exhausted and this is fixed in the NetBox backend, NetBox Operator will automatically reconcile this. +- Example 3b: Restoration of Prefixes + +## Instructions + +### Example 3a: Prefix Exhaustion and Reconciliation + +![Figure 1: Starting Point](exhaustion-1-starting-point.drawio.svg) + +Create a /24 Prefix (e.g. 1.122.0.0/24) with Custom Field Environment set to "prod" in NetBox UI. + +Apply Resource and show PrefixClaims: + +```bash +kubectl --context kind-london apply -f netbox_v1_prefixclaim.yaml +kubectl --context kind-london -n advanced get prefixclaims,prefixes +``` + +Note that only 2 out of the 3 PrefixClaims will become Ready. This is because the /24 Prefix is exhausted already after two Prefixes. This will look similar to this (note the order is non-deterministic): + +```bash +NAME PREFIX PREFIXASSIGNED READY AGE +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True True 2m2s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True True 2m2s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-3 False 2m2s + +NAME PREFIX READY ID URL AGE +prefix.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True 148 http://172.18.1.2/ipam/prefixes/148 2m2s +prefix.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True 149 http://172.18.1.2/ipam/prefixes/149 2m2s +``` + +![Figure 2: Parent Prefix Exhausted](exhaustion-2-prefix-exhausted.drawio.svg) + + +Create another /24 Prefix (e.g. 1.100.0.0/24) with Custom Field Environment set to "prod" in NetBox UI. + +Wait for the PrefixClaim to be reconciled again or trigger reconciliation by e.g. adding an annotation: + +```bash +kubectl --context kind-london -n advanced annotate prefixclaim prefixclaim-exhaustion-sample-3 reconcile="$(date)" --overwrite +``` + +Confirm that the third Prefix is now also assigned: + +```bash +kubectl --context kind-london -n advanced get prefixclaims,prefixes +``` + +Which should look as follows: + +```bash +NAME PREFIX PREFIXASSIGNED READY AGE +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True True 4s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True True 4s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-3 1.100.0.0/25 True True 4s + +NAME PREFIX READY ID URL AGE +prefix.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True 148 http://172.18.1.2/ipam/prefixes/148 4s +prefix.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True 149 http://172.18.1.2/ipam/prefixes/149 4s +prefix.netbox.dev/prefixclaim-exhaustion-sample-3 1.100.0.0/25 True 151 http://172.18.1.2/ipam/prefixes/151 3s``` +``` + +![Figure 3: Parent Prefix Exhaustion fixed](exhaustion-3-after-fix.drawio.svg) + +### Example 3b: Restoration + +![Figure 4: Restoration](restore.drawio.svg) + +Since we set `.spec.preserveInNetbox` to `true`, we can delete and restore the resources. To delete all reasources, delete the entire namespace: + +```bash +kubectl --context kind-london delete ns advanced +``` + +Make sure the resources are gone in Kubernetes: + +```bash +kubectl --context kind-london -n advanced get prefixclaims +``` + +Verify in the NetBox UI that the Prefixes still exist. + +Now apply the manifests again and verify they become ready. + +```bash +kubectl --context kind-london apply -f netbox_v1_prefixclaim.yaml +kubectl --context kind-london -n advanced wait --for=condition=Ready prefixclaims --all +kubectl --context kind-london -n advanced get prefixclaims +``` + +Note that the assigned Prefixes are the same as before. You can also play around with this by just restoring single prefixes. If you're curious about how this is done, make sure to read [the "Restoration from NetBox" section in the main README.md](https://github.com/netbox-community/netbox-operator/tree/main?tab=readme-ov-file#restoration-from-netbox) and to check out the code. Also have a look at the "Netbox Restoration Hash" custom field in NetBox. diff --git a/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg new file mode 100644 index 00000000..4b388bd2 --- /dev/null +++ b/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 0%
Prefix 1.122.0.0/24...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg new file mode 100644 index 00000000..afdba0e3 --- /dev/null +++ b/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
get available prefixes
get available prefixes
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
create/update/delete
create/update/delete
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg new file mode 100644 index 00000000..7a86fc54 --- /dev/null +++ b/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/24
environment: prod
Usage: 50%
Prefix 1.100.0.0/24...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
create/update/delete
create/update/delete
get available prefixes
get available prefixes
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml b/docs/examples/example3-advanced-features/netbox_v1_prefixclaim.yaml similarity index 58% rename from docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml rename to docs/examples/example3-advanced-features/netbox_v1_prefixclaim.yaml index 1a3ca1f0..5233847b 100644 --- a/docs/examples/example5_exhaustion/netbox_v1_prefixclaim.yaml +++ b/docs/examples/example3-advanced-features/netbox_v1_prefixclaim.yaml @@ -1,45 +1,44 @@ --- +apiVersion: v1 +kind: Namespace +metadata: + name: advanced +--- apiVersion: netbox.dev/v1 kind: PrefixClaim metadata: - labels: - app.kubernetes.io/name: netbox-operator - app.kubernetes.io/managed-by: kustomize name: prefixclaim-exhaustion-sample-1 - namespace: restore + namespace: advanced spec: tenant: "MY_TENANT" preserveInNetbox: true parentPrefixSelector: - environment: dev + environment: prod + family: IPv4 prefixLength: "/25" --- apiVersion: netbox.dev/v1 kind: PrefixClaim metadata: - labels: - app.kubernetes.io/name: netbox-operator - app.kubernetes.io/managed-by: kustomize name: prefixclaim-exhaustion-sample-2 - namespace: restore + namespace: advanced spec: tenant: "MY_TENANT" preserveInNetbox: true parentPrefixSelector: - environment: dev + environment: prod + family: IPv4 prefixLength: "/25" --- apiVersion: netbox.dev/v1 kind: PrefixClaim metadata: - labels: - app.kubernetes.io/name: netbox-operator - app.kubernetes.io/managed-by: kustomize name: prefixclaim-exhaustion-sample-3 - namespace: restore + namespace: advanced spec: tenant: "MY_TENANT" preserveInNetbox: true parentPrefixSelector: - environment: dev + environment: prod + family: IPv4 prefixLength: "/25" diff --git a/docs/examples/example3-advanced-features/restore.drawio.svg b/docs/examples/example3-advanced-features/restore.drawio.svg new file mode 100644 index 00000000..bd406f83 --- /dev/null +++ b/docs/examples/example3-advanced-features/restore.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/prefixclaim-sample-with-netbox-running-in-cluster.drawio.svg b/docs/prefixclaim-sample-with-netbox-running-in-cluster.drawio.svg index 3ccdd280..99cd828a 100644 --- a/docs/prefixclaim-sample-with-netbox-running-in-cluster.drawio.svg +++ b/docs/prefixclaim-sample-with-netbox-running-in-cluster.drawio.svg @@ -1,391 +1,4 @@ - - - - - - - - -
-
-
- k8s cluster -
-
-
-
- - k8s cluster - -
-
- - - - -
-
-
- namespace default -
-
-
-
- - namespace default - -
-
- - - - - -
-
-
- user namespace -
-
-
-
- - user namespace - -
-
- - - - - - -
-
-
- consumer -
-
-
-
- - consumer - -
-
- - - - -
-
-
- namespace netbox-operator -
-
-
-
- - namespace netbox-operator - -
-
- - - - - -
-
-
- create -
-
-
-
- - create - -
-
- - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
- - - - -
-
-
- netbox-operator -
-
-
-
- - netbox-operator - -
-
- - - - -
-
-
-
- kind: PrefixClaim -
-
- spec: -
-
- parentPrefix: 2.0.0.0/16 -
-
- prefixLength: /28 -
-
-
-
-
-
- - kind: PrefixClaim... - -
-
- - - - - -
-
-
- User -
- w/ kubectl -
-
-
-
- - User... - -
-
- - - - - -
-
-
- GitOps -
- w/ Argo or Flux -
-
-
-
- - GitOps... - -
-
- - - - -
-
-
- and/or -
-
-
-
- - and/or - -
-
- - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
- - - - - -
-
-
- ownerReference -
-
-
-
- - ownerReference - -
-
- - - - -
-
-
-
- kind: Prefix -
-
- spec: -
-
- prefix: 2.0.0.0/28 -
-
-
-
-
- - kind: Prefix... - -
-
- - - - - -
-
-
- NetBox REST API -
-
-
-
- - NetBox REST API - -
-
- - - - - - -
-
-
- create/update/delete -
-
-
-
- - create/update/delete - -
-
- - - - - -
-
-
- get available prefixes -
-
-
-
- - get available prefixes - -
-
- - - - -
-
-
- Prefix 2.0.0.0/16 -
-
-
-
- - Prefix 2.0.0.0/16 - -
-
- - - - -
-
-
- Prefix 2.0.0.0/28 -
-
-
-
- - Prefix 2.0.0.0/28 - -
-
-
- - - - - Text is not SVG - cannot display - - - -
+ + + +
k8s cluster
k8s cluster
namespace default
namespace default
user namespace
user namespace
namespace netbox-operator
namespace netbox-operator
create
create
reconcile
reconcile
netbox-operator
netbox-operator
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.0/28
kind: Prefix...
NetBox REST API
NetBox REST API
create/update/delete
create/update/delete
get available prefixes
get available prefixes
Prefix 2.0.0.0/16
Prefix 2.0.0.0/16
Prefix 2.0.0.0/28
Prefix 2.0.0.0/28
kind: MetalLBIPPoolNetBox (kro )
spec:
  parentPrefix: 2.0.0.0/16
  prefixLength: /28
kind: MetalLBIPPoolNetBox (kro )...
kind: PrefixClaim
spec:
  parentPrefix: 2.0.0.0/16
  prefixLength: /28
status:
  ipAddress:
kind: PrefixClaim...
kind: IPAddressPool
spec:
  parentPrefix: 2.0.0.0/16
  prefixLength: /28
kind: IPAddressPool...
Text is not SVG - cannot display
\ No newline at end of file From ac1f67828cd97d8f1a9008c713f8c59feaf90262 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:17:23 +0100 Subject: [PATCH 09/28] simplify examples --- docs/examples/README.md | 40 -- .../example1-getting-started/README.md | 35 ++ .../dynamic-prefix-claim.yaml} | 11 +- .../dynamic-prefixclaim.drawio.svg} | 0 .../kustomization.yaml | 2 +- .../metallb-ipaddresspool-netbox.drawio.svg | 465 ++++++++++++++++++ .../sample-deployment.yaml} | 7 +- .../simple_prefixclaim.drawio.svg | 463 +++++++++++++++++ .../simple_prefixclaim.yaml | 12 + docs/examples/example1-simple/README.md | 1 - .../example3/kustomization.yaml | 14 - .../example3/podinfo-int-ipaddresspool.yaml | 14 - docs/examples/example2-multicluster/README.md | 7 + .../example2-multicluster/example1.drawio.svg | 463 +++++++++++++++++ ...ddress-pool-from-netbox-parent-prefix.yaml | 45 -- ...ml => metallb-ip-address-pool-netbox.yaml} | 13 +- docs/examples/set-up/prepare-demo-env.sh | 41 +- 17 files changed, 1493 insertions(+), 140 deletions(-) create mode 100644 docs/examples/example1-getting-started/README.md rename docs/examples/{example1-simple/example1/netbox_v1_prefixclaim.yaml => example1-getting-started/dynamic-prefix-claim.yaml} (56%) rename docs/examples/{example1-simple/example1/example1.drawio.svg => example1-getting-started/dynamic-prefixclaim.drawio.svg} (100%) rename docs/examples/{example1-simple/example2 => example1-getting-started}/kustomization.yaml (88%) create mode 100644 docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg rename docs/examples/{example1-simple/example2/podinfo-podinfo-ipaddresspool.yaml => example1-getting-started/sample-deployment.yaml} (55%) create mode 100644 docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg create mode 100644 docs/examples/example1-getting-started/simple_prefixclaim.yaml delete mode 100644 docs/examples/example1-simple/README.md delete mode 100644 docs/examples/example1-simple/example3/kustomization.yaml delete mode 100644 docs/examples/example1-simple/example3/podinfo-int-ipaddresspool.yaml create mode 100644 docs/examples/example2-multicluster/example1.drawio.svg delete mode 100644 docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml rename docs/examples/set-up/{metallb-ip-address-pool-from-netbox.yaml => metallb-ip-address-pool-netbox.yaml} (74%) diff --git a/docs/examples/README.md b/docs/examples/README.md index 2c0dda90..b79e888f 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -11,43 +11,3 @@ Prerequisites: - kubectl version v1.32.2+ - kind v0.27.0 - docker cli - -The following chapters show some examples which depend on each other. - -# 0. Manually Create a Prefix in NetBox - -Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. - -1. Port-forward NetBox: `kubectl port-forward --context kind-london deploy/netbox 8080:8080 -n default` -2. Open in your favorite browser and log in with the username `admin` and password `admin` -3. Create a new prefix '3.0.4.8/29' with custom fields`environment: prod - -# 1. Claim a Prefix - -1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1/netbox_v1_prefixclaim.yaml` -2. Check that the prefix claim CR got a prefix addigned `kubectl get --context kind-zurich pxc` - -![Example 1](example1/example1.drawio.svg) - -# 2. Claim a Prefix for a Podinfo Deployment and Create a MetalLB IPAddressPool - -This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. - -1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns test` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -k docs/examples/example2 -n test` -3. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc podinfo -n test` - -# 3. Dynamically Claim a Prefix with a Parent Prefix Selector for a Podinfo Deployment and Create a MetalLB IPAddressPool - -1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns int` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -k docs/examples/example3 -n int` -3. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc podinfo -n int` - -# 4. Mutli cluster set up with source of truth in netbox - -1. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance1` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance1 -n instance1` -3. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance1` -4. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance2` -5. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance2 -n instance2` -6. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance2` \ No newline at end of file diff --git a/docs/examples/example1-getting-started/README.md b/docs/examples/example1-getting-started/README.md new file mode 100644 index 00000000..d2e6983b --- /dev/null +++ b/docs/examples/example1-getting-started/README.md @@ -0,0 +1,35 @@ +# Example 1: Getting Started + +# 0. Manually Create a Prefix in NetBox + +Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. + +1. Port-forward NetBox: `kubectl port-forward --context kind-london deploy/netbox 8080:8080 -n default` +2. Open in your favorite browser and log in with the username `admin` and password `admin` +3. Create a new prefix '3.0.0.64/26' with custom fields 'environment: prod' + +# 1.1 Claim a Prefix + +1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/simple_prefixclaim.yaml` +2. Check that the prefix claim CR got a prefix addigned `kubectl get --context kind-zurich pxc,px -w` + +![Example 1.1](simple_prefixclaim.drawio.svg) + +# 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector + +1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns int` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/simple_prefixclaim.yaml` +3. check if the frontend service got an external ip address assigned `kubectl get --context pxc,px -w` + +![Example 1.2](dynamic-prefixclaim.drawio.svg) + +# 1.3 Claim a Prefix for a Podinfo Deployment and Create a MetalLB IPAddressPool + +This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. + +1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns test` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -k docs/examples/example1-getting-started -n test` +3. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc podinfo -n test` + + +![Example 1.3](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/example1-simple/example1/netbox_v1_prefixclaim.yaml b/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml similarity index 56% rename from docs/examples/example1-simple/example1/netbox_v1_prefixclaim.yaml rename to docs/examples/example1-getting-started/dynamic-prefix-claim.yaml index d60e1953..58a1e0dc 100644 --- a/docs/examples/example1-simple/example1/netbox_v1_prefixclaim.yaml +++ b/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml @@ -4,12 +4,11 @@ metadata: labels: app.kubernetes.io/name: netbox-operator app.kubernetes.io/managed-by: kustomize - name: prefixclaim-sample + name: dynamic-prefix-claim spec: tenant: "MY_TENANT" - site: "DM-Akron" - description: "some description" - comments: "your comments" preserveInNetbox: true - parentPrefix: 3.0.4.8/29 - prefixLength: "/32" + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/30" diff --git a/docs/examples/example1-simple/example1/example1.drawio.svg b/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg similarity index 100% rename from docs/examples/example1-simple/example1/example1.drawio.svg rename to docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg diff --git a/docs/examples/example1-simple/example2/kustomization.yaml b/docs/examples/example1-getting-started/kustomization.yaml similarity index 88% rename from docs/examples/example1-simple/example2/kustomization.yaml rename to docs/examples/example1-getting-started/kustomization.yaml index bb88a271..643b8416 100644 --- a/docs/examples/example1-simple/example2/kustomization.yaml +++ b/docs/examples/example1-getting-started/kustomization.yaml @@ -2,7 +2,7 @@ metadata: namespace: test resources: - ../../../../podinfo/kustomize - - podinfo-podinfo-ipaddresspool.yaml + - sample-deployment.yaml patches: - patch: |- diff --git a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg new file mode 100644 index 00000000..68e39aae --- /dev/null +++ b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg @@ -0,0 +1,465 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.64/26 +
+
+ prefixLength: /28 +
+
+
+ status: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.16/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/example1-simple/example2/podinfo-podinfo-ipaddresspool.yaml b/docs/examples/example1-getting-started/sample-deployment.yaml similarity index 55% rename from docs/examples/example1-simple/example2/podinfo-podinfo-ipaddresspool.yaml rename to docs/examples/example1-getting-started/sample-deployment.yaml index b387621d..6c7c63b1 100644 --- a/docs/examples/example1-simple/example2/podinfo-podinfo-ipaddresspool.yaml +++ b/docs/examples/example1-getting-started/sample-deployment.yaml @@ -5,9 +5,6 @@ metadata: spec: name: podinfo-test tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value - site: "DM-Akron" # Use the `name` value instead of the `slug` value - description: "podinfo" - comments: "podinfo in podinfo namespace" preserveInNetbox: true - prefixLength: "/32" - parentPrefix: 3.0.4.8/29 \ No newline at end of file + prefixLength: "/30" + parentPrefix: 3.0.0.64/26 \ No newline at end of file diff --git a/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg b/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg new file mode 100644 index 00000000..8a424179 --- /dev/null +++ b/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg @@ -0,0 +1,463 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.0/16 +
+
+ prefixLength: /28 +
+
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/example1-getting-started/simple_prefixclaim.yaml b/docs/examples/example1-getting-started/simple_prefixclaim.yaml new file mode 100644 index 00000000..d76a5639 --- /dev/null +++ b/docs/examples/example1-getting-started/simple_prefixclaim.yaml @@ -0,0 +1,12 @@ +apiVersion: netbox.dev/v1 +kind: PrefixClaim +metadata: + labels: + app.kubernetes.io/name: netbox-operator + app.kubernetes.io/managed-by: kustomize + name: simple-prefixclaim +spec: + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefix: 3.0.0.64/26 + prefixLength: "/30" diff --git a/docs/examples/example1-simple/README.md b/docs/examples/example1-simple/README.md deleted file mode 100644 index 22980694..00000000 --- a/docs/examples/example1-simple/README.md +++ /dev/null @@ -1 +0,0 @@ -# Example 1: Simple diff --git a/docs/examples/example1-simple/example3/kustomization.yaml b/docs/examples/example1-simple/example3/kustomization.yaml deleted file mode 100644 index 84670460..00000000 --- a/docs/examples/example1-simple/example3/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -resources: - - ../../../../podinfo/kustomize - - podinfo-int-ipaddresspool.yaml - -patches: - - patch: |- - apiVersion: v1 - kind: Service - metadata: - name: podinfo - annotations: - metallb.universe.tf/allow-shared-ip: podinfo-int - spec: - type: LoadBalancer diff --git a/docs/examples/example1-simple/example3/podinfo-int-ipaddresspool.yaml b/docs/examples/example1-simple/example3/podinfo-int-ipaddresspool.yaml deleted file mode 100644 index 0c681f59..00000000 --- a/docs/examples/example1-simple/example3/podinfo-int-ipaddresspool.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector -metadata: - name: podinfo-int -spec: - name: podinfo-int - tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value - site: "DM-Akron" # Use the `name` value instead of the `slug` value - description: "int" - comments: "podinfo in pss namespace" - preserveInNetbox: true - prefixLength: "/32" - parentPrefixSelector: - environment: prod diff --git a/docs/examples/example2-multicluster/README.md b/docs/examples/example2-multicluster/README.md index d80980fd..cb7d651c 100644 --- a/docs/examples/example2-multicluster/README.md +++ b/docs/examples/example2-multicluster/README.md @@ -1 +1,8 @@ # Example 3: Multi Cluster + +1. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance1` +2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance1 -n instance1` +3. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance1` +4. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance2` +5. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance2 -n instance2` +6. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance2` diff --git a/docs/examples/example2-multicluster/example1.drawio.svg b/docs/examples/example2-multicluster/example1.drawio.svg new file mode 100644 index 00000000..8a424179 --- /dev/null +++ b/docs/examples/example2-multicluster/example1.drawio.svg @@ -0,0 +1,463 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.0/16 +
+
+ prefixLength: /28 +
+
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml b/docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml deleted file mode 100644 index 7b07a705..00000000 --- a/docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: kro.run/v1alpha1 -kind: ResourceGraphDefinition -metadata: - name: metallb-ip-address-pool-from-netbox-parent-prefix -spec: - schema: - apiVersion: v1alpha1 - kind: MetalLBIPAddressPoolFromNetBoxParentPrefix - spec: - name: string - tenant: string - site: string - description: string - comments: string - preserveInNetbox: boolean - prefixLength: string - parentPrefix: string - status: - - # Define the resources this API will manage. - resources: - - id: prefixclaim - template: - apiVersion: netbox.dev/v1 - kind: PrefixClaim - metadata: - name: ${schema.spec.name} # Use the name provided by user - spec: - tenant: ${schema.spec.tenant} - description: ${schema.spec.description} - comments: ${schema.spec.comments} - preserveInNetbox: ${schema.spec.preserveInNetbox} - parentPrefix: ${schema.spec.parentPrefix} - prefixLength: ${schema.spec.prefixLength} - - - id: ipaddresspool - template: - apiVersion: metallb.io/v1beta1 - kind: IPAddressPool - metadata: - name: ${schema.spec.name} - namespace: metallb-system - spec: - addresses: - - ${prefixclaim.status.prefix} diff --git a/docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml similarity index 74% rename from docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml rename to docs/examples/set-up/metallb-ip-address-pool-netbox.yaml index b9b1e1fa..f0aa7a92 100644 --- a/docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml +++ b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml @@ -1,24 +1,19 @@ apiVersion: kro.run/v1alpha1 kind: ResourceGraphDefinition metadata: - name: metallb-ip-address-pool-from-netbox-parent-prefix-selector + name: metallb-ip-address-pool--netbox spec: schema: apiVersion: v1alpha1 - kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector + kind: MetalLBIPAddressPoolNetBox spec: name: string tenant: string - site: string - description: string - comments: string preserveInNetbox: boolean prefixLength: string parentPrefixSelector: environment: string - poolName: string - site: string - tenant: string + family: string status: # Define the resources this API will manage. @@ -31,8 +26,6 @@ spec: name: ${schema.spec.name} # Use the name provided by user spec: tenant: ${schema.spec.tenant} - description: ${schema.spec.description} - comments: ${schema.spec.comments} preserveInNetbox: ${schema.spec.preserveInNetbox} prefixLength: ${schema.spec.prefixLength} parentPrefixSelector: ${schema.spec.parentPrefixSelector} diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/set-up/prepare-demo-env.sh index eb773073..ba5fe4d9 100755 --- a/docs/examples/set-up/prepare-demo-env.sh +++ b/docs/examples/set-up/prepare-demo-env.sh @@ -16,8 +16,7 @@ kind load docker-image netbox-operator:build-local --name london kind load docker-image netbox-operator:build-local --name london # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - # install resource graph defintions -kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml -kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml +kubectl apply --context kind-london -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml kubectl config use-context kind-zurich @@ -25,5 +24,39 @@ kind load docker-image netbox-operator:build-local --name zurich kind load docker-image netbox-operator:build-local --name zurich # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - # install resource graph defintions -kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox-parent-prefix.yaml -kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-from-netbox.yaml +kubectl apply -context kind-zurich -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml + +DEPLOYMENT_NAME=netbox-operator +NAMESPACE=netbox-operator-system +CONTEXT=kind-london + +while true; do + # Check if the deployment is ready + READY_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.readyReplicas}') + DESIRED_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.replicas}') + + if [[ "$READY_REPLICAS" == "$DESIRED_REPLICAS" ]] && [[ "$READY_REPLICAS" -gt 0 ]]; then + echo "Deployment $DEPLOYMENT_NAME in cluster $CONTEXT is ready." + break + else + echo "Waiting... Ready replicas in cluster $CONTEXT: $READY_REPLICAS / $DESIRED_REPLICAS" + sleep 5 + fi +done +kubectl apply --context $CONTEXT -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml + +CONTEXT=kind-zurich +while true; do + # Check if the deployment is ready + READY_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.readyReplicas}') + DESIRED_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.replicas}') + + if [[ "$READY_REPLICAS" == "$DESIRED_REPLICAS" ]] && [[ "$READY_REPLICAS" -gt 0 ]]; then + echo "Deployment $DEPLOYMENT_NAME in cluster $CONTEXT is ready." + break + else + echo "Waiting... Ready replicas in cluster $CONTEXT: $READY_REPLICAS / $DESIRED_REPLICAS" + sleep 5 + fi +done +kubectl apply --context $CONTEXT -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml From 6bf795e518c9248100c5e16a4ea2f3e51e993c38 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:26:15 +0100 Subject: [PATCH 10/28] simplify samples --- .../{README.md => README-ex1.md} | 11 +- .../dynamic-prefix-claim.yaml | 1 - .../dynamic-prefixclaim.drawio.svg | 140 ++-- .../ip-address-pool.yaml | 11 + .../metallb-ipaddresspool-netbox.drawio.svg | 344 ++++++-- .../sample-deployment.yaml | 49 +- .../simple_prefixclaim.drawio.svg | 136 +-- .../simple_prefixclaim.yaml | 3 +- .../example2-multicluster/README-ex2.md | 9 + docs/examples/example2-multicluster/README.md | 8 - .../example2-multicluster/example1.drawio.svg | 463 ----------- .../instance1/kustomization.yaml | 14 - .../instance1/podinfo-int-ipaddresspool.yaml | 14 - .../instance2/kustomization.yaml | 14 - .../instance2/podinfo-int-ipaddresspool.yaml | 14 - .../example2-multicluster/london-pools.yaml | 109 +++ .../multicluster.drawio.svg | 774 ++++++++++++++++++ .../example2-multicluster/zurich-pools.yaml | 109 +++ .../metallb-ip-address-pool-netbox.yaml | 4 +- docs/examples/set-up/prepare-demo-env.sh | 7 +- 20 files changed, 1468 insertions(+), 766 deletions(-) rename docs/examples/example1-getting-started/{README.md => README-ex1.md} (61%) create mode 100644 docs/examples/example1-getting-started/ip-address-pool.yaml create mode 100644 docs/examples/example2-multicluster/README-ex2.md delete mode 100644 docs/examples/example2-multicluster/README.md delete mode 100644 docs/examples/example2-multicluster/example1.drawio.svg delete mode 100644 docs/examples/example2-multicluster/instance1/kustomization.yaml delete mode 100644 docs/examples/example2-multicluster/instance1/podinfo-int-ipaddresspool.yaml delete mode 100644 docs/examples/example2-multicluster/instance2/kustomization.yaml delete mode 100644 docs/examples/example2-multicluster/instance2/podinfo-int-ipaddresspool.yaml create mode 100644 docs/examples/example2-multicluster/london-pools.yaml create mode 100644 docs/examples/example2-multicluster/multicluster.drawio.svg create mode 100644 docs/examples/example2-multicluster/zurich-pools.yaml diff --git a/docs/examples/example1-getting-started/README.md b/docs/examples/example1-getting-started/README-ex1.md similarity index 61% rename from docs/examples/example1-getting-started/README.md rename to docs/examples/example1-getting-started/README-ex1.md index d2e6983b..5361813f 100644 --- a/docs/examples/example1-getting-started/README.md +++ b/docs/examples/example1-getting-started/README-ex1.md @@ -6,7 +6,7 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a pref 1. Port-forward NetBox: `kubectl port-forward --context kind-london deploy/netbox 8080:8080 -n default` 2. Open in your favorite browser and log in with the username `admin` and password `admin` -3. Create a new prefix '3.0.0.64/26' with custom fields 'environment: prod' +3. Create a new prefix '3.0.0.64/25' with custom fields 'environment: prod' # 1.1 Claim a Prefix @@ -17,9 +17,8 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a pref # 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector -1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns int` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/simple_prefixclaim.yaml` -3. check if the frontend service got an external ip address assigned `kubectl get --context pxc,px -w` +1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/dynamic-prefix-claim.yaml` +2. Check if the frontend service got an external ip address assigned `kubectl get --context pxc,px -w` ![Example 1.2](dynamic-prefixclaim.drawio.svg) @@ -27,8 +26,8 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a pref This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. -1. create the namespace where podinfo should be deployed `kubectl create --context kind-zurich ns test` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-zurich -k docs/examples/example1-getting-started -n test` +1. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml` +2. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step. `kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml` 3. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc podinfo -n test` diff --git a/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml b/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml index 58a1e0dc..4a6c0d13 100644 --- a/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml +++ b/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml @@ -7,7 +7,6 @@ metadata: name: dynamic-prefix-claim spec: tenant: "MY_TENANT" - preserveInNetbox: true parentPrefixSelector: environment: prod family: IPv4 diff --git a/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg b/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg index 8a424179..920da03b 100644 --- a/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg +++ b/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg @@ -1,11 +1,11 @@ - + - + - + @@ -26,10 +26,10 @@ - + - + @@ -50,17 +50,17 @@ - - + + - + -
+
consumer @@ -68,14 +68,14 @@
- + consumer - + @@ -96,7 +96,7 @@ - + @@ -117,14 +117,14 @@ - - + + -
+
create @@ -132,21 +132,21 @@
- + create - - + + -
+
reconcile @@ -154,21 +154,21 @@
- + reconcile - - + + -
+
create/update/delete @@ -176,42 +176,45 @@
- + create/update/delete - - + + -
+
- get available prefixes + select matching parnet prefix +
+ and get available prefixes +
- - get available prefixes + + select matching parnet prefix... - + -
+
netbox-operator @@ -219,20 +222,20 @@
- + netbox-operator - + -
+
@@ -242,7 +245,10 @@ spec:
- parentPrefix: 2.0.0.0/16 + parentPrefixSelector: +
+
+ environment: prod
prefixLength: /28 @@ -252,20 +258,20 @@
- + kind: PrefixClaim... - + -
+
Prefix 2.0.0.0/16 @@ -273,21 +279,21 @@
- + Prefix 2.0.0.0/16 - - + + -
+
User @@ -297,21 +303,21 @@
- + User... - - + + -
+
GitOps @@ -321,20 +327,20 @@
- + GitOps... - + -
+
and/or @@ -342,21 +348,21 @@
- + and/or - - + + -
+
reconcile @@ -364,21 +370,21 @@
- + reconcile - - + + -
+
ownerReference @@ -386,20 +392,20 @@
- + ownerReference - + -
+
@@ -418,20 +424,20 @@
- + kind: Prefix... - + -
+
Prefix 2.0.0.0/28 @@ -439,17 +445,17 @@
- + Prefix 2.0.0.0/28 - + - + diff --git a/docs/examples/example1-getting-started/ip-address-pool.yaml b/docs/examples/example1-getting-started/ip-address-pool.yaml new file mode 100644 index 00000000..3dd1cf57 --- /dev/null +++ b/docs/examples/example1-getting-started/ip-address-pool.yaml @@ -0,0 +1,11 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-pool +spec: + name: zurich-pool + tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value + prefixLength: "/30" + parentPrefixSelector: + environment: prod + family: IPv4 diff --git a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg index 68e39aae..1eb84676 100644 --- a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg +++ b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg @@ -1,4 +1,4 @@ - + @@ -8,9 +8,9 @@ - + - +
@@ -32,9 +32,9 @@ - + - +
@@ -50,12 +50,16 @@ - + + - + + + + - +
@@ -74,9 +78,9 @@ - + - +
@@ -92,12 +96,12 @@ - + - + - +
@@ -113,14 +117,14 @@ - - + + - + - -
+ +
create @@ -128,21 +132,21 @@
- + create - - + + - + - -
+ +
reconcile @@ -150,21 +154,21 @@
- + reconcile - + - + - -
+ +
create/update/delete @@ -172,21 +176,21 @@
- + create/update/delete - - + + - + - -
+ +
get available prefixes @@ -194,20 +198,20 @@
- + get available prefixes - + - + - -
+ +
netbox-operator @@ -215,20 +219,24 @@
- + netbox-operator - + + + + + - + - -
+ +
@@ -238,7 +246,7 @@ spec:
- parentPrefix: 2.0.0.64/26 + parentPrefix: 2.0.0.0/26
prefixLength: /28 @@ -254,7 +262,7 @@
- + kind: PrefixClaim... @@ -264,9 +272,9 @@ - + - +
@@ -282,13 +290,13 @@ - + - + - +
@@ -310,9 +318,9 @@ - + - +
@@ -333,9 +341,9 @@ - + - +
@@ -351,36 +359,38 @@ - - + + - + - -
+ +
- reconcile + + reconcile +
- + reconcile - - + + - + - -
+ +
ownerReference @@ -388,20 +398,20 @@
- + ownerReference - + - + - -
+ +
@@ -411,7 +421,7 @@ spec:
- prefix: 2.0.0.16/28 + prefix: 2.0.0.0/28

@@ -420,7 +430,7 @@
- + kind: Prefix... @@ -430,9 +440,9 @@ - + - +
@@ -448,11 +458,189 @@ - + + + + + + + + + + + + + + + + +
+
+
+
+ kind: MetallbIPAddressPoolNetBox +
+
+ spec: +
+
+ name: my-pool +
+
+ parentPrefixSelector: +
+
+ environment: prod +
+
+ prefixLength: /28 +
+
+
+
+
+
+
+
+
+ + kind: MetallbIPAddressPoolNetBox... + +
+
+
+ + + + +
+
+
+ + owner reference + +
+
+
+
+ + owner reference + +
+
+
+ + + + + + + +
+
+
+
+ kind: IPAddressPool +
+
+ spec: +
+
+ addresses: 2.0.0.0/28 +
+
+
+
+
+
+ + kind: IPAddressPool... + +
+
+
+ + + + +
+
+
+ + read status and update spec + +
+
+
+
+ + read status and update spec + +
+
+
+ + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
diff --git a/docs/examples/example1-getting-started/sample-deployment.yaml b/docs/examples/example1-getting-started/sample-deployment.yaml index 6c7c63b1..135bbf53 100644 --- a/docs/examples/example1-getting-started/sample-deployment.yaml +++ b/docs/examples/example1-getting-started/sample-deployment.yaml @@ -1,10 +1,43 @@ -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolFromNetBoxParentPrefix +--- +apiVersion: v1 +kind: Namespace metadata: - name: podinfo-test + name: nginx +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-nginx + namespace: nginx +spec: + selector: + matchLabels: + run: my-nginx + replicas: 2 + template: + metadata: + labels: + run: my-nginx + spec: + containers: + - name: my-nginx + image: nginx + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: my-nginx + namespace: nginx + labels: + run: my-nginx + annotations: + metallb.universe.tf/address-pool: zurich-pool spec: - name: podinfo-test - tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value - preserveInNetbox: true - prefixLength: "/30" - parentPrefix: 3.0.0.64/26 \ No newline at end of file + type: LoadBalancer + ports: + - port: 80 + protocol: TCP + selector: + run: my-nginx diff --git a/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg b/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg index 8a424179..9e28233a 100644 --- a/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg +++ b/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg @@ -1,11 +1,11 @@ - + - + - + @@ -26,16 +26,16 @@ - + - + -
+
user namespace @@ -43,24 +43,24 @@
- + user namespace - - + + - + -
+
consumer @@ -68,14 +68,14 @@
- + consumer - + @@ -96,13 +96,13 @@ - + -
+
namespace netbox-operator @@ -110,21 +110,21 @@
- + namespace netbox-operator - - + + -
+
create @@ -132,21 +132,21 @@
- + create - - + + -
+
reconcile @@ -154,21 +154,21 @@
- + reconcile - - + + -
+
create/update/delete @@ -176,21 +176,21 @@
- + create/update/delete - - + + -
+
get available prefixes @@ -198,20 +198,20 @@
- + get available prefixes - + -
+
netbox-operator @@ -219,20 +219,20 @@
- + netbox-operator - + -
+
@@ -252,20 +252,20 @@
- + kind: PrefixClaim... - + -
+
Prefix 2.0.0.0/16 @@ -273,21 +273,21 @@
- + Prefix 2.0.0.0/16 - - + + -
+
User @@ -297,21 +297,21 @@
- + User... - - + + -
+
GitOps @@ -321,20 +321,20 @@
- + GitOps... - + -
+
and/or @@ -342,21 +342,21 @@
- + and/or - - + + -
+
reconcile @@ -364,21 +364,21 @@
- + reconcile - - + + -
+
ownerReference @@ -386,20 +386,20 @@
- + ownerReference - + -
+
@@ -418,20 +418,20 @@
- + kind: Prefix... - + -
+
Prefix 2.0.0.0/28 @@ -439,17 +439,17 @@
- + Prefix 2.0.0.0/28 - + - + diff --git a/docs/examples/example1-getting-started/simple_prefixclaim.yaml b/docs/examples/example1-getting-started/simple_prefixclaim.yaml index d76a5639..aeaa0f0f 100644 --- a/docs/examples/example1-getting-started/simple_prefixclaim.yaml +++ b/docs/examples/example1-getting-started/simple_prefixclaim.yaml @@ -7,6 +7,5 @@ metadata: name: simple-prefixclaim spec: tenant: "MY_TENANT" - preserveInNetbox: true - parentPrefix: 3.0.0.64/26 + parentPrefix: 3.0.0.64/25 prefixLength: "/30" diff --git a/docs/examples/example2-multicluster/README-ex2.md b/docs/examples/example2-multicluster/README-ex2.md new file mode 100644 index 00000000..6807aa27 --- /dev/null +++ b/docs/examples/example2-multicluster/README-ex2.md @@ -0,0 +1,9 @@ +# Example 2: Multi Cluster + +This example shows how to claim multiple prefixes from different clusters and make them available as metalLB ip address pools. + +1. Create ip address pools on the london cluster `kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml` +2. Create ip address pool on the zurich cluster `kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml` +3. Look up the created prefix claims and metalLB ipaddresspools `kubectl get --context kind-london pxc,ipaddresspools -A` and `kubectl get --context kind-zurich pxc,ipaddresspools -A` + +![Example 2](multicluster.drawio.svg) diff --git a/docs/examples/example2-multicluster/README.md b/docs/examples/example2-multicluster/README.md deleted file mode 100644 index cb7d651c..00000000 --- a/docs/examples/example2-multicluster/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Example 3: Multi Cluster - -1. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance1` -2. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance1 -n instance1` -3. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance1` -4. create the namespace where podinfo should be deployed `kubectl create --context kind-london ns instance2` -5. Install podinfo with with the kustomization and apply the instance of the resource graph definition to claim a prefix and create the MetalLB IPAddressPool `kubectl apply --context kind-london -k docs/examples/example4/instance2 -n instance2` -6. check if the frontend service got an external ip address assigned `kubectl get --context kind-london svc podinfo -n instance2` diff --git a/docs/examples/example2-multicluster/example1.drawio.svg b/docs/examples/example2-multicluster/example1.drawio.svg deleted file mode 100644 index 8a424179..00000000 --- a/docs/examples/example2-multicluster/example1.drawio.svg +++ /dev/null @@ -1,463 +0,0 @@ - - - - - - - - - - - - - -
-
-
- k8s cluster -
-
-
-
- - k8s cluster - -
-
-
- - - - - - - - - - -
-
-
- user namespace -
-
-
-
- - user namespace - -
-
-
- - - - - - - - - - - -
-
-
- consumer -
-
-
-
- - consumer - -
-
-
- - - - - - - -
-
-
- NetBox REST API -
-
-
-
- - NetBox REST API - -
-
-
- - - - - - - -
-
-
- namespace netbox-operator -
-
-
-
- - namespace netbox-operator - -
-
-
- - - - - - - - -
-
-
- create -
-
-
-
- - create - -
-
-
- - - - - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- create/update/delete -
-
-
-
- - create/update/delete - -
-
-
- - - - - - - - -
-
-
- get available prefixes -
-
-
-
- - get available prefixes - -
-
-
- - - - - - - -
-
-
- netbox-operator -
-
-
-
- - netbox-operator - -
-
-
- - - - - - - -
-
-
-
- kind: PrefixClaim -
-
- spec: -
-
- parentPrefix: 2.0.0.0/16 -
-
- prefixLength: /28 -
-
-
-
-
-
- - kind: PrefixClaim... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/16 -
-
-
-
- - Prefix 2.0.0.0/16 - -
-
-
- - - - - - - - -
-
-
- User -
- w/ kubectl -
-
-
-
- - User... - -
-
-
- - - - - - - - -
-
-
- GitOps -
- w/ Argo or Flux -
-
-
-
- - GitOps... - -
-
-
- - - - - - - -
-
-
- and/or -
-
-
-
- - and/or - -
-
-
- - - - - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- ownerReference -
-
-
-
- - ownerReference - -
-
-
- - - - - - - -
-
-
-
- kind: Prefix -
-
- spec: -
-
- prefix: 2.0.0.0/28 -
-
-
-
-
-
-
-
- - kind: Prefix... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/28 -
-
-
-
- - Prefix 2.0.0.0/28 - -
-
-
- - - - - - -
- - - - - Text is not SVG - cannot display - - - -
\ No newline at end of file diff --git a/docs/examples/example2-multicluster/instance1/kustomization.yaml b/docs/examples/example2-multicluster/instance1/kustomization.yaml deleted file mode 100644 index 7ce6dd7f..00000000 --- a/docs/examples/example2-multicluster/instance1/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -resources: - - ../../../../../podinfo/kustomize - - podinfo-int-ipaddresspool.yaml - -patches: - - patch: |- - apiVersion: v1 - kind: Service - metadata: - name: podinfo - annotations: - metallb.universe.tf/allow-shared-ip: podinfo1 - spec: - type: LoadBalancer diff --git a/docs/examples/example2-multicluster/instance1/podinfo-int-ipaddresspool.yaml b/docs/examples/example2-multicluster/instance1/podinfo-int-ipaddresspool.yaml deleted file mode 100644 index 5421eaa6..00000000 --- a/docs/examples/example2-multicluster/instance1/podinfo-int-ipaddresspool.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector -metadata: - name: podinfo1 -spec: - name: podinfo1 - tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value - site: "DM-Akron" # Use the `name` value instead of the `slug` value - description: "instance1" - comments: "podinfo in pss namespace" - preserveInNetbox: true - prefixLength: "/32" - parentPrefixSelector: - environment: prod diff --git a/docs/examples/example2-multicluster/instance2/kustomization.yaml b/docs/examples/example2-multicluster/instance2/kustomization.yaml deleted file mode 100644 index f71509bd..00000000 --- a/docs/examples/example2-multicluster/instance2/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -resources: - - ../../../../../podinfo/kustomize - - podinfo-int-ipaddresspool.yaml - -patches: - - patch: |- - apiVersion: v1 - kind: Service - metadata: - name: podinfo - annotations: - metallb.universe.tf/allow-shared-ip: podinfo2 - spec: - type: LoadBalancer diff --git a/docs/examples/example2-multicluster/instance2/podinfo-int-ipaddresspool.yaml b/docs/examples/example2-multicluster/instance2/podinfo-int-ipaddresspool.yaml deleted file mode 100644 index c11648d8..00000000 --- a/docs/examples/example2-multicluster/instance2/podinfo-int-ipaddresspool.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolFromNetBoxParentPrefixSelector -metadata: - name: podinfo2 -spec: - name: podinfo2 - tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value - site: "DM-Akron" # Use the `name` value instead of the `slug` value - description: "instance2" - comments: "podinfo in pss namespace" - preserveInNetbox: true - prefixLength: "/32" - parentPrefixSelector: - environment: prod diff --git a/docs/examples/example2-multicluster/london-pools.yaml b/docs/examples/example2-multicluster/london-pools.yaml new file mode 100644 index 00000000..a4f50714 --- /dev/null +++ b/docs/examples/example2-multicluster/london-pools.yaml @@ -0,0 +1,109 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-1 +spec: + name: london-network-1 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-2 +spec: + name: london-network-2 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-3 +spec: + name: london-network-3 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-4 +spec: + name: london-network-4 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-5 +spec: + name: london-network-5 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-6 +spec: + name: london-network-6 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-7 +spec: + name: london-network-7 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-8 +spec: + name: london-network-8 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-9 +spec: + name: london-network-9 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: london-network-10 +spec: + name: london-network-10 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod diff --git a/docs/examples/example2-multicluster/multicluster.drawio.svg b/docs/examples/example2-multicluster/multicluster.drawio.svg new file mode 100644 index 00000000..b1b15d6f --- /dev/null +++ b/docs/examples/example2-multicluster/multicluster.drawio.svg @@ -0,0 +1,774 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + + + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.0/16 +
+
+ prefixLength: /28 +
+
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.0/16 +
+
+ prefixLength: /28 +
+
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.16/28 +
+
+
+
+ + Prefix 2.0.0.16/28 + +
+
+
+ + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/example2-multicluster/zurich-pools.yaml b/docs/examples/example2-multicluster/zurich-pools.yaml new file mode 100644 index 00000000..e36852cd --- /dev/null +++ b/docs/examples/example2-multicluster/zurich-pools.yaml @@ -0,0 +1,109 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-1 +spec: + name: zurich-network-1 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-2 +spec: + name: zurich-network-2 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-3 +spec: + name: zurich-network-3 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-4 +spec: + name: zurich-network-4 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-5 +spec: + name: zurich-network-5 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-6 +spec: + name: zurich-network-6 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-7 +spec: + name: zurich-network-7 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-8 +spec: + name: zurich-network-8 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-9 +spec: + name: zurich-network-9 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-network-10 +spec: + name: zurich-network-10 + tenant: "MY_TENANT" + prefixLength: "/30" + parentPrefixSelector: + environment: prod diff --git a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml index f0aa7a92..e3cd955d 100644 --- a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml +++ b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml @@ -9,7 +9,6 @@ spec: spec: name: string tenant: string - preserveInNetbox: boolean prefixLength: string parentPrefixSelector: environment: string @@ -23,10 +22,9 @@ spec: apiVersion: netbox.dev/v1 kind: PrefixClaim metadata: - name: ${schema.spec.name} # Use the name provided by user + name: ${schema.spec.name} spec: tenant: ${schema.spec.tenant} - preserveInNetbox: ${schema.spec.preserveInNetbox} prefixLength: ${schema.spec.prefixLength} parentPrefixSelector: ${schema.spec.parentPrefixSelector} diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/set-up/prepare-demo-env.sh index ba5fe4d9..f5d31fe7 100755 --- a/docs/examples/set-up/prepare-demo-env.sh +++ b/docs/examples/set-up/prepare-demo-env.sh @@ -9,24 +9,19 @@ kubectl config use-context kind-london kubectl apply -f docs/examples/set-up/netbox-svc.yaml kubectl apply -f docs/examples/set-up/netbox-l2advertisement.yaml - # install NetBox Operator kubectl config use-context kind-london kind load docker-image netbox-operator:build-local --name london kind load docker-image netbox-operator:build-local --name london # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - -# install resource graph defintions -kubectl apply --context kind-london -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml kubectl config use-context kind-zurich kind load docker-image netbox-operator:build-local --name zurich kind load docker-image netbox-operator:build-local --name zurich # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - -# install resource graph defintions -kubectl apply -context kind-zurich -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml -DEPLOYMENT_NAME=netbox-operator +DEPLOYMENT_NAME=netbox-operator-controller-manager NAMESPACE=netbox-operator-system CONTEXT=kind-london From d0c4ef904560672d9616538df46be18715d067c1 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:57:17 +0100 Subject: [PATCH 11/28] add drawing of demo setup --- docs/examples/README.md | 2 + docs/examples/demo-setup.drawio.svg | 226 ++++++++++++++++++ .../example1-getting-started/README-ex1.md | 13 +- .../kustomization.yaml | 16 -- .../simple_prefixclaim.yaml | 2 +- .../example2-multicluster/london-pools.yaml | 55 ----- .../example2-multicluster/zurich-pools.yaml | 55 ----- docs/examples/set-up/cluster-cfg.yaml | 2 +- kind/local-env.sh | 2 +- 9 files changed, 238 insertions(+), 135 deletions(-) create mode 100644 docs/examples/demo-setup.drawio.svg delete mode 100644 docs/examples/example1-getting-started/kustomization.yaml diff --git a/docs/examples/README.md b/docs/examples/README.md index b79e888f..f34a3ff0 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -2,6 +2,8 @@ This folder shows some examples how the NetBox Operator can be used. The demo environment can be prepared with the 'docs/examples/set-up/prepare-demo-env.sh' script, which creates two kind clusters with NetBox Operator and [kro] installed. One one of the clusters a NetBox instance is installed which is available to both NetBox Operator deployments. +![Demo Set Up](demo-setup.drawio.svg) + [kro]: https://github.com/kro-run/kro/ Prerequisites: diff --git a/docs/examples/demo-setup.drawio.svg b/docs/examples/demo-setup.drawio.svg new file mode 100644 index 00000000..0c4fe028 --- /dev/null +++ b/docs/examples/demo-setup.drawio.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + +
+
+
+ Zurich Cluster +
+
+
+
+ + Zurich Cluster + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ London Cluster +
+
+
+
+ + London Cluster + +
+
+
+ + + + + + + +
+
+
+ NetBox Operator +
+
+
+
+ + NetBox Operator + +
+
+
+ + + + + + + +
+
+
+ NetBox Operator +
+
+
+
+ + NetBox Operator + +
+
+
+ + + + + + + +
+
+
+ + MetalLB + +
+ Load Balancer +
+
+
+
+
+ + MetalLB... + +
+
+
+ + + + + + + +
+
+
+ + Kubernetes Resource Orchestrator | kro + +
+
+
+
+ + Kubernetes Resource... + +
+
+
+ + + + + + + +
+
+
+ MetalLB +
+ Load Balancer +
+
+
+
+
+ + MetalLB... + +
+
+
+ + + + + + + +
+
+
+ Kubernetes Resource Orchestrator | kro +
+
+
+
+ + Kubernetes Resource... + +
+
+
+ + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/example1-getting-started/README-ex1.md b/docs/examples/example1-getting-started/README-ex1.md index 5361813f..9e02cb2c 100644 --- a/docs/examples/example1-getting-started/README-ex1.md +++ b/docs/examples/example1-getting-started/README-ex1.md @@ -4,9 +4,9 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. -1. Port-forward NetBox: `kubectl port-forward --context kind-london deploy/netbox 8080:8080 -n default` +1. Port-forward NetBox: `kubectl port-forward --context kind-london deploy/netbox 8080:8080` 2. Open in your favorite browser and log in with the username `admin` and password `admin` -3. Create a new prefix '3.0.0.64/25' with custom fields 'environment: prod' +3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' # 1.1 Claim a Prefix @@ -22,13 +22,14 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a pref ![Example 1.2](dynamic-prefixclaim.drawio.svg) -# 1.3 Claim a Prefix for a Podinfo Deployment and Create a MetalLB IPAddressPool +# 1.3 Claim a Prefix and Create a MetalLB IPAddressPool, create a depoyment which is exposed with a service using an ip from the claimed prefix This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. -1. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml` -2. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step. `kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml` -3. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc podinfo -n test` +1. Apply the kro resource graph definition, defining the mapping from the prefix claim to the metalLB ip address pool `kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml` +2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml` +3. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step. `kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml` +4. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc my-nginx -n nginx` ![Example 1.3](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/example1-getting-started/kustomization.yaml b/docs/examples/example1-getting-started/kustomization.yaml deleted file mode 100644 index 643b8416..00000000 --- a/docs/examples/example1-getting-started/kustomization.yaml +++ /dev/null @@ -1,16 +0,0 @@ -metadata: - namespace: test -resources: - - ../../../../podinfo/kustomize - - sample-deployment.yaml - -patches: - - patch: |- - apiVersion: v1 - kind: Service - metadata: - name: podinfo - annotations: - metallb.universe.tf/allow-shared-ip: podinfo-test - spec: - type: LoadBalancer diff --git a/docs/examples/example1-getting-started/simple_prefixclaim.yaml b/docs/examples/example1-getting-started/simple_prefixclaim.yaml index aeaa0f0f..483a0c3d 100644 --- a/docs/examples/example1-getting-started/simple_prefixclaim.yaml +++ b/docs/examples/example1-getting-started/simple_prefixclaim.yaml @@ -7,5 +7,5 @@ metadata: name: simple-prefixclaim spec: tenant: "MY_TENANT" - parentPrefix: 3.0.0.64/25 + parentPrefix: 3.0.0.64/26 prefixLength: "/30" diff --git a/docs/examples/example2-multicluster/london-pools.yaml b/docs/examples/example2-multicluster/london-pools.yaml index a4f50714..4012778c 100644 --- a/docs/examples/example2-multicluster/london-pools.yaml +++ b/docs/examples/example2-multicluster/london-pools.yaml @@ -52,58 +52,3 @@ spec: prefixLength: "/30" parentPrefixSelector: environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: london-network-6 -spec: - name: london-network-6 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: london-network-7 -spec: - name: london-network-7 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: london-network-8 -spec: - name: london-network-8 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: london-network-9 -spec: - name: london-network-9 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: london-network-10 -spec: - name: london-network-10 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod diff --git a/docs/examples/example2-multicluster/zurich-pools.yaml b/docs/examples/example2-multicluster/zurich-pools.yaml index e36852cd..a9cff12f 100644 --- a/docs/examples/example2-multicluster/zurich-pools.yaml +++ b/docs/examples/example2-multicluster/zurich-pools.yaml @@ -52,58 +52,3 @@ spec: prefixLength: "/30" parentPrefixSelector: environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: zurich-network-6 -spec: - name: zurich-network-6 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: zurich-network-7 -spec: - name: zurich-network-7 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: zurich-network-8 -spec: - name: zurich-network-8 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: zurich-network-9 -spec: - name: zurich-network-9 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: zurich-network-10 -spec: - name: zurich-network-10 - tenant: "MY_TENANT" - prefixLength: "/30" - parentPrefixSelector: - environment: prod diff --git a/docs/examples/set-up/cluster-cfg.yaml b/docs/examples/set-up/cluster-cfg.yaml index 5a4429ce..0dafb9be 100644 --- a/docs/examples/set-up/cluster-cfg.yaml +++ b/docs/examples/set-up/cluster-cfg.yaml @@ -11,4 +11,4 @@ nodes: kind: InitConfiguration nodeRegistration: kubeletExtraArgs: - node-labels: "ingress-ready=true" \ No newline at end of file + node-labels: "ingress-ready=true" diff --git a/kind/local-env.sh b/kind/local-env.sh index 795b53f7..3eb01e6d 100755 --- a/kind/local-env.sh +++ b/kind/local-env.sh @@ -39,4 +39,4 @@ fi kind create cluster || echo "cluster already exists, continuing..." kubectl wait --for=jsonpath='{.status.phase}'=Active --timeout=1s namespace/${NAMESPACE} -./kind/deploy-netbox.sh kind $VERSION $NAMESPACE \ No newline at end of file +./kind/deploy-netbox.sh kind $VERSION $NAMESPACE From 2da095e801d341a55f0f5c69b7f8903083c289f0 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Wed, 19 Mar 2025 14:42:02 +0100 Subject: [PATCH 12/28] Fix diagrams --- .../exhaustion-1-starting-point.drawio.svg | 2 +- .../exhaustion-2-prefix-exhausted.drawio.svg | 2 +- .../exhaustion-3-after-fix.drawio.svg | 2 +- docs/examples/example3-advanced-features/restore.drawio.svg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg index 4b388bd2..94e9ea20 100644 --- a/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg +++ b/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg @@ -1,4 +1,4 @@ -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 0%
Prefix 1.122.0.0/24...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
Text is not SVG - cannot display
\ No newline at end of file +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 0%
Prefix 1.122.0.0/24...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg index afdba0e3..a2377d28 100644 --- a/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg +++ b/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg @@ -1,4 +1,4 @@ -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
get available prefixes
get available prefixes
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
create/update/delete
create/update/delete
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
get available prefixes
get available prefixes
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
create/update/delete
create/update/delete
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg index 7a86fc54..13dd249e 100644 --- a/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg +++ b/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg @@ -1,4 +1,4 @@ -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/24
environment: prod
Usage: 50%
Prefix 1.100.0.0/24...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
create/update/delete
create/update/delete
get available prefixes
get available prefixes
Text is not SVG - cannot display
\ No newline at end of file +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/24
environment: prod
Usage: 50%
Prefix 1.100.0.0/24...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
create/update/delete
create/update/delete
get available prefixes
get available prefixes
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/restore.drawio.svg b/docs/examples/example3-advanced-features/restore.drawio.svg index bd406f83..3f51de0a 100644 --- a/docs/examples/example3-advanced-features/restore.drawio.svg +++ b/docs/examples/example3-advanced-features/restore.drawio.svg @@ -1,4 +1,4 @@ -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file From 97408bb9704918524fcf72319e9e17c1938cc45c Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:42:47 +0100 Subject: [PATCH 13/28] add curl pod --- .../example1-getting-started/README-ex1.md | 7 +-- .../metallb-ipaddresspool-netbox.drawio.svg | 38 ++++++------- .../example2-multicluster/README-ex2.md | 2 +- .../multicluster.drawio.svg | 54 ++++++++++--------- docs/examples/set-up/prepare-demo-env.sh | 3 ++ 5 files changed, 56 insertions(+), 48 deletions(-) diff --git a/docs/examples/example1-getting-started/README-ex1.md b/docs/examples/example1-getting-started/README-ex1.md index 9e02cb2c..b3ea2f9e 100644 --- a/docs/examples/example1-getting-started/README-ex1.md +++ b/docs/examples/example1-getting-started/README-ex1.md @@ -11,14 +11,14 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a pref # 1.1 Claim a Prefix 1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/simple_prefixclaim.yaml` -2. Check that the prefix claim CR got a prefix addigned `kubectl get --context kind-zurich pxc,px -w` +2. Check that the prefix claim CR got a prefix addigned `watch -n 1 kubectl get --context kind-zurich pxc,px` ![Example 1.1](simple_prefixclaim.drawio.svg) # 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector 1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/dynamic-prefix-claim.yaml` -2. Check if the frontend service got an external ip address assigned `kubectl get --context pxc,px -w` +2. Check that the prefix claim CR got a prefix addigned `watch -n 1 kubectl get --context kind-zurich pxc,px` ![Example 1.2](dynamic-prefixclaim.drawio.svg) @@ -29,7 +29,8 @@ This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. Th 1. Apply the kro resource graph definition, defining the mapping from the prefix claim to the metalLB ip address pool `kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml` 2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml` 3. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step. `kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml` -4. check if the frontend service got an external ip address assigned `kubectl get --context kind-zurich svc my-nginx -n nginx` +4. check if the prefixclaim and ipaddresspool got created `watch -n 1 kubectl get --context kind-zurich pxc,ipaddresspools my-nginx -A` +5. check if the service got an external ip address assigned `watch -n 1 kubectl get --context kind-zurich svc my-nginx -n nginx` ![Example 1.3](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg index 1eb84676..5d8997e1 100644 --- a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg +++ b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg @@ -1,4 +1,4 @@ - + @@ -117,14 +117,14 @@ - + -
+
create @@ -132,14 +132,14 @@
- + create - + @@ -168,7 +168,7 @@ -
+
create/update/delete @@ -176,7 +176,7 @@
- + create/update/delete @@ -190,7 +190,7 @@ -
+
get available prefixes @@ -198,7 +198,7 @@
- + get available prefixes @@ -359,14 +359,14 @@ - + -
+
@@ -376,7 +376,7 @@
- + reconcile @@ -517,7 +517,7 @@ -
+
@@ -567,7 +567,7 @@ -
+
@@ -587,7 +587,7 @@ -
+
@@ -597,7 +597,7 @@
- + create @@ -607,7 +607,7 @@ -
+
@@ -617,7 +617,7 @@
- + create @@ -627,7 +627,7 @@ -
+
create diff --git a/docs/examples/example2-multicluster/README-ex2.md b/docs/examples/example2-multicluster/README-ex2.md index 6807aa27..a3edcbed 100644 --- a/docs/examples/example2-multicluster/README-ex2.md +++ b/docs/examples/example2-multicluster/README-ex2.md @@ -4,6 +4,6 @@ This example shows how to claim multiple prefixes from different clusters and ma 1. Create ip address pools on the london cluster `kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml` 2. Create ip address pool on the zurich cluster `kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml` -3. Look up the created prefix claims and metalLB ipaddresspools `kubectl get --context kind-london pxc,ipaddresspools -A` and `kubectl get --context kind-zurich pxc,ipaddresspools -A` +3. Look up the created prefix claims and metalLB ipaddresspools `watch -n 1 kubectl get --context kind-london pxc,ipaddresspools -A` and `watch -n 1 kubectl get --context kind-zurich pxc,ipaddresspools -A` ![Example 2](multicluster.drawio.svg) diff --git a/docs/examples/example2-multicluster/multicluster.drawio.svg b/docs/examples/example2-multicluster/multicluster.drawio.svg index b1b15d6f..a32f6afc 100644 --- a/docs/examples/example2-multicluster/multicluster.drawio.svg +++ b/docs/examples/example2-multicluster/multicluster.drawio.svg @@ -1,4 +1,4 @@ - + @@ -53,6 +53,10 @@ + + + + @@ -132,7 +136,7 @@
- + create @@ -146,7 +150,7 @@ -
+
reconcile @@ -154,15 +158,15 @@
- + reconcile - - + + @@ -172,7 +176,7 @@ -
+
get available prefixes @@ -180,7 +184,7 @@
- + get available prefixes @@ -331,8 +335,8 @@ - - + + @@ -346,7 +350,7 @@
- + reconcile
@@ -360,7 +364,7 @@ -
+
ownerReference @@ -507,7 +511,7 @@ -
+
create @@ -529,7 +533,7 @@ -
+
reconcile @@ -537,7 +541,7 @@
- + reconcile @@ -555,7 +559,7 @@
- + create/update/delete
@@ -624,8 +628,8 @@
- - + + @@ -639,7 +643,7 @@
- + reconcile @@ -653,7 +657,7 @@ -
+
ownerReference @@ -661,7 +665,7 @@
- + ownerReference @@ -724,7 +728,7 @@ -
+
get available prefixes @@ -732,7 +736,7 @@
- + get available prefixes @@ -742,7 +746,7 @@ -
+
create/update/delete @@ -750,7 +754,7 @@
- + create/update/delete diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/set-up/prepare-demo-env.sh index f5d31fe7..dceb42cc 100755 --- a/docs/examples/set-up/prepare-demo-env.sh +++ b/docs/examples/set-up/prepare-demo-env.sh @@ -20,6 +20,9 @@ kubectl config use-context kind-zurich kind load docker-image netbox-operator:build-local --name zurich kind load docker-image netbox-operator:build-local --name zurich # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image kustomize build docs/examples/set-up/ | kubectl apply -f - +kind load docker-image curlimages/curl --name zurich +kind load docker-image curlimages/curl --name zurich +kubectl run curl --image curlimages/curl --image-pull-policy=Never -- sleep infinity DEPLOYMENT_NAME=netbox-operator-controller-manager NAMESPACE=netbox-operator-system From 97261602663f596928cb93cc45068ef968e5b3a3 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Thu, 20 Mar 2025 11:36:29 +0100 Subject: [PATCH 14/28] Add a note to test at scale --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 4ae46011..da7136b7 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ Example of assigning a Prefix using PrefixClaim: Key information can be found in the yaml formatted output of these resources, as well as in the events and Operator logs. +To test at scale, you can use yq to patch the Kubernetes manifests. The following is an example to create 100 IpAddressClaims based on the sample yaml file: + +```bash +for i in {001..100}; do + name="ipc-${i}" yq e '.metadata.name=strenv(name)' config/samples/netbox_v1_ipaddressclaim.yaml | kubectl apply -f - +done +``` + # Mixed usage of Prefixes Note that NetBox does handle the Address management of Prefixes separately from IP Ranges and IP Addresses. This is important to know when you plan to use the same NetBox Prefix as a parentPrefix for your IpAddressClaims, IpRangeClaims and PrefixClaims. From 1273ce80162ece5c1a078965be749e47ab8d561f Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Fri, 21 Mar 2025 19:16:17 +0100 Subject: [PATCH 15/28] reformat commands to example readmes --- .../example1-getting-started/README-ex1.md | 50 +++++++++++++++---- .../example2-multicluster/README-ex2.md | 19 +++++-- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/docs/examples/example1-getting-started/README-ex1.md b/docs/examples/example1-getting-started/README-ex1.md index b3ea2f9e..e0bb4ae7 100644 --- a/docs/examples/example1-getting-started/README-ex1.md +++ b/docs/examples/example1-getting-started/README-ex1.md @@ -4,21 +4,36 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. -1. Port-forward NetBox: `kubectl port-forward --context kind-london deploy/netbox 8080:8080` +1. Port-forward NetBox: +```bash +kubectl port-forward --context kind-london deploy/netbox 8080:8080 +``` 2. Open in your favorite browser and log in with the username `admin` and password `admin` 3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' # 1.1 Claim a Prefix -1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/simple_prefixclaim.yaml` -2. Check that the prefix claim CR got a prefix addigned `watch -n 1 kubectl get --context kind-zurich pxc,px` +1. Apply the manifest defining the prefix claim +```bash +kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/simple_prefixclaim.yaml +``` +2. Check that the prefix claim CR got a prefix addigned +```bash +watch kubectl get --context kind-zurich pxc,px +``` ![Example 1.1](simple_prefixclaim.drawio.svg) # 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector -1. Apply the manifest defining the prefix claim `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/dynamic-prefix-claim.yaml` -2. Check that the prefix claim CR got a prefix addigned `watch -n 1 kubectl get --context kind-zurich pxc,px` +1. Apply the manifest defining the prefix claim +```bash +kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/dynamic-prefix-claim.yaml +``` +2. Check that the prefix claim CR got a prefix addigned +```bash +watch kubectl get --context kind-zurich pxc,px +``` ![Example 1.2](dynamic-prefixclaim.drawio.svg) @@ -26,11 +41,26 @@ Before prefixes and ip addresses can be claimed with the NetBox operator, a pref This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. -1. Apply the kro resource graph definition, defining the mapping from the prefix claim to the metalLB ip address pool `kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml` -2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox `kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml` -3. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step. `kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml` -4. check if the prefixclaim and ipaddresspool got created `watch -n 1 kubectl get --context kind-zurich pxc,ipaddresspools my-nginx -A` -5. check if the service got an external ip address assigned `watch -n 1 kubectl get --context kind-zurich svc my-nginx -n nginx` +1. Apply the kro resource graph definition, defining the mapping from the prefix claim to the metalLB ip address pool +```bash +kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml +``` +2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox +```bash +kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml +``` +3. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step +```bash +kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml +``` +4. check if the prefixclaim and ipaddresspool got created +```bash +watch kubectl get --context kind-zurich pxc,ipaddresspools my-nginx -A +``` +5. check if the service got an external ip address assigned +```bash +watch kubectl get --context kind-zurich svc my-nginx -n nginx +``` ![Example 1.3](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/example2-multicluster/README-ex2.md b/docs/examples/example2-multicluster/README-ex2.md index a3edcbed..95d5f60b 100644 --- a/docs/examples/example2-multicluster/README-ex2.md +++ b/docs/examples/example2-multicluster/README-ex2.md @@ -2,8 +2,21 @@ This example shows how to claim multiple prefixes from different clusters and make them available as metalLB ip address pools. -1. Create ip address pools on the london cluster `kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml` -2. Create ip address pool on the zurich cluster `kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml` -3. Look up the created prefix claims and metalLB ipaddresspools `watch -n 1 kubectl get --context kind-london pxc,ipaddresspools -A` and `watch -n 1 kubectl get --context kind-zurich pxc,ipaddresspools -A` +1. Create ip address pools on the london cluster +```bash +kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml +``` +2. Create ip address pool on the zurich cluster +```bash +kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml +``` +3. Look up the created prefix claims and metalLB ipaddresspools +```bash +watch kubectl get --context kind-london pxc,ipaddresspools -A +``` +and +```bash +watch kubectl get --context kind-zurich pxc,ipaddresspools -A +``` ![Example 2](multicluster.drawio.svg) From 8f709709169eb8c9177a23478436c8f78bf2dbac Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 24 Mar 2025 08:46:01 +0100 Subject: [PATCH 16/28] Add new example structure --- .../new/example1-getting-started/README.md | 42 ++ .../prefixclaim-dynamic.drawio.svg | 469 +++++++++++++ .../prefixclaim-dynamic.yaml | 13 + .../prefixclaim-simple.drawio.svg | 463 +++++++++++++ .../prefixclaim-simple.yaml | 11 + docs/examples/new/example2-krm-glue/README.md | 9 + .../kro-rdg-poolfromnetbox.yaml | 11 + .../metallb-ipaddresspool-netbox.drawio.svg | 653 ++++++++++++++++++ .../example2-krm-glue/sample-deployment.yaml | 43 ++ .../new/example3-exhaustion/README.md | 67 ++ .../exhaustion-1-starting-point.drawio.svg | 4 + .../exhaustion-2-prefix-exhausted.drawio.svg | 4 + .../exhaustion-3-after-fix.drawio.svg | 4 + .../kro-rdg-poolfromnetbox.yaml | 39 ++ .../new/example4-restoration/README.md | 34 + .../kro-rdg-poolfromnetbox.yaml | 39 ++ .../example4-restoration/restore.drawio.svg | 4 + .../new/example5-multicluster/README.md | 8 + .../metallb-ip-address-pool-netbox.yaml | 1 + 19 files changed, 1918 insertions(+) create mode 100644 docs/examples/new/example1-getting-started/README.md create mode 100644 docs/examples/new/example1-getting-started/prefixclaim-dynamic.drawio.svg create mode 100644 docs/examples/new/example1-getting-started/prefixclaim-dynamic.yaml create mode 100644 docs/examples/new/example1-getting-started/prefixclaim-simple.drawio.svg create mode 100644 docs/examples/new/example1-getting-started/prefixclaim-simple.yaml create mode 100644 docs/examples/new/example2-krm-glue/README.md create mode 100644 docs/examples/new/example2-krm-glue/kro-rdg-poolfromnetbox.yaml create mode 100644 docs/examples/new/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg create mode 100644 docs/examples/new/example2-krm-glue/sample-deployment.yaml create mode 100644 docs/examples/new/example3-exhaustion/README.md create mode 100644 docs/examples/new/example3-exhaustion/exhaustion-1-starting-point.drawio.svg create mode 100644 docs/examples/new/example3-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg create mode 100644 docs/examples/new/example3-exhaustion/exhaustion-3-after-fix.drawio.svg create mode 100644 docs/examples/new/example3-exhaustion/kro-rdg-poolfromnetbox.yaml create mode 100644 docs/examples/new/example4-restoration/README.md create mode 100644 docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml create mode 100644 docs/examples/new/example4-restoration/restore.drawio.svg create mode 100644 docs/examples/new/example5-multicluster/README.md diff --git a/docs/examples/new/example1-getting-started/README.md b/docs/examples/new/example1-getting-started/README.md new file mode 100644 index 00000000..ea9a1970 --- /dev/null +++ b/docs/examples/new/example1-getting-started/README.md @@ -0,0 +1,42 @@ +# Example 1: Getting Started + +# 0. Ma![img.png](img.png)nually Create a Prefix in NetBox + +Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. + +1. Port-forward NetBox: +```bash +kubectl port-forward --context kind-london deploy/netbox 8080:8080 +``` +2. Open in your favorite browser and log in with the username `admin` and password `admin` +3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' + +# 1.1 Claim a Prefix + +In this example, we use a `.spec.parentPrefix` that we know in advance. This is useful if you already know exactly from which prefix you want to claim from. + +1. Apply the manifest defining the prefix claim +```bash +kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/prefixclaim-simple.yaml +``` +2. Check that the prefix claim CR got a prefix addigned +```bash +watch kubectl get --context kind-zurich pxc,px +``` + +![Example 1.1](prefixclaim-simple.drawio.svg) + +# 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector + +In this example, we use a `.spec.parentPrefixSelector`, which is a list of selectors that tell NetBox Operator from which parent prefixes to claim our Prefix from. + +1. Apply the manifest defining the prefix claim +```bash +kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/prefixclaim-dynamic.yaml +``` +2. Check that the prefix claim CR got a prefix addigned +```bash +watch kubectl get --context kind-zurich pxc,px +``` + +![Example 1.2](prefixclaim-dynamic.drawio.svg) diff --git a/docs/examples/new/example1-getting-started/prefixclaim-dynamic.drawio.svg b/docs/examples/new/example1-getting-started/prefixclaim-dynamic.drawio.svg new file mode 100644 index 00000000..920da03b --- /dev/null +++ b/docs/examples/new/example1-getting-started/prefixclaim-dynamic.drawio.svg @@ -0,0 +1,469 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ select matching parnet prefix +
+ and get available prefixes +
+
+
+
+
+ + select matching parnet prefix... + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefixSelector: +
+
+ environment: prod +
+
+ prefixLength: /28 +
+
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/new/example1-getting-started/prefixclaim-dynamic.yaml b/docs/examples/new/example1-getting-started/prefixclaim-dynamic.yaml new file mode 100644 index 00000000..4a6c0d13 --- /dev/null +++ b/docs/examples/new/example1-getting-started/prefixclaim-dynamic.yaml @@ -0,0 +1,13 @@ +apiVersion: netbox.dev/v1 +kind: PrefixClaim +metadata: + labels: + app.kubernetes.io/name: netbox-operator + app.kubernetes.io/managed-by: kustomize + name: dynamic-prefix-claim +spec: + tenant: "MY_TENANT" + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/30" diff --git a/docs/examples/new/example1-getting-started/prefixclaim-simple.drawio.svg b/docs/examples/new/example1-getting-started/prefixclaim-simple.drawio.svg new file mode 100644 index 00000000..9e28233a --- /dev/null +++ b/docs/examples/new/example1-getting-started/prefixclaim-simple.drawio.svg @@ -0,0 +1,463 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.0/16 +
+
+ prefixLength: /28 +
+
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/new/example1-getting-started/prefixclaim-simple.yaml b/docs/examples/new/example1-getting-started/prefixclaim-simple.yaml new file mode 100644 index 00000000..483a0c3d --- /dev/null +++ b/docs/examples/new/example1-getting-started/prefixclaim-simple.yaml @@ -0,0 +1,11 @@ +apiVersion: netbox.dev/v1 +kind: PrefixClaim +metadata: + labels: + app.kubernetes.io/name: netbox-operator + app.kubernetes.io/managed-by: kustomize + name: simple-prefixclaim +spec: + tenant: "MY_TENANT" + parentPrefix: 3.0.0.64/26 + prefixLength: "/30" diff --git a/docs/examples/new/example2-krm-glue/README.md b/docs/examples/new/example2-krm-glue/README.md new file mode 100644 index 00000000..25d6fd91 --- /dev/null +++ b/docs/examples/new/example2-krm-glue/README.md @@ -0,0 +1,9 @@ +# Example 2: Glue NetBox CRs to MetalLB CRs + +## Introduction + +So we have Prefixes represented as Kubernetes Resources. Now what can we do with this? + +We use kro.io to glue this to MetalLB IPAddressPools + +diagram and more instructions here... diff --git a/docs/examples/new/example2-krm-glue/kro-rdg-poolfromnetbox.yaml b/docs/examples/new/example2-krm-glue/kro-rdg-poolfromnetbox.yaml new file mode 100644 index 00000000..3dd1cf57 --- /dev/null +++ b/docs/examples/new/example2-krm-glue/kro-rdg-poolfromnetbox.yaml @@ -0,0 +1,11 @@ +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: zurich-pool +spec: + name: zurich-pool + tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value + prefixLength: "/30" + parentPrefixSelector: + environment: prod + family: IPv4 diff --git a/docs/examples/new/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg b/docs/examples/new/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg new file mode 100644 index 00000000..5d8997e1 --- /dev/null +++ b/docs/examples/new/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg @@ -0,0 +1,653 @@ + + + + + + + + + + + + + +
+
+
+ k8s cluster +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ user namespace +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ consumer +
+
+
+
+ + consumer + +
+
+
+ + + + + + + +
+
+
+ NetBox REST API +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ namespace netbox-operator +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ reconcile +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ create/update/delete +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ get available prefixes +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ netbox-operator +
+
+
+
+ + netbox-operator + +
+
+
+ + + + + + + + + + + +
+
+
+
+ kind: PrefixClaim +
+
+ spec: +
+
+ parentPrefix: 2.0.0.0/26 +
+
+ prefixLength: /28 +
+
+
+ status: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+ + kind: PrefixClaim... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/16 +
+
+
+
+ + Prefix 2.0.0.0/16 + +
+
+
+ + + + + + + + +
+
+
+ User +
+ w/ kubectl +
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ GitOps +
+ w/ Argo or Flux +
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ and/or +
+
+
+
+ + and/or + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ ownerReference +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ kind: Prefix +
+
+ spec: +
+
+ prefix: 2.0.0.0/28 +
+
+
+
+
+
+
+
+ + kind: Prefix... + +
+
+
+ + + + + + + +
+
+
+ Prefix 2.0.0.0/28 +
+
+
+
+ + Prefix 2.0.0.0/28 + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ kind: MetallbIPAddressPoolNetBox +
+
+ spec: +
+
+ name: my-pool +
+
+ parentPrefixSelector: +
+
+ environment: prod +
+
+ prefixLength: /28 +
+
+
+
+
+
+
+
+
+ + kind: MetallbIPAddressPoolNetBox... + +
+
+
+ + + + +
+
+
+ + owner reference + +
+
+
+
+ + owner reference + +
+
+
+ + + + + + + +
+
+
+
+ kind: IPAddressPool +
+
+ spec: +
+
+ addresses: 2.0.0.0/28 +
+
+
+
+
+
+ + kind: IPAddressPool... + +
+
+
+ + + + +
+
+
+ + read status and update spec + +
+
+
+
+ + read status and update spec + +
+
+
+ + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + +
+
+
+ create +
+
+
+
+ + create + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/new/example2-krm-glue/sample-deployment.yaml b/docs/examples/new/example2-krm-glue/sample-deployment.yaml new file mode 100644 index 00000000..135bbf53 --- /dev/null +++ b/docs/examples/new/example2-krm-glue/sample-deployment.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: nginx +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-nginx + namespace: nginx +spec: + selector: + matchLabels: + run: my-nginx + replicas: 2 + template: + metadata: + labels: + run: my-nginx + spec: + containers: + - name: my-nginx + image: nginx + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: my-nginx + namespace: nginx + labels: + run: my-nginx + annotations: + metallb.universe.tf/address-pool: zurich-pool +spec: + type: LoadBalancer + ports: + - port: 80 + protocol: TCP + selector: + run: my-nginx diff --git a/docs/examples/new/example3-exhaustion/README.md b/docs/examples/new/example3-exhaustion/README.md new file mode 100644 index 00000000..b4cb3b6f --- /dev/null +++ b/docs/examples/new/example3-exhaustion/README.md @@ -0,0 +1,67 @@ +# Example 3: Advanced Feature Prefix Exhaustion + +## Introduction + +NetBox Operator offers a few advanced features. In this example we showcase how NetBox Operator can recover from prefix exhaustion. + +When a Prefix is exhausted and this is fixed in the NetBox backend (e.g. by the Infrastructure team), NetBox Operator will automatically reconcile this. + +## Instructions + +![Figure 1: Starting Point](exhaustion-1-starting-point.drawio.svg) + +Create a /24 Prefix (e.g. 1.122.0.0/24) with Custom Field Environment set to "prod" in NetBox UI. + +Apply Resource and show PrefixClaims: + +```bash +kubectl --context kind-london create ns advanced +kubectl --context kind-london apply -f kro-rdg-poolfromnetbox.yaml +kubectl --context kind-london -n advanced get prefixclaims,prefixes +``` + +Note that only 2 out of the 3 PrefixClaims will become Ready. This is because the /24 Prefix is exhausted already after two Prefixes. This will look similar to this (note the order is non-deterministic): + +```bash +NAME PREFIX PREFIXASSIGNED READY AGE +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True True 2m2s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True True 2m2s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-3 False 2m2s + +NAME PREFIX READY ID URL AGE +prefix.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True 148 http://172.18.1.2/ipam/prefixes/148 2m2s +prefix.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True 149 http://172.18.1.2/ipam/prefixes/149 2m2s +``` + +![Figure 2: Parent Prefix Exhausted](exhaustion-2-prefix-exhausted.drawio.svg) + + +Create another /24 Prefix (e.g. 1.100.0.0/24) with Custom Field Environment set to "prod" in NetBox UI. + +Wait for the PrefixClaim to be reconciled again or trigger reconciliation by e.g. adding an annotation: + +```bash +kubectl --context kind-london -n advanced annotate prefixclaim prefixclaim-exhaustion-sample-3 reconcile="$(date)" --overwrite +``` + +Confirm that the third Prefix is now also assigned: + +```bash +kubectl --context kind-london -n advanced get prefixclaims,prefixes +``` + +Which should look as follows: + +```bash +NAME PREFIX PREFIXASSIGNED READY AGE +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True True 4s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True True 4s +prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-3 1.100.0.0/25 True True 4s + +NAME PREFIX READY ID URL AGE +prefix.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True 148 http://172.18.1.2/ipam/prefixes/148 4s +prefix.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True 149 http://172.18.1.2/ipam/prefixes/149 4s +prefix.netbox.dev/prefixclaim-exhaustion-sample-3 1.100.0.0/25 True 151 http://172.18.1.2/ipam/prefixes/151 3s``` +``` + +![Figure 3: Parent Prefix Exhaustion fixed](exhaustion-3-after-fix.drawio.svg) diff --git a/docs/examples/new/example3-exhaustion/exhaustion-1-starting-point.drawio.svg b/docs/examples/new/example3-exhaustion/exhaustion-1-starting-point.drawio.svg new file mode 100644 index 00000000..94e9ea20 --- /dev/null +++ b/docs/examples/new/example3-exhaustion/exhaustion-1-starting-point.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 0%
Prefix 1.122.0.0/24...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/new/example3-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg b/docs/examples/new/example3-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg new file mode 100644 index 00000000..a2377d28 --- /dev/null +++ b/docs/examples/new/example3-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
get available prefixes
get available prefixes
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
create/update/delete
create/update/delete
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/new/example3-exhaustion/exhaustion-3-after-fix.drawio.svg b/docs/examples/new/example3-exhaustion/exhaustion-3-after-fix.drawio.svg new file mode 100644 index 00000000..13dd249e --- /dev/null +++ b/docs/examples/new/example3-exhaustion/exhaustion-3-after-fix.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/24
environment: prod
Usage: 50%
Prefix 1.100.0.0/24...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
create/update/delete
create/update/delete
get available prefixes
get available prefixes
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/new/example3-exhaustion/kro-rdg-poolfromnetbox.yaml b/docs/examples/new/example3-exhaustion/kro-rdg-poolfromnetbox.yaml new file mode 100644 index 00000000..344407fb --- /dev/null +++ b/docs/examples/new/example3-exhaustion/kro-rdg-poolfromnetbox.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-exhaustion-sample-1 +spec: + name: prefixclaim-exhaustion-sample-1 + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/25" +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-exhaustion-sample-2 +spec: + name: prefixclaim-exhaustion-sample-2 + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/25" +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-exhaustion-sample-3 +spec: + name: prefixclaim-exhaustion-sample-3 + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/25" diff --git a/docs/examples/new/example4-restoration/README.md b/docs/examples/new/example4-restoration/README.md new file mode 100644 index 00000000..1797d511 --- /dev/null +++ b/docs/examples/new/example4-restoration/README.md @@ -0,0 +1,34 @@ +# Example 3: Advanced Feature Restoration + +## Introduction + +NetBox Operator offers a few advanced features. In this example we showcase how NetBox Operator can restore prefixes. This is especially useful when e.g. you need sticky IPs or Prefixes when redeploying an entire cluster. + +## Instructions + +![Figure 4: Restoration](restore.drawio.svg) + +Since we set `.spec.preserveInNetbox` to `true`, we can delete and restore the resources. To delete all reasources, delete the entire namespace: + +```bash +kubectl --context kind-london delete ns advanced +``` + +Make sure the resources are gone in Kubernetes: + +```bash +kubectl --context kind-london -n advanced get prefixclaims +``` + +Verify in the NetBox UI that the Prefixes still exist. + +Now apply the manifests again and verify they become ready. + +```bash +kubectl --context kind-london create ns advanced +kubectl --context kind-london apply -f kro-rdg-poolfromnetbox.yaml +kubectl --context kind-london -n advanced wait --for=condition=Ready prefixclaims --all +kubectl --context kind-london -n advanced get prefixclaims +``` + +Note that the assigned Prefixes are the same as before. You can also play around with this by just restoring single prefixes. If you're curious about how this is done, make sure to read [the "Restoration from NetBox" section in the main README.md](https://github.com/netbox-community/netbox-operator/tree/main?tab=readme-ov-file#restoration-from-netbox) and to check out the code. Also have a look at the "Netbox Restoration Hash" custom field in NetBox. diff --git a/docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml b/docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml new file mode 100644 index 00000000..344407fb --- /dev/null +++ b/docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-exhaustion-sample-1 +spec: + name: prefixclaim-exhaustion-sample-1 + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/25" +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-exhaustion-sample-2 +spec: + name: prefixclaim-exhaustion-sample-2 + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/25" +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-exhaustion-sample-3 +spec: + name: prefixclaim-exhaustion-sample-3 + tenant: "MY_TENANT" + preserveInNetbox: true + parentPrefixSelector: + environment: prod + family: IPv4 + prefixLength: "/25" diff --git a/docs/examples/new/example4-restoration/restore.drawio.svg b/docs/examples/new/example4-restoration/restore.drawio.svg new file mode 100644 index 00000000..3f51de0a --- /dev/null +++ b/docs/examples/new/example4-restoration/restore.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/new/example5-multicluster/README.md b/docs/examples/new/example5-multicluster/README.md new file mode 100644 index 00000000..0e9e0d89 --- /dev/null +++ b/docs/examples/new/example5-multicluster/README.md @@ -0,0 +1,8 @@ +# Example 4: Multi Cluster capabilities + +## Introduction + +NetBox Operator uses NetBox to avoid IP overlaps. This means that we can use NetBox Operator on multiple clusters. You can try this out using the example in this directory. + +diagram and more instructions here... +most of set-up directory would go here \ No newline at end of file diff --git a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml index e3cd955d..a2ea0fe6 100644 --- a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml +++ b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml @@ -27,6 +27,7 @@ spec: tenant: ${schema.spec.tenant} prefixLength: ${schema.spec.prefixLength} parentPrefixSelector: ${schema.spec.parentPrefixSelector} + preserveInNetbox: ${schema.spec.preserveInNetbox} - id: ipaddresspool template: From 75304128f7306eb2947254177e39036ba1e0fe57 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 24 Mar 2025 16:03:34 +0100 Subject: [PATCH 17/28] Fix example3 and example4 --- .../README.md | 25 ++++++++---- .../kro-rdg-poolfromnetbox.yaml | 39 +++++++++++++++++++ .../example3-restoration/restore.drawio.svg | 4 ++ .../README.md | 0 .../exhaustion-1-starting-point.drawio.svg | 0 .../exhaustion-2-prefix-exhausted.drawio.svg | 0 .../exhaustion-3-after-fix.drawio.svg | 0 .../kro-rdg-poolfromnetbox.yaml | 0 .../kro-rdg-poolfromnetbox.yaml | 39 ------------------- .../example4-restoration/restore.drawio.svg | 4 -- .../metallb-ip-address-pool-netbox.yaml | 2 + 11 files changed, 62 insertions(+), 51 deletions(-) rename docs/examples/new/{example4-restoration => example3-restoration}/README.md (52%) create mode 100644 docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml create mode 100644 docs/examples/new/example3-restoration/restore.drawio.svg rename docs/examples/new/{example3-exhaustion => example4-exhaustion}/README.md (100%) rename docs/examples/new/{example3-exhaustion => example4-exhaustion}/exhaustion-1-starting-point.drawio.svg (100%) rename docs/examples/new/{example3-exhaustion => example4-exhaustion}/exhaustion-2-prefix-exhausted.drawio.svg (100%) rename docs/examples/new/{example3-exhaustion => example4-exhaustion}/exhaustion-3-after-fix.drawio.svg (100%) rename docs/examples/new/{example3-exhaustion => example4-exhaustion}/kro-rdg-poolfromnetbox.yaml (100%) delete mode 100644 docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml delete mode 100644 docs/examples/new/example4-restoration/restore.drawio.svg diff --git a/docs/examples/new/example4-restoration/README.md b/docs/examples/new/example3-restoration/README.md similarity index 52% rename from docs/examples/new/example4-restoration/README.md rename to docs/examples/new/example3-restoration/README.md index 1797d511..73885207 100644 --- a/docs/examples/new/example4-restoration/README.md +++ b/docs/examples/new/example3-restoration/README.md @@ -1,23 +1,32 @@ -# Example 3: Advanced Feature Restoration +# Example 3: restoration Feature Restoration ## Introduction -NetBox Operator offers a few advanced features. In this example we showcase how NetBox Operator can restore prefixes. This is especially useful when e.g. you need sticky IPs or Prefixes when redeploying an entire cluster. +NetBox Operator offers a few restoration features. In this example we showcase how NetBox Operator can restore prefixes. This is especially useful when e.g. you need sticky IPs or Prefixes when redeploying an entire cluster. ## Instructions +First, let's create some resources we want to restore later. + +```bash +kubectl create ns restoration +kubectl -n restoration apply -f kro-rdg-poolfromnetbox.yaml +kubectl -n restoration wait --for=condition=Ready prefixclaims --all +kubectl -n restoration get prefixclaims +``` + ![Figure 4: Restoration](restore.drawio.svg) Since we set `.spec.preserveInNetbox` to `true`, we can delete and restore the resources. To delete all reasources, delete the entire namespace: ```bash -kubectl --context kind-london delete ns advanced +kubectl delete ns restoration ``` Make sure the resources are gone in Kubernetes: ```bash -kubectl --context kind-london -n advanced get prefixclaims +kubectl -n restoration get prefixclaims ``` Verify in the NetBox UI that the Prefixes still exist. @@ -25,10 +34,10 @@ Verify in the NetBox UI that the Prefixes still exist. Now apply the manifests again and verify they become ready. ```bash -kubectl --context kind-london create ns advanced -kubectl --context kind-london apply -f kro-rdg-poolfromnetbox.yaml -kubectl --context kind-london -n advanced wait --for=condition=Ready prefixclaims --all -kubectl --context kind-london -n advanced get prefixclaims +kubectl create ns restoration +kubectl -n restoration apply -f kro-rdg-poolfromnetbox.yaml +kubectl -n restoration wait --for=condition=Ready prefixclaims --all +kubectl -n restoration get prefixclaims ``` Note that the assigned Prefixes are the same as before. You can also play around with this by just restoring single prefixes. If you're curious about how this is done, make sure to read [the "Restoration from NetBox" section in the main README.md](https://github.com/netbox-community/netbox-operator/tree/main?tab=readme-ov-file#restoration-from-netbox) and to check out the code. Also have a look at the "Netbox Restoration Hash" custom field in NetBox. diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml new file mode 100644 index 00000000..1fa3b128 --- /dev/null +++ b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-restoration-sample-1 +spec: + name: prefixclaim-restoration-sample-1 + tenant: "Dunder-Mifflin, Inc." + preserveInNetbox: true + parentPrefixSelector: + tenant: "Dunder-Mifflin, Inc." + family: IPv4 + prefixLength: "/32" +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-restoration-sample-2 +spec: + name: prefixclaim-restoration-sample-2 + tenant: "Dunder-Mifflin, Inc." + preserveInNetbox: true + parentPrefixSelector: + tenant: "Dunder-Mifflin, Inc." + family: IPv4 + prefixLength: "/32" +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-restoration-sample-3 +spec: + name: prefixclaim-restoration-sample-3 + tenant: "Dunder-Mifflin, Inc." + preserveInNetbox: true + parentPrefixSelector: + tenant: "Dunder-Mifflin, Inc." + family: IPv4 + prefixLength: "/32" diff --git a/docs/examples/new/example3-restoration/restore.drawio.svg b/docs/examples/new/example3-restoration/restore.drawio.svg new file mode 100644 index 00000000..42b47a89 --- /dev/null +++ b/docs/examples/new/example3-restoration/restore.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.0/32
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 2.0.0.0/32
Prefix 2.0.0.0/32
Prefix 2.0.0.1/32
Prefix 2.0.0.1/32
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.1/32
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.2/32
kind: Prefix...
Prefix 2.0.0.2/32
Prefix 2.0.0.2/32
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/new/example3-exhaustion/README.md b/docs/examples/new/example4-exhaustion/README.md similarity index 100% rename from docs/examples/new/example3-exhaustion/README.md rename to docs/examples/new/example4-exhaustion/README.md diff --git a/docs/examples/new/example3-exhaustion/exhaustion-1-starting-point.drawio.svg b/docs/examples/new/example4-exhaustion/exhaustion-1-starting-point.drawio.svg similarity index 100% rename from docs/examples/new/example3-exhaustion/exhaustion-1-starting-point.drawio.svg rename to docs/examples/new/example4-exhaustion/exhaustion-1-starting-point.drawio.svg diff --git a/docs/examples/new/example3-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg b/docs/examples/new/example4-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg similarity index 100% rename from docs/examples/new/example3-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg rename to docs/examples/new/example4-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg diff --git a/docs/examples/new/example3-exhaustion/exhaustion-3-after-fix.drawio.svg b/docs/examples/new/example4-exhaustion/exhaustion-3-after-fix.drawio.svg similarity index 100% rename from docs/examples/new/example3-exhaustion/exhaustion-3-after-fix.drawio.svg rename to docs/examples/new/example4-exhaustion/exhaustion-3-after-fix.drawio.svg diff --git a/docs/examples/new/example3-exhaustion/kro-rdg-poolfromnetbox.yaml b/docs/examples/new/example4-exhaustion/kro-rdg-poolfromnetbox.yaml similarity index 100% rename from docs/examples/new/example3-exhaustion/kro-rdg-poolfromnetbox.yaml rename to docs/examples/new/example4-exhaustion/kro-rdg-poolfromnetbox.yaml diff --git a/docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml b/docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml deleted file mode 100644 index 344407fb..00000000 --- a/docs/examples/new/example4-restoration/kro-rdg-poolfromnetbox.yaml +++ /dev/null @@ -1,39 +0,0 @@ ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: prefixclaim-exhaustion-sample-1 -spec: - name: prefixclaim-exhaustion-sample-1 - tenant: "MY_TENANT" - preserveInNetbox: true - parentPrefixSelector: - environment: prod - family: IPv4 - prefixLength: "/25" ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: prefixclaim-exhaustion-sample-2 -spec: - name: prefixclaim-exhaustion-sample-2 - tenant: "MY_TENANT" - preserveInNetbox: true - parentPrefixSelector: - environment: prod - family: IPv4 - prefixLength: "/25" ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: prefixclaim-exhaustion-sample-3 -spec: - name: prefixclaim-exhaustion-sample-3 - tenant: "MY_TENANT" - preserveInNetbox: true - parentPrefixSelector: - environment: prod - family: IPv4 - prefixLength: "/25" diff --git a/docs/examples/new/example4-restoration/restore.drawio.svg b/docs/examples/new/example4-restoration/restore.drawio.svg deleted file mode 100644 index 3f51de0a..00000000 --- a/docs/examples/new/example4-restoration/restore.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml index a2ea0fe6..21e5ebaa 100644 --- a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml +++ b/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml @@ -10,7 +10,9 @@ spec: name: string tenant: string prefixLength: string + preserveInNetbox: boolean parentPrefixSelector: + tenant: string environment: string family: string status: From 936a0a52f690178450a845f9bef8c78dc35f8814 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 24 Mar 2025 16:35:44 +0100 Subject: [PATCH 18/28] Remove old advanced example --- .../example3-advanced-features/README.md | 97 ------------------- .../exhaustion-1-starting-point.drawio.svg | 4 - .../exhaustion-2-prefix-exhausted.drawio.svg | 4 - .../exhaustion-3-after-fix.drawio.svg | 4 - .../netbox_v1_prefixclaim.yaml | 44 --------- .../restore.drawio.svg | 4 - 6 files changed, 157 deletions(-) delete mode 100644 docs/examples/example3-advanced-features/README.md delete mode 100644 docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg delete mode 100644 docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg delete mode 100644 docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg delete mode 100644 docs/examples/example3-advanced-features/netbox_v1_prefixclaim.yaml delete mode 100644 docs/examples/example3-advanced-features/restore.drawio.svg diff --git a/docs/examples/example3-advanced-features/README.md b/docs/examples/example3-advanced-features/README.md deleted file mode 100644 index 8df1f659..00000000 --- a/docs/examples/example3-advanced-features/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# Example 3: Advanced Features - -## Description - -This demo showcases the two following cases: - -- Example 3a: Automatic Reconcilation in case of Prefix Exhaustion. When a Prefix is exhausted and this is fixed in the NetBox backend, NetBox Operator will automatically reconcile this. -- Example 3b: Restoration of Prefixes - -## Instructions - -### Example 3a: Prefix Exhaustion and Reconciliation - -![Figure 1: Starting Point](exhaustion-1-starting-point.drawio.svg) - -Create a /24 Prefix (e.g. 1.122.0.0/24) with Custom Field Environment set to "prod" in NetBox UI. - -Apply Resource and show PrefixClaims: - -```bash -kubectl --context kind-london apply -f netbox_v1_prefixclaim.yaml -kubectl --context kind-london -n advanced get prefixclaims,prefixes -``` - -Note that only 2 out of the 3 PrefixClaims will become Ready. This is because the /24 Prefix is exhausted already after two Prefixes. This will look similar to this (note the order is non-deterministic): - -```bash -NAME PREFIX PREFIXASSIGNED READY AGE -prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True True 2m2s -prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True True 2m2s -prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-3 False 2m2s - -NAME PREFIX READY ID URL AGE -prefix.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True 148 http://172.18.1.2/ipam/prefixes/148 2m2s -prefix.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True 149 http://172.18.1.2/ipam/prefixes/149 2m2s -``` - -![Figure 2: Parent Prefix Exhausted](exhaustion-2-prefix-exhausted.drawio.svg) - - -Create another /24 Prefix (e.g. 1.100.0.0/24) with Custom Field Environment set to "prod" in NetBox UI. - -Wait for the PrefixClaim to be reconciled again or trigger reconciliation by e.g. adding an annotation: - -```bash -kubectl --context kind-london -n advanced annotate prefixclaim prefixclaim-exhaustion-sample-3 reconcile="$(date)" --overwrite -``` - -Confirm that the third Prefix is now also assigned: - -```bash -kubectl --context kind-london -n advanced get prefixclaims,prefixes -``` - -Which should look as follows: - -```bash -NAME PREFIX PREFIXASSIGNED READY AGE -prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True True 4s -prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True True 4s -prefixclaim.netbox.dev/prefixclaim-exhaustion-sample-3 1.100.0.0/25 True True 4s - -NAME PREFIX READY ID URL AGE -prefix.netbox.dev/prefixclaim-exhaustion-sample-1 1.122.0.0/25 True 148 http://172.18.1.2/ipam/prefixes/148 4s -prefix.netbox.dev/prefixclaim-exhaustion-sample-2 1.122.0.128/25 True 149 http://172.18.1.2/ipam/prefixes/149 4s -prefix.netbox.dev/prefixclaim-exhaustion-sample-3 1.100.0.0/25 True 151 http://172.18.1.2/ipam/prefixes/151 3s``` -``` - -![Figure 3: Parent Prefix Exhaustion fixed](exhaustion-3-after-fix.drawio.svg) - -### Example 3b: Restoration - -![Figure 4: Restoration](restore.drawio.svg) - -Since we set `.spec.preserveInNetbox` to `true`, we can delete and restore the resources. To delete all reasources, delete the entire namespace: - -```bash -kubectl --context kind-london delete ns advanced -``` - -Make sure the resources are gone in Kubernetes: - -```bash -kubectl --context kind-london -n advanced get prefixclaims -``` - -Verify in the NetBox UI that the Prefixes still exist. - -Now apply the manifests again and verify they become ready. - -```bash -kubectl --context kind-london apply -f netbox_v1_prefixclaim.yaml -kubectl --context kind-london -n advanced wait --for=condition=Ready prefixclaims --all -kubectl --context kind-london -n advanced get prefixclaims -``` - -Note that the assigned Prefixes are the same as before. You can also play around with this by just restoring single prefixes. If you're curious about how this is done, make sure to read [the "Restoration from NetBox" section in the main README.md](https://github.com/netbox-community/netbox-operator/tree/main?tab=readme-ov-file#restoration-from-netbox) and to check out the code. Also have a look at the "Netbox Restoration Hash" custom field in NetBox. diff --git a/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg deleted file mode 100644 index 94e9ea20..00000000 --- a/docs/examples/example3-advanced-features/exhaustion-1-starting-point.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 0%
Prefix 1.122.0.0/24...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg deleted file mode 100644 index a2377d28..00000000 --- a/docs/examples/example3-advanced-features/exhaustion-2-prefix-exhausted.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
get available prefixes
get available prefixes
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
create/update/delete
create/update/delete
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg b/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg deleted file mode 100644 index 13dd249e..00000000 --- a/docs/examples/example3-advanced-features/exhaustion-3-after-fix.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/24
environment: prod
Usage: 100%
Prefix 1.122.0.0/24...
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/24
environment: prod
Usage: 50%
Prefix 1.100.0.0/24...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
create/update/delete
create/update/delete
get available prefixes
get available prefixes
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-advanced-features/netbox_v1_prefixclaim.yaml b/docs/examples/example3-advanced-features/netbox_v1_prefixclaim.yaml deleted file mode 100644 index 5233847b..00000000 --- a/docs/examples/example3-advanced-features/netbox_v1_prefixclaim.yaml +++ /dev/null @@ -1,44 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: advanced ---- -apiVersion: netbox.dev/v1 -kind: PrefixClaim -metadata: - name: prefixclaim-exhaustion-sample-1 - namespace: advanced -spec: - tenant: "MY_TENANT" - preserveInNetbox: true - parentPrefixSelector: - environment: prod - family: IPv4 - prefixLength: "/25" ---- -apiVersion: netbox.dev/v1 -kind: PrefixClaim -metadata: - name: prefixclaim-exhaustion-sample-2 - namespace: advanced -spec: - tenant: "MY_TENANT" - preserveInNetbox: true - parentPrefixSelector: - environment: prod - family: IPv4 - prefixLength: "/25" ---- -apiVersion: netbox.dev/v1 -kind: PrefixClaim -metadata: - name: prefixclaim-exhaustion-sample-3 - namespace: advanced -spec: - tenant: "MY_TENANT" - preserveInNetbox: true - parentPrefixSelector: - environment: prod - family: IPv4 - prefixLength: "/25" diff --git a/docs/examples/example3-advanced-features/restore.drawio.svg b/docs/examples/example3-advanced-features/restore.drawio.svg deleted file mode 100644 index 3f51de0a..00000000 --- a/docs/examples/example3-advanced-features/restore.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.0/25
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 1.122.0.0/25
Prefix 1.122.0.0/25
Prefix 1.122.0.128/25
Prefix 1.122.0.128/25
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.122.0.128/25
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    environment: prod  prefixLength: /25
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 1.100.0.0/25
kind: Prefix...
Prefix 1.100.0.0/25
Prefix 1.100.0.0/25
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file From e6a6779e738f2d448fcbf537148d8d80521f8d92 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 24 Mar 2025 17:20:34 +0100 Subject: [PATCH 19/28] Enhance restoration example --- .../new/example3-restoration/README.md | 32 +++++++++++---- .../kro-rdg-poolfromnetbox.yaml | 39 ------------------- .../kro-rdg-poolfromnetbox1.yaml | 13 +++++++ .../kro-rdg-poolfromnetbox2.yaml | 13 +++++++ .../kro-rdg-poolfromnetbox3.yaml | 13 +++++++ ...tore.drawio.svg => restoration.drawio.svg} | 0 6 files changed, 64 insertions(+), 46 deletions(-) delete mode 100644 docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml create mode 100644 docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox1.yaml create mode 100644 docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox2.yaml create mode 100644 docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox3.yaml rename docs/examples/new/example3-restoration/{restore.drawio.svg => restoration.drawio.svg} (100%) diff --git a/docs/examples/new/example3-restoration/README.md b/docs/examples/new/example3-restoration/README.md index 73885207..9fce7748 100644 --- a/docs/examples/new/example3-restoration/README.md +++ b/docs/examples/new/example3-restoration/README.md @@ -2,22 +2,31 @@ ## Introduction -NetBox Operator offers a few restoration features. In this example we showcase how NetBox Operator can restore prefixes. This is especially useful when e.g. you need sticky IPs or Prefixes when redeploying an entire cluster. +NetBox Operator offers a few restoration features. In this example we showcase how NetBox Operator can restoration prefixes. This is especially useful when e.g. you need sticky IPs or Prefixes when redeploying an entire cluster. ## Instructions -First, let's create some resources we want to restore later. +First, let's create some resources we want to restoration later. ```bash kubectl create ns restoration -kubectl -n restoration apply -f kro-rdg-poolfromnetbox.yaml +echo "\n\nCreating kro-rdg-poolfromnetbox1.yaml" +kubectl -n restoration apply -f kro-rdg-poolfromnetbox1.yaml +kubectl -n restoration wait --for=condition=Ready prefixclaims --all +kubectl -n restoration get prefixclaims +echo "\n\nCreating kro-rdg-poolfromnetbox2.yaml" +kubectl -n restoration apply -f kro-rdg-poolfromnetbox2.yaml +kubectl -n restoration wait --for=condition=Ready prefixclaims --all +kubectl -n restoration get prefixclaims +echo "\n\nCreating kro-rdg-poolfromnetbox3.yaml" +kubectl -n restoration apply -f kro-rdg-poolfromnetbox3.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims ``` -![Figure 4: Restoration](restore.drawio.svg) +![Figure 4: Restoration](restoration.drawio.svg) -Since we set `.spec.preserveInNetbox` to `true`, we can delete and restore the resources. To delete all reasources, delete the entire namespace: +Since we set `.spec.preserveInNetbox` to `true`, we can delete and restoration the resources. To delete all reasources, delete the entire namespace: ```bash kubectl delete ns restoration @@ -31,11 +40,20 @@ kubectl -n restoration get prefixclaims Verify in the NetBox UI that the Prefixes still exist. -Now apply the manifests again and verify they become ready. +Now apply the manifests again and verify they become ready. We apply the manifests in the reverse order to make sure the order does not matter ```bash kubectl create ns restoration -kubectl -n restoration apply -f kro-rdg-poolfromnetbox.yaml +echo "\n\nCreating kro-rdg-poolfromnetbox3.yaml" +kubectl -n restoration apply -f kro-rdg-poolfromnetbox3.yaml +kubectl -n restoration wait --for=condition=Ready prefixclaims --all +kubectl -n restoration get prefixclaims +echo "\n\nCreating kro-rdg-poolfromnetbox2.yaml" +kubectl -n restoration apply -f kro-rdg-poolfromnetbox2.yaml +kubectl -n restoration wait --for=condition=Ready prefixclaims --all +kubectl -n restoration get prefixclaims +echo "\n\nCreating kro-rdg-poolfromnetbox1.yaml" +kubectl -n restoration apply -f kro-rdg-poolfromnetbox1.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims ``` diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml deleted file mode 100644 index 1fa3b128..00000000 --- a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox.yaml +++ /dev/null @@ -1,39 +0,0 @@ ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: prefixclaim-restoration-sample-1 -spec: - name: prefixclaim-restoration-sample-1 - tenant: "Dunder-Mifflin, Inc." - preserveInNetbox: true - parentPrefixSelector: - tenant: "Dunder-Mifflin, Inc." - family: IPv4 - prefixLength: "/32" ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: prefixclaim-restoration-sample-2 -spec: - name: prefixclaim-restoration-sample-2 - tenant: "Dunder-Mifflin, Inc." - preserveInNetbox: true - parentPrefixSelector: - tenant: "Dunder-Mifflin, Inc." - family: IPv4 - prefixLength: "/32" ---- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: prefixclaim-restoration-sample-3 -spec: - name: prefixclaim-restoration-sample-3 - tenant: "Dunder-Mifflin, Inc." - preserveInNetbox: true - parentPrefixSelector: - tenant: "Dunder-Mifflin, Inc." - family: IPv4 - prefixLength: "/32" diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox1.yaml b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox1.yaml new file mode 100644 index 00000000..d468113b --- /dev/null +++ b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox1.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-restoration-sample-1 +spec: + name: prefixclaim-restoration-sample-1 + tenant: "Dunder-Mifflin, Inc." + preserveInNetbox: true + parentPrefixSelector: + tenant: "Dunder-Mifflin, Inc." + family: IPv4 + prefixLength: "/32" diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox2.yaml b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox2.yaml new file mode 100644 index 00000000..495cd187 --- /dev/null +++ b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox2.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-restoration-sample-2 +spec: + name: prefixclaim-restoration-sample-2 + tenant: "Dunder-Mifflin, Inc." + preserveInNetbox: true + parentPrefixSelector: + tenant: "Dunder-Mifflin, Inc." + family: IPv4 + prefixLength: "/32" diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox3.yaml b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox3.yaml new file mode 100644 index 00000000..2c995760 --- /dev/null +++ b/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox3.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: kro.run/v1alpha1 +kind: MetalLBIPAddressPoolNetBox +metadata: + name: prefixclaim-restoration-sample-3 +spec: + name: prefixclaim-restoration-sample-3 + tenant: "Dunder-Mifflin, Inc." + preserveInNetbox: true + parentPrefixSelector: + tenant: "Dunder-Mifflin, Inc." + family: IPv4 + prefixLength: "/32" diff --git a/docs/examples/new/example3-restoration/restore.drawio.svg b/docs/examples/new/example3-restoration/restoration.drawio.svg similarity index 100% rename from docs/examples/new/example3-restoration/restore.drawio.svg rename to docs/examples/new/example3-restoration/restoration.drawio.svg From 3df24580b170e5b51ffd16fbe848afa86e8ee8d1 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 24 Mar 2025 17:22:03 +0100 Subject: [PATCH 20/28] Fix exhaustion --- docs/examples/new/example4-exhaustion/README.md | 10 +++++----- docs/examples/new/example5-multicluster/README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/examples/new/example4-exhaustion/README.md b/docs/examples/new/example4-exhaustion/README.md index b4cb3b6f..8fa33687 100644 --- a/docs/examples/new/example4-exhaustion/README.md +++ b/docs/examples/new/example4-exhaustion/README.md @@ -15,9 +15,9 @@ Create a /24 Prefix (e.g. 1.122.0.0/24) with Custom Field Environment set to "pr Apply Resource and show PrefixClaims: ```bash -kubectl --context kind-london create ns advanced -kubectl --context kind-london apply -f kro-rdg-poolfromnetbox.yaml -kubectl --context kind-london -n advanced get prefixclaims,prefixes +kubectl create ns advanced +kubectl apply -f kro-rdg-poolfromnetbox.yaml +kubectl -n advanced get prefixclaims,prefixes ``` Note that only 2 out of the 3 PrefixClaims will become Ready. This is because the /24 Prefix is exhausted already after two Prefixes. This will look similar to this (note the order is non-deterministic): @@ -41,13 +41,13 @@ Create another /24 Prefix (e.g. 1.100.0.0/24) with Custom Field Environment set Wait for the PrefixClaim to be reconciled again or trigger reconciliation by e.g. adding an annotation: ```bash -kubectl --context kind-london -n advanced annotate prefixclaim prefixclaim-exhaustion-sample-3 reconcile="$(date)" --overwrite +kubectl -n advanced annotate prefixclaim prefixclaim-exhaustion-sample-3 reconcile="$(date)" --overwrite ``` Confirm that the third Prefix is now also assigned: ```bash -kubectl --context kind-london -n advanced get prefixclaims,prefixes +kubectl -n advanced get prefixclaims,prefixes ``` Which should look as follows: diff --git a/docs/examples/new/example5-multicluster/README.md b/docs/examples/new/example5-multicluster/README.md index 0e9e0d89..0e8086ce 100644 --- a/docs/examples/new/example5-multicluster/README.md +++ b/docs/examples/new/example5-multicluster/README.md @@ -1,4 +1,4 @@ -# Example 4: Multi Cluster capabilities +# Example 4: Advanced Feature Multi Cluster Support ## Introduction From 7f4fe1916882ac3bfb4f13a9cd4ad32ebbbc4a04 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Mon, 24 Mar 2025 17:37:33 +0100 Subject: [PATCH 21/28] Add drawio --- .../new/example3-restoration/restoration-simple.drawio.svg | 4 ++++ docs/examples/new/example3-restoration/restoration.drawio.svg | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 docs/examples/new/example3-restoration/restoration-simple.drawio.svg diff --git a/docs/examples/new/example3-restoration/restoration-simple.drawio.svg b/docs/examples/new/example3-restoration/restoration-simple.drawio.svg new file mode 100644 index 00000000..61ef35f1 --- /dev/null +++ b/docs/examples/new/example3-restoration/restoration-simple.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster
k8s cluster
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-op...
netbox-operator
netbox-operat...
create
create
User
w/ kubectl
Use...
NetBox REST API
NetBox REST API
Prefix 2.0.0.0/32
Prefix 2.0.0....
Prefix 2.0.0.1/32
Prefix 2.0.0....
Prefix 2.0.0.2/32
Prefix 2.0.0....
restore by looking up
restoration hash
restore by looking up...
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
PrefixClaim
PrefixClaim
IPAddressPool
IPAddressPool
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
PrefixClaim
PrefixClaim
IPAddressPool
IPAddressPool
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
PrefixClaim
PrefixClaim
IPAddressPool
IPAddressPool
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/new/example3-restoration/restoration.drawio.svg b/docs/examples/new/example3-restoration/restoration.drawio.svg index 42b47a89..0c4de4cf 100644 --- a/docs/examples/new/example3-restoration/restoration.drawio.svg +++ b/docs/examples/new/example3-restoration/restoration.drawio.svg @@ -1,4 +1,4 @@ -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
create & reconcile
create & reconcile
netbox-operator
netbox-operator
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
create
create
User
w/ kubectl
User...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.0/32
kind: Prefix...
NetBox REST API
NetBox REST API
Prefix 2.0.0.0/32
Prefix 2.0.0.0/32
Prefix 2.0.0.1/32
Prefix 2.0.0.1/32
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.1/32
kind: Prefix...
create & reconcile
create & reconcile
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
reconcile
reconcile
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.2/32
kind: Prefix...
Prefix 2.0.0.2/32
Prefix 2.0.0.2/32
restore by looking up
restoration hash
restore by looking up...
Text is not SVG - cannot display
\ No newline at end of file +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
netbox-operator
netbox-operator
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 2.0.0.0/32
Prefix 2.0.0.0/32
Prefix 2.0.0.1/32
Prefix 2.0.0.1/32
Prefix 2.0.0.2/32
Prefix 2.0.0.2/32
restore by looking up
restoration hash
restore by looking up...
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
status:
  prefix: 2.0.0.0/32
kind: PrefixClaim...
kind: IPAddressPool
spec:
  addresses
  - 2.0.0.0/32
kind: IPAddressPool...
kro mapping
kro mapping
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
status:
  prefix: 2.0.0.0/32
kind: PrefixClaim...
kind: IPAddressPool
spec:
  addresses
  - 2.0.0.0/32
kind: IPAddressPool...
kro mapping
kro mapping
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
status:
  prefix: 2.0.0.0/32
kind: PrefixClaim...
kind: IPAddressPool
spec:
  addresses
  - 2.0.0.0/32
kind: IPAddressPool...
kro mapping
kro mapping
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file From 1fbeafd70454e254ab1e261c8ae174a450a6cc1c Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Mon, 24 Mar 2025 15:01:59 +0100 Subject: [PATCH 22/28] add drawings with large font --- ...ipaddresspool-netbox-large-font.drawio.svg | 446 ++++++++++++++++ .../simple_prefixclaim-large-font.drawio.svg | 485 ++++++++++++++++++ 2 files changed, 931 insertions(+) create mode 100644 docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg create mode 100644 docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg diff --git a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg new file mode 100644 index 00000000..77e8163d --- /dev/null +++ b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg @@ -0,0 +1,446 @@ + + + + + + + + + + + + + +
+
+
+ + k8s cluster + +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ + user namespace + +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ + namespace metallb-ip-address-pool-netbox-operator + +
+
+
+
+ + namespace metallb-ip-address... + +
+
+
+ + + + + + + + + + + +
+
+
+ + metallb-ip-address-pool-netbox-operator + +
+
+
+
+ + metallb-ip-address-p... + +
+
+
+ + + + + + + +
+
+
+
+ + PrefixClaim CR + +
+
+ + status + + + : + +
+
+ + prefix: + + 2.0.0.0/28 + + +
+
+
+
+
+ + PrefixClaim CR... + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + + + +
+
+
+
+ + MetalLBIPAddress- + +
+
+ + PoolNetBox CR + +
+
+
+
+
+ + MetalLBIPAddress-... + +
+
+
+ + + + + + + +
+
+
+ + namespace metallb-system + +
+
+
+
+ + namespace metallb-system + +
+
+
+ + + + + + + +
+
+
+
+ + IPAddressPool CR + +
+
+ + spec: + +
+
+ + ipaddresspools: + +
+
+ + - + + 2.0.0.0/28 + + +
+
+
+
+
+ + IPAddressPool CR... + +
+
+
+ + + + +
+
+
+ + read status + +
+
+
+
+ + read status + +
+
+
+ + + + + + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ + consumer + +
+
+
+
+ + consumer + +
+
+
+ + + + + + + + +
+
+
+ + User +
+ w/ kubectl +
+
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ + GitOps +
+ w/ Argo or Flux +
+
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ + and/or + +
+
+
+
+ + and/or + +
+
+
+ + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg b/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg new file mode 100644 index 00000000..94742445 --- /dev/null +++ b/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg @@ -0,0 +1,485 @@ + + + + + + + + + + + + + +
+
+
+ + k8s cluster + +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ + user namespace + +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ + NetBox REST API + +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ + namespace netbox-operator + +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ + create/update/delete + +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ + get available prefixes + +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + +
+
+
+ + NetBox Operator + +
+
+
+
+ + NetBox Operator + +
+
+
+ + + + + + + +
+
+
+
+ + PrefixClaim CR + +
+
+
+
+
+ + PrefixClaim CR + +
+
+
+ + + + + + + +
+
+
+ + Parent +
+ Prefix +
+
+
+
+
+ + Parent... + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ + ownerReference + +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ + Prefix CR + +
+
+
+
+
+ + Prefix CR + +
+
+
+ + + + + + + +
+
+
+ + Claimed Prefix + +
+
+
+
+ + Claimed Prefix + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + consumer + +
+
+
+
+ + consumer + +
+
+
+ + + + + + + + +
+
+
+ + User +
+ w/ kubectl +
+
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ + GitOps +
+ w/ Argo or Flux +
+
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ + and/or + +
+
+
+
+ + and/or + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file From d8b63c26e6620ba3fe5f8cb432371e849393c1d0 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:42:33 +0100 Subject: [PATCH 23/28] move files to refactored example structure --- .../example1-getting-started/README-ex1.md | 66 -- .../dynamic-prefix-claim.yaml | 13 - .../dynamic-prefixclaim.drawio.svg | 469 ------------- .../ip-address-pool.yaml | 11 - ...ipaddresspool-netbox-large-font.drawio.svg | 446 ------------ .../metallb-ipaddresspool-netbox.drawio.svg | 653 ------------------ .../sample-deployment.yaml | 43 -- .../simple_prefixclaim.drawio.svg | 463 ------------- .../simple_prefixclaim.yaml | 11 - .../example2-multicluster/README-ex2.md | 22 - docs/examples/new/example2-krm-glue/README.md | 24 +- .../simple_prefixclaim-large-font.drawio.svg | 0 .../new/example5-multicluster/README-ex2.md | 1 + .../new/example5-multicluster/README.md | 22 +- .../example5-multicluster}/london-pools.yaml | 0 .../multicluster.drawio.svg | 0 .../example5-multicluster}/zurich-pools.yaml | 0 17 files changed, 44 insertions(+), 2200 deletions(-) delete mode 100644 docs/examples/example1-getting-started/README-ex1.md delete mode 100644 docs/examples/example1-getting-started/dynamic-prefix-claim.yaml delete mode 100644 docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg delete mode 100644 docs/examples/example1-getting-started/ip-address-pool.yaml delete mode 100644 docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg delete mode 100644 docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg delete mode 100644 docs/examples/example1-getting-started/sample-deployment.yaml delete mode 100644 docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg delete mode 100644 docs/examples/example1-getting-started/simple_prefixclaim.yaml delete mode 100644 docs/examples/example2-multicluster/README-ex2.md rename docs/examples/{example1-getting-started => new/example2-krm-glue}/simple_prefixclaim-large-font.drawio.svg (100%) create mode 100644 docs/examples/new/example5-multicluster/README-ex2.md rename docs/examples/{example2-multicluster => new/example5-multicluster}/london-pools.yaml (100%) rename docs/examples/{example2-multicluster => new/example5-multicluster}/multicluster.drawio.svg (100%) rename docs/examples/{example2-multicluster => new/example5-multicluster}/zurich-pools.yaml (100%) diff --git a/docs/examples/example1-getting-started/README-ex1.md b/docs/examples/example1-getting-started/README-ex1.md deleted file mode 100644 index e0bb4ae7..00000000 --- a/docs/examples/example1-getting-started/README-ex1.md +++ /dev/null @@ -1,66 +0,0 @@ -# Example 1: Getting Started - -# 0. Manually Create a Prefix in NetBox - -Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. - -1. Port-forward NetBox: -```bash -kubectl port-forward --context kind-london deploy/netbox 8080:8080 -``` -2. Open in your favorite browser and log in with the username `admin` and password `admin` -3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' - -# 1.1 Claim a Prefix - -1. Apply the manifest defining the prefix claim -```bash -kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/simple_prefixclaim.yaml -``` -2. Check that the prefix claim CR got a prefix addigned -```bash -watch kubectl get --context kind-zurich pxc,px -``` - -![Example 1.1](simple_prefixclaim.drawio.svg) - -# 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector - -1. Apply the manifest defining the prefix claim -```bash -kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/dynamic-prefix-claim.yaml -``` -2. Check that the prefix claim CR got a prefix addigned -```bash -watch kubectl get --context kind-zurich pxc,px -``` - -![Example 1.2](dynamic-prefixclaim.drawio.svg) - -# 1.3 Claim a Prefix and Create a MetalLB IPAddressPool, create a depoyment which is exposed with a service using an ip from the claimed prefix - -This example uses [kro] to map the claimed prefix to a MetalLB IPAddressPool. The required resource graph definitions and kro were installed with the set-up script. - -1. Apply the kro resource graph definition, defining the mapping from the prefix claim to the metalLB ip address pool -```bash -kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml -``` -2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox -```bash -kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml -``` -3. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step -```bash -kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml -``` -4. check if the prefixclaim and ipaddresspool got created -```bash -watch kubectl get --context kind-zurich pxc,ipaddresspools my-nginx -A -``` -5. check if the service got an external ip address assigned -```bash -watch kubectl get --context kind-zurich svc my-nginx -n nginx -``` - - -![Example 1.3](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml b/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml deleted file mode 100644 index 4a6c0d13..00000000 --- a/docs/examples/example1-getting-started/dynamic-prefix-claim.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: netbox.dev/v1 -kind: PrefixClaim -metadata: - labels: - app.kubernetes.io/name: netbox-operator - app.kubernetes.io/managed-by: kustomize - name: dynamic-prefix-claim -spec: - tenant: "MY_TENANT" - parentPrefixSelector: - environment: prod - family: IPv4 - prefixLength: "/30" diff --git a/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg b/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg deleted file mode 100644 index 920da03b..00000000 --- a/docs/examples/example1-getting-started/dynamic-prefixclaim.drawio.svg +++ /dev/null @@ -1,469 +0,0 @@ - - - - - - - - - - - - - -
-
-
- k8s cluster -
-
-
-
- - k8s cluster - -
-
-
- - - - - - - - - - -
-
-
- user namespace -
-
-
-
- - user namespace - -
-
-
- - - - - - - - - - - -
-
-
- consumer -
-
-
-
- - consumer - -
-
-
- - - - - - - -
-
-
- NetBox REST API -
-
-
-
- - NetBox REST API - -
-
-
- - - - - - - -
-
-
- namespace netbox-operator -
-
-
-
- - namespace netbox-operator - -
-
-
- - - - - - - - -
-
-
- create -
-
-
-
- - create - -
-
-
- - - - - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- create/update/delete -
-
-
-
- - create/update/delete - -
-
-
- - - - - - - - -
-
-
- select matching parnet prefix -
- and get available prefixes -
-
-
-
-
- - select matching parnet prefix... - -
-
-
- - - - - - - -
-
-
- netbox-operator -
-
-
-
- - netbox-operator - -
-
-
- - - - - - - -
-
-
-
- kind: PrefixClaim -
-
- spec: -
-
- parentPrefixSelector: -
-
- environment: prod -
-
- prefixLength: /28 -
-
-
-
-
-
- - kind: PrefixClaim... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/16 -
-
-
-
- - Prefix 2.0.0.0/16 - -
-
-
- - - - - - - - -
-
-
- User -
- w/ kubectl -
-
-
-
- - User... - -
-
-
- - - - - - - - -
-
-
- GitOps -
- w/ Argo or Flux -
-
-
-
- - GitOps... - -
-
-
- - - - - - - -
-
-
- and/or -
-
-
-
- - and/or - -
-
-
- - - - - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- ownerReference -
-
-
-
- - ownerReference - -
-
-
- - - - - - - -
-
-
-
- kind: Prefix -
-
- spec: -
-
- prefix: 2.0.0.0/28 -
-
-
-
-
-
-
-
- - kind: Prefix... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/28 -
-
-
-
- - Prefix 2.0.0.0/28 - -
-
-
- - - - - - -
- - - - - Text is not SVG - cannot display - - - -
\ No newline at end of file diff --git a/docs/examples/example1-getting-started/ip-address-pool.yaml b/docs/examples/example1-getting-started/ip-address-pool.yaml deleted file mode 100644 index 3dd1cf57..00000000 --- a/docs/examples/example1-getting-started/ip-address-pool.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox -metadata: - name: zurich-pool -spec: - name: zurich-pool - tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value - prefixLength: "/30" - parentPrefixSelector: - environment: prod - family: IPv4 diff --git a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg deleted file mode 100644 index 77e8163d..00000000 --- a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox-large-font.drawio.svg +++ /dev/null @@ -1,446 +0,0 @@ - - - - - - - - - - - - - -
-
-
- - k8s cluster - -
-
-
-
- - k8s cluster - -
-
-
- - - - - - - - - - -
-
-
- - user namespace - -
-
-
-
- - user namespace - -
-
-
- - - - - - - - - - - -
-
-
- - namespace metallb-ip-address-pool-netbox-operator - -
-
-
-
- - namespace metallb-ip-address... - -
-
-
- - - - - - - - - - - -
-
-
- - metallb-ip-address-pool-netbox-operator - -
-
-
-
- - metallb-ip-address-p... - -
-
-
- - - - - - - -
-
-
-
- - PrefixClaim CR - -
-
- - status - - - : - -
-
- - prefix: - - 2.0.0.0/28 - - -
-
-
-
-
- - PrefixClaim CR... - -
-
-
- - - - - - - - -
-
-
- - reconcile - -
-
-
-
- - reconcile - -
-
-
- - - - - - - - - - -
-
-
-
- - MetalLBIPAddress- - -
-
- - PoolNetBox CR - -
-
-
-
-
- - MetalLBIPAddress-... - -
-
-
- - - - - - - -
-
-
- - namespace metallb-system - -
-
-
-
- - namespace metallb-system - -
-
-
- - - - - - - -
-
-
-
- - IPAddressPool CR - -
-
- - spec: - -
-
- - ipaddresspools: - -
-
- - - - - 2.0.0.0/28 - - -
-
-
-
-
- - IPAddressPool CR... - -
-
-
- - - - -
-
-
- - read status - -
-
-
-
- - read status - -
-
-
- - - - - - - - -
-
-
- - create - -
-
-
-
- - create - -
-
-
- - - - - - - - -
-
-
- - consumer - -
-
-
-
- - consumer - -
-
-
- - - - - - - - -
-
-
- - User -
- w/ kubectl -
-
-
-
-
- - User... - -
-
-
- - - - - - - - -
-
-
- - GitOps -
- w/ Argo or Flux -
-
-
-
-
- - GitOps... - -
-
-
- - - - - - - -
-
-
- - and/or - -
-
-
-
- - and/or - -
-
-
- - - - -
-
-
- - create - -
-
-
-
- - create - -
-
-
-
- - - - - Text is not SVG - cannot display - - - -
\ No newline at end of file diff --git a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg b/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg deleted file mode 100644 index 5d8997e1..00000000 --- a/docs/examples/example1-getting-started/metallb-ipaddresspool-netbox.drawio.svg +++ /dev/null @@ -1,653 +0,0 @@ - - - - - - - - - - - - - -
-
-
- k8s cluster -
-
-
-
- - k8s cluster - -
-
-
- - - - - - - - - - -
-
-
- user namespace -
-
-
-
- - user namespace - -
-
-
- - - - - - - - - - - -
-
-
- consumer -
-
-
-
- - consumer - -
-
-
- - - - - - - -
-
-
- NetBox REST API -
-
-
-
- - NetBox REST API - -
-
-
- - - - - - - -
-
-
- namespace netbox-operator -
-
-
-
- - namespace netbox-operator - -
-
-
- - - - - - - - -
-
-
- create -
-
-
-
- - create - -
-
-
- - - - - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- create/update/delete -
-
-
-
- - create/update/delete - -
-
-
- - - - - - - - -
-
-
- get available prefixes -
-
-
-
- - get available prefixes - -
-
-
- - - - - - - -
-
-
- netbox-operator -
-
-
-
- - netbox-operator - -
-
-
- - - - - - - - - - - -
-
-
-
- kind: PrefixClaim -
-
- spec: -
-
- parentPrefix: 2.0.0.0/26 -
-
- prefixLength: /28 -
-
-
- status: -
-
- prefix: 2.0.0.0/28 -
-
-
-
-
- - kind: PrefixClaim... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/16 -
-
-
-
- - Prefix 2.0.0.0/16 - -
-
-
- - - - - - - - -
-
-
- User -
- w/ kubectl -
-
-
-
- - User... - -
-
-
- - - - - - - - -
-
-
- GitOps -
- w/ Argo or Flux -
-
-
-
- - GitOps... - -
-
-
- - - - - - - -
-
-
- and/or -
-
-
-
- - and/or - -
-
-
- - - - - - - - -
-
-
- - reconcile - -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- ownerReference -
-
-
-
- - ownerReference - -
-
-
- - - - - - - -
-
-
-
- kind: Prefix -
-
- spec: -
-
- prefix: 2.0.0.0/28 -
-
-
-
-
-
-
-
- - kind: Prefix... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/28 -
-
-
-
- - Prefix 2.0.0.0/28 - -
-
-
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
- kind: MetallbIPAddressPoolNetBox -
-
- spec: -
-
- name: my-pool -
-
- parentPrefixSelector: -
-
- environment: prod -
-
- prefixLength: /28 -
-
-
-
-
-
-
-
-
- - kind: MetallbIPAddressPoolNetBox... - -
-
-
- - - - -
-
-
- - owner reference - -
-
-
-
- - owner reference - -
-
-
- - - - - - - -
-
-
-
- kind: IPAddressPool -
-
- spec: -
-
- addresses: 2.0.0.0/28 -
-
-
-
-
-
- - kind: IPAddressPool... - -
-
-
- - - - -
-
-
- - read status and update spec - -
-
-
-
- - read status and update spec - -
-
-
- - - - -
-
-
- - create - -
-
-
-
- - create - -
-
-
- - - - -
-
-
- - create - -
-
-
-
- - create - -
-
-
- - - - -
-
-
- create -
-
-
-
- - create - -
-
-
-
- - - - - Text is not SVG - cannot display - - - -
\ No newline at end of file diff --git a/docs/examples/example1-getting-started/sample-deployment.yaml b/docs/examples/example1-getting-started/sample-deployment.yaml deleted file mode 100644 index 135bbf53..00000000 --- a/docs/examples/example1-getting-started/sample-deployment.yaml +++ /dev/null @@ -1,43 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: nginx ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: my-nginx - namespace: nginx -spec: - selector: - matchLabels: - run: my-nginx - replicas: 2 - template: - metadata: - labels: - run: my-nginx - spec: - containers: - - name: my-nginx - image: nginx - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: my-nginx - namespace: nginx - labels: - run: my-nginx - annotations: - metallb.universe.tf/address-pool: zurich-pool -spec: - type: LoadBalancer - ports: - - port: 80 - protocol: TCP - selector: - run: my-nginx diff --git a/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg b/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg deleted file mode 100644 index 9e28233a..00000000 --- a/docs/examples/example1-getting-started/simple_prefixclaim.drawio.svg +++ /dev/null @@ -1,463 +0,0 @@ - - - - - - - - - - - - - -
-
-
- k8s cluster -
-
-
-
- - k8s cluster - -
-
-
- - - - - - - - - - -
-
-
- user namespace -
-
-
-
- - user namespace - -
-
-
- - - - - - - - - - - -
-
-
- consumer -
-
-
-
- - consumer - -
-
-
- - - - - - - -
-
-
- NetBox REST API -
-
-
-
- - NetBox REST API - -
-
-
- - - - - - - -
-
-
- namespace netbox-operator -
-
-
-
- - namespace netbox-operator - -
-
-
- - - - - - - - -
-
-
- create -
-
-
-
- - create - -
-
-
- - - - - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- create/update/delete -
-
-
-
- - create/update/delete - -
-
-
- - - - - - - - -
-
-
- get available prefixes -
-
-
-
- - get available prefixes - -
-
-
- - - - - - - -
-
-
- netbox-operator -
-
-
-
- - netbox-operator - -
-
-
- - - - - - - -
-
-
-
- kind: PrefixClaim -
-
- spec: -
-
- parentPrefix: 2.0.0.0/16 -
-
- prefixLength: /28 -
-
-
-
-
-
- - kind: PrefixClaim... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/16 -
-
-
-
- - Prefix 2.0.0.0/16 - -
-
-
- - - - - - - - -
-
-
- User -
- w/ kubectl -
-
-
-
- - User... - -
-
-
- - - - - - - - -
-
-
- GitOps -
- w/ Argo or Flux -
-
-
-
- - GitOps... - -
-
-
- - - - - - - -
-
-
- and/or -
-
-
-
- - and/or - -
-
-
- - - - - - - - -
-
-
- reconcile -
-
-
-
- - reconcile - -
-
-
- - - - - - - - -
-
-
- ownerReference -
-
-
-
- - ownerReference - -
-
-
- - - - - - - -
-
-
-
- kind: Prefix -
-
- spec: -
-
- prefix: 2.0.0.0/28 -
-
-
-
-
-
-
-
- - kind: Prefix... - -
-
-
- - - - - - - -
-
-
- Prefix 2.0.0.0/28 -
-
-
-
- - Prefix 2.0.0.0/28 - -
-
-
- - - - - - -
- - - - - Text is not SVG - cannot display - - - -
\ No newline at end of file diff --git a/docs/examples/example1-getting-started/simple_prefixclaim.yaml b/docs/examples/example1-getting-started/simple_prefixclaim.yaml deleted file mode 100644 index 483a0c3d..00000000 --- a/docs/examples/example1-getting-started/simple_prefixclaim.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: netbox.dev/v1 -kind: PrefixClaim -metadata: - labels: - app.kubernetes.io/name: netbox-operator - app.kubernetes.io/managed-by: kustomize - name: simple-prefixclaim -spec: - tenant: "MY_TENANT" - parentPrefix: 3.0.0.64/26 - prefixLength: "/30" diff --git a/docs/examples/example2-multicluster/README-ex2.md b/docs/examples/example2-multicluster/README-ex2.md deleted file mode 100644 index 95d5f60b..00000000 --- a/docs/examples/example2-multicluster/README-ex2.md +++ /dev/null @@ -1,22 +0,0 @@ -# Example 2: Multi Cluster - -This example shows how to claim multiple prefixes from different clusters and make them available as metalLB ip address pools. - -1. Create ip address pools on the london cluster -```bash -kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml -``` -2. Create ip address pool on the zurich cluster -```bash -kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml -``` -3. Look up the created prefix claims and metalLB ipaddresspools -```bash -watch kubectl get --context kind-london pxc,ipaddresspools -A -``` -and -```bash -watch kubectl get --context kind-zurich pxc,ipaddresspools -A -``` - -![Example 2](multicluster.drawio.svg) diff --git a/docs/examples/new/example2-krm-glue/README.md b/docs/examples/new/example2-krm-glue/README.md index 25d6fd91..9dc4c985 100644 --- a/docs/examples/new/example2-krm-glue/README.md +++ b/docs/examples/new/example2-krm-glue/README.md @@ -6,4 +6,26 @@ So we have Prefixes represented as Kubernetes Resources. Now what can we do with We use kro.io to glue this to MetalLB IPAddressPools -diagram and more instructions here... +1. Apply the kro resource graph definition, defining the mapping from the prefix claim to the metalLB ip address pool +```bash +kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml +``` +2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox +```bash +kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml +``` +3. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step +```bash +kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml +``` +4. check if the prefixclaim and ipaddresspool got created +```bash +watch kubectl get --context kind-zurich pxc,ipaddresspools my-nginx -A +``` +5. check if the service got an external ip address assigned +```bash +watch kubectl get --context kind-zurich svc my-nginx -n nginx +``` + + +![Example 1.3](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg b/docs/examples/new/example2-krm-glue/simple_prefixclaim-large-font.drawio.svg similarity index 100% rename from docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg rename to docs/examples/new/example2-krm-glue/simple_prefixclaim-large-font.drawio.svg diff --git a/docs/examples/new/example5-multicluster/README-ex2.md b/docs/examples/new/example5-multicluster/README-ex2.md new file mode 100644 index 00000000..587d34e2 --- /dev/null +++ b/docs/examples/new/example5-multicluster/README-ex2.md @@ -0,0 +1 @@ +# Example 2: Multi Cluster diff --git a/docs/examples/new/example5-multicluster/README.md b/docs/examples/new/example5-multicluster/README.md index 0e8086ce..5d7dd537 100644 --- a/docs/examples/new/example5-multicluster/README.md +++ b/docs/examples/new/example5-multicluster/README.md @@ -4,5 +4,23 @@ NetBox Operator uses NetBox to avoid IP overlaps. This means that we can use NetBox Operator on multiple clusters. You can try this out using the example in this directory. -diagram and more instructions here... -most of set-up directory would go here \ No newline at end of file +This example shows how to claim multiple prefixes from different clusters and make them available as metalLB ip address pools. + +1. Create ip address pools on the london cluster +```bash +kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml +``` +2. Create ip address pool on the zurich cluster +```bash +kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml +``` +3. Look up the created prefix claims and metalLB ipaddresspools +```bash +watch kubectl get --context kind-london pxc,ipaddresspools -A +``` +and +```bash +watch kubectl get --context kind-zurich pxc,ipaddresspools -A +``` + +![Example 2](multicluster.drawio.svg) \ No newline at end of file diff --git a/docs/examples/example2-multicluster/london-pools.yaml b/docs/examples/new/example5-multicluster/london-pools.yaml similarity index 100% rename from docs/examples/example2-multicluster/london-pools.yaml rename to docs/examples/new/example5-multicluster/london-pools.yaml diff --git a/docs/examples/example2-multicluster/multicluster.drawio.svg b/docs/examples/new/example5-multicluster/multicluster.drawio.svg similarity index 100% rename from docs/examples/example2-multicluster/multicluster.drawio.svg rename to docs/examples/new/example5-multicluster/multicluster.drawio.svg diff --git a/docs/examples/example2-multicluster/zurich-pools.yaml b/docs/examples/new/example5-multicluster/zurich-pools.yaml similarity index 100% rename from docs/examples/example2-multicluster/zurich-pools.yaml rename to docs/examples/new/example5-multicluster/zurich-pools.yaml From 8039bc46557c4df8ef319b6654c7e690c92b81ef Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Wed, 26 Mar 2025 22:56:43 +0100 Subject: [PATCH 24/28] move examples from new to examples folder --- .../example1-getting-started/README.md | 61 ++ .../dynamic_prefixclaim-large-font.drawio.svg | 532 ++++++++++++++++++ .../prefixclaim-dynamic.drawio.svg | 0 .../prefixclaim-dynamic.yaml | 0 .../prefixclaim-simple.drawio.svg | 0 .../prefixclaim-simple.yaml | 0 .../simple_prefixclaim-large-font.drawio.svg | 0 docs/examples/example2-krm-glue/README.md | 45 ++ .../kro-rdg-poolfromnetbox.yaml | 0 .../metallb-ip-address-pool-netbox.yaml | 4 +- .../metallb-ipaddresspool-netbox.drawio.svg | 0 .../example2-krm-glue/prepare-demo-env.sh | 42 ++ .../example2-krm-glue/sample-deployment.yaml | 1 + .../{new => }/example3-restoration/README.md | 0 .../kro-rdg-poolfromnetbox1.yaml | 0 .../kro-rdg-poolfromnetbox2.yaml | 0 .../kro-rdg-poolfromnetbox3.yaml | 0 .../restoration-simple.drawio.svg | 0 .../restoration.drawio.svg | 0 .../{new => }/example4-exhaustion/README.md | 0 .../exhaustion-1-starting-point.drawio.svg | 0 .../exhaustion-2-prefix-exhausted.drawio.svg | 0 .../exhaustion-3-after-fix.drawio.svg | 0 .../kro-rdg-poolfromnetbox.yaml | 0 .../{new => }/example5-multicluster/README.md | 4 +- .../example5-multicluster/london-pools.yaml | 0 .../multicluster.drawio.svg | 0 .../example5-multicluster/zurich-pools.yaml | 0 .../new/example1-getting-started/README.md | 42 -- docs/examples/new/example2-krm-glue/README.md | 31 - .../new/example5-multicluster/README-ex2.md | 1 - docs/examples/set-up/create-kind-clusters.sh | 8 - 32 files changed, 684 insertions(+), 87 deletions(-) create mode 100644 docs/examples/example1-getting-started/README.md create mode 100644 docs/examples/example1-getting-started/dynamic_prefixclaim-large-font.drawio.svg rename docs/examples/{new => }/example1-getting-started/prefixclaim-dynamic.drawio.svg (100%) rename docs/examples/{new => }/example1-getting-started/prefixclaim-dynamic.yaml (100%) rename docs/examples/{new => }/example1-getting-started/prefixclaim-simple.drawio.svg (100%) rename docs/examples/{new => }/example1-getting-started/prefixclaim-simple.yaml (100%) rename docs/examples/{new/example2-krm-glue => example1-getting-started}/simple_prefixclaim-large-font.drawio.svg (100%) create mode 100644 docs/examples/example2-krm-glue/README.md rename docs/examples/{new => }/example2-krm-glue/kro-rdg-poolfromnetbox.yaml (100%) rename docs/examples/{set-up => example2-krm-glue}/metallb-ip-address-pool-netbox.yaml (88%) rename docs/examples/{new => }/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg (100%) create mode 100755 docs/examples/example2-krm-glue/prepare-demo-env.sh rename docs/examples/{new => }/example2-krm-glue/sample-deployment.yaml (95%) rename docs/examples/{new => }/example3-restoration/README.md (100%) rename docs/examples/{new => }/example3-restoration/kro-rdg-poolfromnetbox1.yaml (100%) rename docs/examples/{new => }/example3-restoration/kro-rdg-poolfromnetbox2.yaml (100%) rename docs/examples/{new => }/example3-restoration/kro-rdg-poolfromnetbox3.yaml (100%) rename docs/examples/{new => }/example3-restoration/restoration-simple.drawio.svg (100%) rename docs/examples/{new => }/example3-restoration/restoration.drawio.svg (100%) rename docs/examples/{new => }/example4-exhaustion/README.md (100%) rename docs/examples/{new => }/example4-exhaustion/exhaustion-1-starting-point.drawio.svg (100%) rename docs/examples/{new => }/example4-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg (100%) rename docs/examples/{new => }/example4-exhaustion/exhaustion-3-after-fix.drawio.svg (100%) rename docs/examples/{new => }/example4-exhaustion/kro-rdg-poolfromnetbox.yaml (100%) rename docs/examples/{new => }/example5-multicluster/README.md (86%) rename docs/examples/{new => }/example5-multicluster/london-pools.yaml (100%) rename docs/examples/{new => }/example5-multicluster/multicluster.drawio.svg (100%) rename docs/examples/{new => }/example5-multicluster/zurich-pools.yaml (100%) delete mode 100644 docs/examples/new/example1-getting-started/README.md delete mode 100644 docs/examples/new/example2-krm-glue/README.md delete mode 100644 docs/examples/new/example5-multicluster/README-ex2.md diff --git a/docs/examples/example1-getting-started/README.md b/docs/examples/example1-getting-started/README.md new file mode 100644 index 00000000..83df9517 --- /dev/null +++ b/docs/examples/example1-getting-started/README.md @@ -0,0 +1,61 @@ +# Example 1: Getting Started + +# 0.1 Create a local cluster with nebox-installed + +1. use the 'create-kind' target from the Makefile to create a kind cluster and deploy NetBox on it +```bash +make create-kind +``` + +# 0.2 Manually Create a Prefix in NetBox + +Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. + +1. Port-forward NetBox: +```bash +kubectl port-forward deploy/netbox 8080:8080 +``` +2. Open in your favorite browser and log in with the username `admin` and password `admin` +3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' + +# 1.1 Claim a Prefix + +In this example, we use a `.spec.parentPrefix` that we know in advance. This is useful if you already know exactly from which prefix you want to claim from. + +Navigate to 'docs/examples/example1-getting-started' to run the following commands. + +1. Inspect the spec of the sample prefix claim CR +```bash +cat prefixclaim-simple.yaml +``` +2. Apply the manifest defining the prefix claim +```bash +kubectl apply -f prefixclaim-simple.yaml +``` +3. Check that the prefix claim CR got a prefix assigned +```bash +kubectl get pxc,px +``` + +![Example 1.1](prefixclaim-simple.drawio.svg) + +# 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector + +In this example, we use a `.spec.parentPrefixSelector`, which is a list of selectors that tell NetBox Operator from which parent prefixes to claim our Prefix from. + +Navigate to 'docs/examples/example1-getting-started' to run the following commands. + +1. Inspect the spec of the sample prefix claim CR +```bash +cat prefixclaim-dynamic.yaml +``` +2. Apply the manifest defining the prefix claim +```bash +kubectl apply -f prefixclaim-dynamic.yaml +``` +3. Check that the prefix claim CR got a prefix addigned +```bash +kubectl get pxc,px +``` + +![Example 1.2](prefixclaim-dynamic.drawio.svg) diff --git a/docs/examples/example1-getting-started/dynamic_prefixclaim-large-font.drawio.svg b/docs/examples/example1-getting-started/dynamic_prefixclaim-large-font.drawio.svg new file mode 100644 index 00000000..56e2c3d2 --- /dev/null +++ b/docs/examples/example1-getting-started/dynamic_prefixclaim-large-font.drawio.svg @@ -0,0 +1,532 @@ + + + + + + + + + + + + + +
+
+
+ + k8s cluster + +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ + user namespace + +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ + NetBox REST API + +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ + namespace netbox-operator + +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ + create/update/delete + +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ + get available prefixes + +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + + + + + +
+
+
+ + NetBox Operator + +
+
+
+
+ + NetBox Operator + +
+
+
+ + + + + + + +
+
+
+
+ + PrefixClaim CR + +
+
+
+
+
+ + PrefixClaim CR + +
+
+
+ + + + + + + +
+
+
+ + Parent +
+ Prefix +
+
+
+
+
+ + Parent... + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + +
+
+
+ + ownerReference + +
+
+
+
+ + ownerReference + +
+
+
+ + + + + + + +
+
+
+
+ + Prefix CR + +
+
+
+
+
+ + Prefix CR + +
+
+
+ + + + + + + +
+
+
+ + Claimed Prefix + +
+
+
+
+ + Claimed Prefix + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + consumer + +
+
+
+
+ + consumer + +
+
+
+ + + + + + + + +
+
+
+ + User +
+ w/ kubectl +
+
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ + GitOps +
+ w/ Argo or Flux +
+
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ + and/or + +
+
+
+
+ + and/or + +
+
+
+ + + + + + + +
+
+
+ + Matching Prefixes + +
+
+
+
+ + Matching Prefixes + +
+
+
+ + + + +
+
+
+ + get matching prefixes + +
+
+
+
+ + get matching prefixes + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/new/example1-getting-started/prefixclaim-dynamic.drawio.svg b/docs/examples/example1-getting-started/prefixclaim-dynamic.drawio.svg similarity index 100% rename from docs/examples/new/example1-getting-started/prefixclaim-dynamic.drawio.svg rename to docs/examples/example1-getting-started/prefixclaim-dynamic.drawio.svg diff --git a/docs/examples/new/example1-getting-started/prefixclaim-dynamic.yaml b/docs/examples/example1-getting-started/prefixclaim-dynamic.yaml similarity index 100% rename from docs/examples/new/example1-getting-started/prefixclaim-dynamic.yaml rename to docs/examples/example1-getting-started/prefixclaim-dynamic.yaml diff --git a/docs/examples/new/example1-getting-started/prefixclaim-simple.drawio.svg b/docs/examples/example1-getting-started/prefixclaim-simple.drawio.svg similarity index 100% rename from docs/examples/new/example1-getting-started/prefixclaim-simple.drawio.svg rename to docs/examples/example1-getting-started/prefixclaim-simple.drawio.svg diff --git a/docs/examples/new/example1-getting-started/prefixclaim-simple.yaml b/docs/examples/example1-getting-started/prefixclaim-simple.yaml similarity index 100% rename from docs/examples/new/example1-getting-started/prefixclaim-simple.yaml rename to docs/examples/example1-getting-started/prefixclaim-simple.yaml diff --git a/docs/examples/new/example2-krm-glue/simple_prefixclaim-large-font.drawio.svg b/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg similarity index 100% rename from docs/examples/new/example2-krm-glue/simple_prefixclaim-large-font.drawio.svg rename to docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg diff --git a/docs/examples/example2-krm-glue/README.md b/docs/examples/example2-krm-glue/README.md new file mode 100644 index 00000000..3982feab --- /dev/null +++ b/docs/examples/example2-krm-glue/README.md @@ -0,0 +1,45 @@ +# Example 2: Glue NetBox CRs to MetalLB CRs + +## Introduction + +So we have Prefixes represented as Kubernetes Resources. Now what can we do with this? + +We use kro.run to glue this to MetalLB IPAddressPools + +Navigate to 'docs/examples/example2-krm-glue' to run the following commands. + +0. Install kro and metallb with the installation script `docs/examples/new/example2-krm-glue/prepare-demo-env.sh` +Then navigate to 'docs/examples/example2-krm-glue' to follow the steps below. + +1. Inspect the spec of the sample prefix claim CR +```bash +cat kro-rdg-poolfromnetbox.yaml +``` +2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox +```bash +kubectl apply -f kro-rdg-poolfromnetbox.yaml +``` +3. Check if the prefixclaim CR and the metalLB ipaddresspool CR got created +```bash +kubectl get pxc,ipaddresspool -A +``` +4. Inspect the spec of the sample prefix claim CR +```bash +cat sample-deployment.yaml +``` +5. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step +```bash +kubectl apply -f sample-deployment.yaml +``` +6. check if the service got an external ip address assigned +```bash +kubectl get svc my-nginx -n nginx +``` +7. try to connect to your service with the external ip +```bash +k exec curl -it -- sh +curl +``` + + +![Example 2](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/new/example2-krm-glue/kro-rdg-poolfromnetbox.yaml b/docs/examples/example2-krm-glue/kro-rdg-poolfromnetbox.yaml similarity index 100% rename from docs/examples/new/example2-krm-glue/kro-rdg-poolfromnetbox.yaml rename to docs/examples/example2-krm-glue/kro-rdg-poolfromnetbox.yaml diff --git a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml b/docs/examples/example2-krm-glue/metallb-ip-address-pool-netbox.yaml similarity index 88% rename from docs/examples/set-up/metallb-ip-address-pool-netbox.yaml rename to docs/examples/example2-krm-glue/metallb-ip-address-pool-netbox.yaml index 21e5ebaa..620d7328 100644 --- a/docs/examples/set-up/metallb-ip-address-pool-netbox.yaml +++ b/docs/examples/example2-krm-glue/metallb-ip-address-pool-netbox.yaml @@ -1,7 +1,7 @@ apiVersion: kro.run/v1alpha1 kind: ResourceGraphDefinition metadata: - name: metallb-ip-address-pool--netbox + name: metallb-ip-address-pool-netbox spec: schema: apiVersion: v1alpha1 @@ -10,7 +10,6 @@ spec: name: string tenant: string prefixLength: string - preserveInNetbox: boolean parentPrefixSelector: tenant: string environment: string @@ -29,7 +28,6 @@ spec: tenant: ${schema.spec.tenant} prefixLength: ${schema.spec.prefixLength} parentPrefixSelector: ${schema.spec.parentPrefixSelector} - preserveInNetbox: ${schema.spec.preserveInNetbox} - id: ipaddresspool template: diff --git a/docs/examples/new/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg b/docs/examples/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg similarity index 100% rename from docs/examples/new/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg rename to docs/examples/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg diff --git a/docs/examples/example2-krm-glue/prepare-demo-env.sh b/docs/examples/example2-krm-glue/prepare-demo-env.sh new file mode 100755 index 00000000..cd615fb8 --- /dev/null +++ b/docs/examples/example2-krm-glue/prepare-demo-env.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e + +# install netbox in the london cluster and load demo data +make deploy-kind + +# install curl pod to demo access to created service +kind load docker-image curlimages/curl +kind load docker-image curlimages/curl +kubectl run curl --image curlimages/curl --image-pull-policy=Never -- sleep infinity + +# load the nginx image into the kind cluster +kind load docker-image nginx +kind load docker-image nginx + +DEPLOYMENT_NAME=netbox-operator-controller-manager +NAMESPACE=netbox-operator-system +CONTEXT=kind-kind + +# install MetalLB +kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml + +# install kro +helm install kro oci://ghcr.io/kro-run/kro/kro \ + --namespace kro \ + --create-namespace \ + --version=0.2.1 + +while true; do + # Check if the deployment is ready + READY_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.readyReplicas}') + DESIRED_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.replicas}') + + if [[ "$READY_REPLICAS" == "$DESIRED_REPLICAS" ]] && [[ "$READY_REPLICAS" -gt 0 ]]; then + echo "Deployment $DEPLOYMENT_NAME in cluster $CONTEXT is ready." + break + else + echo "Waiting... Ready replicas in cluster $CONTEXT: $READY_REPLICAS / $DESIRED_REPLICAS" + sleep 5 + fi +done +kubectl apply --context $CONTEXT -f docs/examples/new/example2-krm-glue/metallb-ip-address-pool-netbox.yaml diff --git a/docs/examples/new/example2-krm-glue/sample-deployment.yaml b/docs/examples/example2-krm-glue/sample-deployment.yaml similarity index 95% rename from docs/examples/new/example2-krm-glue/sample-deployment.yaml rename to docs/examples/example2-krm-glue/sample-deployment.yaml index 135bbf53..51ea0e2e 100644 --- a/docs/examples/new/example2-krm-glue/sample-deployment.yaml +++ b/docs/examples/example2-krm-glue/sample-deployment.yaml @@ -22,6 +22,7 @@ spec: containers: - name: my-nginx image: nginx + imagePullPolicy: Never ports: - containerPort: 80 --- diff --git a/docs/examples/new/example3-restoration/README.md b/docs/examples/example3-restoration/README.md similarity index 100% rename from docs/examples/new/example3-restoration/README.md rename to docs/examples/example3-restoration/README.md diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox1.yaml b/docs/examples/example3-restoration/kro-rdg-poolfromnetbox1.yaml similarity index 100% rename from docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox1.yaml rename to docs/examples/example3-restoration/kro-rdg-poolfromnetbox1.yaml diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox2.yaml b/docs/examples/example3-restoration/kro-rdg-poolfromnetbox2.yaml similarity index 100% rename from docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox2.yaml rename to docs/examples/example3-restoration/kro-rdg-poolfromnetbox2.yaml diff --git a/docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox3.yaml b/docs/examples/example3-restoration/kro-rdg-poolfromnetbox3.yaml similarity index 100% rename from docs/examples/new/example3-restoration/kro-rdg-poolfromnetbox3.yaml rename to docs/examples/example3-restoration/kro-rdg-poolfromnetbox3.yaml diff --git a/docs/examples/new/example3-restoration/restoration-simple.drawio.svg b/docs/examples/example3-restoration/restoration-simple.drawio.svg similarity index 100% rename from docs/examples/new/example3-restoration/restoration-simple.drawio.svg rename to docs/examples/example3-restoration/restoration-simple.drawio.svg diff --git a/docs/examples/new/example3-restoration/restoration.drawio.svg b/docs/examples/example3-restoration/restoration.drawio.svg similarity index 100% rename from docs/examples/new/example3-restoration/restoration.drawio.svg rename to docs/examples/example3-restoration/restoration.drawio.svg diff --git a/docs/examples/new/example4-exhaustion/README.md b/docs/examples/example4-exhaustion/README.md similarity index 100% rename from docs/examples/new/example4-exhaustion/README.md rename to docs/examples/example4-exhaustion/README.md diff --git a/docs/examples/new/example4-exhaustion/exhaustion-1-starting-point.drawio.svg b/docs/examples/example4-exhaustion/exhaustion-1-starting-point.drawio.svg similarity index 100% rename from docs/examples/new/example4-exhaustion/exhaustion-1-starting-point.drawio.svg rename to docs/examples/example4-exhaustion/exhaustion-1-starting-point.drawio.svg diff --git a/docs/examples/new/example4-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg b/docs/examples/example4-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg similarity index 100% rename from docs/examples/new/example4-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg rename to docs/examples/example4-exhaustion/exhaustion-2-prefix-exhausted.drawio.svg diff --git a/docs/examples/new/example4-exhaustion/exhaustion-3-after-fix.drawio.svg b/docs/examples/example4-exhaustion/exhaustion-3-after-fix.drawio.svg similarity index 100% rename from docs/examples/new/example4-exhaustion/exhaustion-3-after-fix.drawio.svg rename to docs/examples/example4-exhaustion/exhaustion-3-after-fix.drawio.svg diff --git a/docs/examples/new/example4-exhaustion/kro-rdg-poolfromnetbox.yaml b/docs/examples/example4-exhaustion/kro-rdg-poolfromnetbox.yaml similarity index 100% rename from docs/examples/new/example4-exhaustion/kro-rdg-poolfromnetbox.yaml rename to docs/examples/example4-exhaustion/kro-rdg-poolfromnetbox.yaml diff --git a/docs/examples/new/example5-multicluster/README.md b/docs/examples/example5-multicluster/README.md similarity index 86% rename from docs/examples/new/example5-multicluster/README.md rename to docs/examples/example5-multicluster/README.md index 5d7dd537..a1689f45 100644 --- a/docs/examples/new/example5-multicluster/README.md +++ b/docs/examples/example5-multicluster/README.md @@ -16,11 +16,11 @@ kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zuri ``` 3. Look up the created prefix claims and metalLB ipaddresspools ```bash -watch kubectl get --context kind-london pxc,ipaddresspools -A +kubectl get --context kind-london pxc,ipaddresspools -A ``` and ```bash -watch kubectl get --context kind-zurich pxc,ipaddresspools -A +kubectl get --context kind-zurich pxc,ipaddresspools -A ``` ![Example 2](multicluster.drawio.svg) \ No newline at end of file diff --git a/docs/examples/new/example5-multicluster/london-pools.yaml b/docs/examples/example5-multicluster/london-pools.yaml similarity index 100% rename from docs/examples/new/example5-multicluster/london-pools.yaml rename to docs/examples/example5-multicluster/london-pools.yaml diff --git a/docs/examples/new/example5-multicluster/multicluster.drawio.svg b/docs/examples/example5-multicluster/multicluster.drawio.svg similarity index 100% rename from docs/examples/new/example5-multicluster/multicluster.drawio.svg rename to docs/examples/example5-multicluster/multicluster.drawio.svg diff --git a/docs/examples/new/example5-multicluster/zurich-pools.yaml b/docs/examples/example5-multicluster/zurich-pools.yaml similarity index 100% rename from docs/examples/new/example5-multicluster/zurich-pools.yaml rename to docs/examples/example5-multicluster/zurich-pools.yaml diff --git a/docs/examples/new/example1-getting-started/README.md b/docs/examples/new/example1-getting-started/README.md deleted file mode 100644 index ea9a1970..00000000 --- a/docs/examples/new/example1-getting-started/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Example 1: Getting Started - -# 0. Ma![img.png](img.png)nually Create a Prefix in NetBox - -Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. - -1. Port-forward NetBox: -```bash -kubectl port-forward --context kind-london deploy/netbox 8080:8080 -``` -2. Open in your favorite browser and log in with the username `admin` and password `admin` -3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' - -# 1.1 Claim a Prefix - -In this example, we use a `.spec.parentPrefix` that we know in advance. This is useful if you already know exactly from which prefix you want to claim from. - -1. Apply the manifest defining the prefix claim -```bash -kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/prefixclaim-simple.yaml -``` -2. Check that the prefix claim CR got a prefix addigned -```bash -watch kubectl get --context kind-zurich pxc,px -``` - -![Example 1.1](prefixclaim-simple.drawio.svg) - -# 1.2 Dynamically Claim a Prefix with a Parent Prefix Selector - -In this example, we use a `.spec.parentPrefixSelector`, which is a list of selectors that tell NetBox Operator from which parent prefixes to claim our Prefix from. - -1. Apply the manifest defining the prefix claim -```bash -kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/prefixclaim-dynamic.yaml -``` -2. Check that the prefix claim CR got a prefix addigned -```bash -watch kubectl get --context kind-zurich pxc,px -``` - -![Example 1.2](prefixclaim-dynamic.drawio.svg) diff --git a/docs/examples/new/example2-krm-glue/README.md b/docs/examples/new/example2-krm-glue/README.md deleted file mode 100644 index 9dc4c985..00000000 --- a/docs/examples/new/example2-krm-glue/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Example 2: Glue NetBox CRs to MetalLB CRs - -## Introduction - -So we have Prefixes represented as Kubernetes Resources. Now what can we do with this? - -We use kro.io to glue this to MetalLB IPAddressPools - -1. Apply the kro resource graph definition, defining the mapping from the prefix claim to the metalLB ip address pool -```bash -kubectl apply -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml -``` -2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox -```bash -kubectl apply --context kind-zurich -f docs/examples/example1-getting-started/ip-address-pool.yaml -``` -3. Apply the manifests to createa deployment with a service that gets a ip assigned from the metalLB pool created in the prevoius step -```bash -kubectl apply --context kind-zurich -k docs/examples/example1-getting-started/sample-deployment.yaml -``` -4. check if the prefixclaim and ipaddresspool got created -```bash -watch kubectl get --context kind-zurich pxc,ipaddresspools my-nginx -A -``` -5. check if the service got an external ip address assigned -```bash -watch kubectl get --context kind-zurich svc my-nginx -n nginx -``` - - -![Example 1.3](metallb-ipaddresspool-netbox.drawio.svg) diff --git a/docs/examples/new/example5-multicluster/README-ex2.md b/docs/examples/new/example5-multicluster/README-ex2.md deleted file mode 100644 index 587d34e2..00000000 --- a/docs/examples/new/example5-multicluster/README-ex2.md +++ /dev/null @@ -1 +0,0 @@ -# Example 2: Multi Cluster diff --git a/docs/examples/set-up/create-kind-clusters.sh b/docs/examples/set-up/create-kind-clusters.sh index bcf6d58f..f9db0288 100755 --- a/docs/examples/set-up/create-kind-clusters.sh +++ b/docs/examples/set-up/create-kind-clusters.sh @@ -39,14 +39,6 @@ for clustername in "$@"; do fi kind create cluster --name $clustername --config $temp_config || { echo -e "${RED}Error: Failed to create cluster ${clustername}${NC}"; rm -f "$temp_config"; exit 1; } - # install MetalLB - kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml - - # install kro - helm install kro oci://ghcr.io/kro-run/kro/kro \ - --namespace kro \ - --create-namespace \ - --version=0.2.1 else echo -e "${RED}Error: Configuration file $config_file not found${NC}" exit 1 From 056d1b2b04b845ec4fbd9e2af363d43940fcbb79 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:02:23 +0100 Subject: [PATCH 25/28] fix sample 1 and 2 for new structure --- docs/examples/README.md | 6 +- .../example1-getting-started/README.md | 9 +- .../prefixclaim-dynamic.drawio.svg | 144 ++-- .../simple_prefixclaim-large-font.drawio.svg | 140 ++-- .../README.md | 14 +- ...ancer-ip-pool-netbox-large-font.drawio.svg | 738 ++++++++++++++++++ .../load-balancer-ip-pool-netbox.yaml} | 6 +- .../metallb-ipaddresspool-netbox.drawio.svg | 0 .../prepare-demo-env.sh | 2 +- .../sample-deployment.yaml | 0 .../zurich-pool.yaml} | 2 +- docs/examples/example5-multicluster/README.md | 11 +- .../cluster-cfg.yaml | 0 .../create-kind-clusters.sh | 0 .../demo-setup.drawio.svg | 0 .../kustomization.yaml | 0 .../example5-multicluster/london-pools.yaml | 40 +- .../netbox-l2advertisement.yaml | 0 .../netbox-svc.yaml | 0 .../prepare-demo-env.sh | 1 - .../example5-multicluster/zurich-pools.yaml | 36 +- 21 files changed, 942 insertions(+), 207 deletions(-) rename docs/examples/{example2-krm-glue => example2-load-balancer-ip}/README.md (74%) create mode 100644 docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg rename docs/examples/{example2-krm-glue/metallb-ip-address-pool-netbox.yaml => example2-load-balancer-ip/load-balancer-ip-pool-netbox.yaml} (86%) rename docs/examples/{example2-krm-glue => example2-load-balancer-ip}/metallb-ipaddresspool-netbox.drawio.svg (100%) rename docs/examples/{example2-krm-glue => example2-load-balancer-ip}/prepare-demo-env.sh (92%) rename docs/examples/{example2-krm-glue => example2-load-balancer-ip}/sample-deployment.yaml (100%) rename docs/examples/{example2-krm-glue/kro-rdg-poolfromnetbox.yaml => example2-load-balancer-ip/zurich-pool.yaml} (88%) rename docs/examples/{set-up => example5-multicluster}/cluster-cfg.yaml (100%) rename docs/examples/{set-up => example5-multicluster}/create-kind-clusters.sh (100%) rename docs/examples/{ => example5-multicluster}/demo-setup.drawio.svg (100%) rename docs/examples/{set-up => example5-multicluster}/kustomization.yaml (100%) rename docs/examples/{set-up => example5-multicluster}/netbox-l2advertisement.yaml (100%) rename docs/examples/{set-up => example5-multicluster}/netbox-svc.yaml (100%) rename docs/examples/{set-up => example5-multicluster}/prepare-demo-env.sh (96%) diff --git a/docs/examples/README.md b/docs/examples/README.md index f34a3ff0..f16d2c99 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -1,10 +1,8 @@ # NetBox Operator Examples -This folder shows some examples how the NetBox Operator can be used. The demo environment can be prepared with the 'docs/examples/set-up/prepare-demo-env.sh' script, which creates two kind clusters with NetBox Operator and [kro] installed. One one of the clusters a NetBox instance is installed which is available to both NetBox Operator deployments. +This folder shows some examples how the NetBox Operator can be used. -![Demo Set Up](demo-setup.drawio.svg) - -[kro]: https://github.com/kro-run/kro/ +Each example folder contains a README.md which explains how you can set up your local enviroment to step through the examples. Prerequisites: - go version v1.24.0+ diff --git a/docs/examples/example1-getting-started/README.md b/docs/examples/example1-getting-started/README.md index 83df9517..ad4721c1 100644 --- a/docs/examples/example1-getting-started/README.md +++ b/docs/examples/example1-getting-started/README.md @@ -2,9 +2,10 @@ # 0.1 Create a local cluster with nebox-installed -1. use the 'create-kind' target from the Makefile to create a kind cluster and deploy NetBox on it +1. use the 'create-kind' and 'deploy-kind' targets from the Makefile to create a kind cluster and deploy NetBox and NetBox Operator on it ```bash make create-kind +make deploy-kind ``` # 0.2 Manually Create a Prefix in NetBox @@ -18,12 +19,14 @@ kubectl port-forward deploy/netbox 8080:8080 2. Open in your favorite browser and log in with the username `admin` and password `admin` 3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' +# 0.3 Navigate to the example + +Navigate to 'docs/examples/example1-getting-started' to run the examples below + # 1.1 Claim a Prefix In this example, we use a `.spec.parentPrefix` that we know in advance. This is useful if you already know exactly from which prefix you want to claim from. -Navigate to 'docs/examples/example1-getting-started' to run the following commands. - 1. Inspect the spec of the sample prefix claim CR ```bash cat prefixclaim-simple.yaml diff --git a/docs/examples/example1-getting-started/prefixclaim-dynamic.drawio.svg b/docs/examples/example1-getting-started/prefixclaim-dynamic.drawio.svg index 920da03b..00f401cd 100644 --- a/docs/examples/example1-getting-started/prefixclaim-dynamic.drawio.svg +++ b/docs/examples/example1-getting-started/prefixclaim-dynamic.drawio.svg @@ -1,17 +1,17 @@ - + - + - + -
+
k8s cluster @@ -19,23 +19,23 @@
- + k8s cluster - + - + -
+
user namespace @@ -43,24 +43,24 @@
- + user namespace - - + + - + -
+
consumer @@ -68,20 +68,20 @@
- + consumer - + -
+
NetBox REST API @@ -89,20 +89,20 @@
- + NetBox REST API - + -
+
namespace netbox-operator @@ -110,21 +110,21 @@
- + namespace netbox-operator - - + + -
+
create @@ -132,21 +132,21 @@
- + create - - + + -
+
reconcile @@ -154,21 +154,21 @@
- + reconcile - - + + -
+
create/update/delete @@ -176,21 +176,21 @@
- + create/update/delete - - + + -
+
select matching parnet prefix @@ -201,20 +201,20 @@
- + select matching parnet prefix... - + -
+
netbox-operator @@ -222,20 +222,20 @@
- + netbox-operator - + -
+
@@ -258,20 +258,20 @@
- + kind: PrefixClaim... - + -
+
Prefix 2.0.0.0/16 @@ -279,21 +279,21 @@
- + Prefix 2.0.0.0/16 - - + + -
+
User @@ -303,21 +303,21 @@
- + User... - - + + -
+
GitOps @@ -327,20 +327,20 @@
- + GitOps... - + -
+
and/or @@ -348,21 +348,21 @@
- + and/or - - + + -
+
reconcile @@ -370,21 +370,21 @@
- + reconcile - - + + -
+
ownerReference @@ -392,20 +392,20 @@
- + ownerReference - + -
+
@@ -424,20 +424,20 @@
- + kind: Prefix... - + -
+
Prefix 2.0.0.0/28 @@ -445,17 +445,17 @@
- + Prefix 2.0.0.0/28 - + - + diff --git a/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg b/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg index 94742445..d89c2dbe 100644 --- a/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg +++ b/docs/examples/example1-getting-started/simple_prefixclaim-large-font.drawio.svg @@ -1,17 +1,17 @@ - + - + - + -
+
@@ -21,23 +21,23 @@
- + k8s cluster - + - + -
+
@@ -47,24 +47,24 @@
- + user namespace - - + + - + -
+
@@ -74,20 +74,20 @@
- + NetBox REST API - + -
+
@@ -97,21 +97,21 @@
- + namespace netbox-operator - - + + -
+
@@ -121,21 +121,21 @@
- + create - - + + -
+
@@ -145,21 +145,21 @@
- + reconcile - - + + -
+
@@ -169,21 +169,21 @@
- + create/update/delete - - + + -
+
@@ -193,20 +193,20 @@
- + get available prefixes - + -
+
@@ -216,20 +216,20 @@
- + NetBox Operator - + -
+
@@ -241,20 +241,20 @@
- + PrefixClaim CR - + -
+
@@ -266,21 +266,21 @@
- + Parent... - - + + -
+
@@ -290,21 +290,21 @@
- + reconcile - - + + -
+
@@ -314,20 +314,20 @@
- + ownerReference - + -
+
@@ -339,20 +339,20 @@
- + Prefix CR - + -
+
@@ -362,27 +362,27 @@
- + Claimed Prefix - + - + - + -
+
@@ -392,21 +392,21 @@
- + consumer - - + + -
+
@@ -418,15 +418,15 @@
- + User... - - + + @@ -451,13 +451,13 @@ - + -
+
@@ -467,7 +467,7 @@
- + and/or diff --git a/docs/examples/example2-krm-glue/README.md b/docs/examples/example2-load-balancer-ip/README.md similarity index 74% rename from docs/examples/example2-krm-glue/README.md rename to docs/examples/example2-load-balancer-ip/README.md index 3982feab..ef102175 100644 --- a/docs/examples/example2-krm-glue/README.md +++ b/docs/examples/example2-load-balancer-ip/README.md @@ -6,18 +6,16 @@ So we have Prefixes represented as Kubernetes Resources. Now what can we do with We use kro.run to glue this to MetalLB IPAddressPools -Navigate to 'docs/examples/example2-krm-glue' to run the following commands. - -0. Install kro and metallb with the installation script `docs/examples/new/example2-krm-glue/prepare-demo-env.sh` -Then navigate to 'docs/examples/example2-krm-glue' to follow the steps below. +0. Install kro and metallb with the installation script `docs/examples/example2-load-balancer-ip/prepare-demo-env.sh` +Then navigate to 'docs/examples/example2-load-balancer-ip' to follow the steps below. 1. Inspect the spec of the sample prefix claim CR ```bash -cat kro-rdg-poolfromnetbox.yaml +cat zurich-pool.yaml ``` 2. Apply the manifests to create a deployment with a service and a metallb-ip-address-pool-netbox to create a metalLB IPAddressPool from the prefix claimed from NetBox ```bash -kubectl apply -f kro-rdg-poolfromnetbox.yaml +kubectl apply -f zurich-pool.yaml ``` 3. Check if the prefixclaim CR and the metalLB ipaddresspool CR got created ```bash @@ -31,9 +29,9 @@ cat sample-deployment.yaml ```bash kubectl apply -f sample-deployment.yaml ``` -6. check if the service got an external ip address assigned +6. check if the service got an external ip address assigned and that the nginx deployment is ready ```bash -kubectl get svc my-nginx -n nginx +kubectl get deploy,svc -n nginx ``` 7. try to connect to your service with the external ip ```bash diff --git a/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg b/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg new file mode 100644 index 00000000..1a6f6cc7 --- /dev/null +++ b/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg @@ -0,0 +1,738 @@ + + + + + + + + + + + + + +
+
+
+ + k8s cluster + +
+
+
+
+ + k8s cluster + +
+
+
+ + + + + + + + + + +
+
+
+ + user namespace + +
+
+
+
+ + user namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ + kro namespace + +
+
+
+
+ + kro namespace + +
+
+
+ + + + + + + + + + + +
+
+
+ + kubernetes resource orchstrator | kro + +
+
+
+
+ + kubernetes resource... + +
+
+
+ + + + + + + +
+
+
+
+ + PrefixClaim CR + +
+
+ + status + + + : + +
+
+ + prefix: + + 2.0.0.0/28 + + +
+
+
+
+
+ + PrefixClaim CR... + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+ + + + + + + + + + +
+
+
+
+ + MetalLBIPAddress- + +
+
+ + PoolNetBox CR + +
+
+
+
+
+ + MetalLBIPAddress-... + +
+
+
+ + + + + + + +
+
+
+ + namespace metallb-system + +
+
+
+
+ + namespace metallb-system + +
+
+
+ + + + + + + +
+
+
+
+ + IPAddressPool CR + +
+
+ + spec: + +
+
+ + ipaddresspools: + +
+
+ + - + + 2.0.0.0/28 + + +
+
+
+
+
+ + IPAddressPool CR... + +
+
+
+ + + + +
+
+
+ + read status + +
+
+
+
+ + read status + +
+
+
+ + + + + + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ + consumer + +
+
+
+
+ + consumer + +
+
+
+ + + + + + + + +
+
+
+ + User +
+ w/ kubectl +
+
+
+
+
+ + User... + +
+
+
+ + + + + + + + +
+
+
+ + GitOps +
+ w/ Argo or Flux +
+
+
+
+
+ + GitOps... + +
+
+
+ + + + + + + +
+
+
+ + and/or + +
+
+
+
+ + and/or + +
+
+
+ + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + + + + +
+
+
+ + NetBox REST API + +
+
+
+
+ + NetBox REST API + +
+
+
+ + + + + + + +
+
+
+ + namespace netbox-operator + +
+
+
+
+ + namespace netbox-operator + +
+
+
+ + + + + + + + +
+
+
+ + create/update/delete + +
+
+
+
+ + create/update/delete + +
+
+
+ + + + + + + + +
+
+
+ + get available prefixes + +
+
+
+
+ + get available prefixes + +
+
+
+ + + + + + + + + + + +
+
+
+ + NetBox Operator + +
+
+
+
+ + NetBox Operator + +
+
+
+ + + + + + + +
+
+
+ + Parent +
+ Prefix +
+
+
+
+
+ + Parent... + +
+
+
+ + + + + + + +
+
+
+ + Claimed Prefix + +
+
+
+
+ + Claimed Prefix + +
+
+
+ + + + + + + + + + +
+
+
+ + Matching Prefixes + +
+
+
+
+ + Matching Prefixes + +
+
+
+ + + + +
+
+
+ + get matching prefixes + +
+
+
+
+ + get matching prefixes + +
+
+
+ + + + + + + +
+
+
+
+ + Prefix CR + +
+
+
+
+
+ + Prefix CR + +
+
+
+ + + + + + + + + + + + +
+
+
+ + create + +
+
+
+
+ + create + +
+
+
+ + + + + + + + +
+
+
+ + reconcile + +
+
+
+
+ + reconcile + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/examples/example2-krm-glue/metallb-ip-address-pool-netbox.yaml b/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox.yaml similarity index 86% rename from docs/examples/example2-krm-glue/metallb-ip-address-pool-netbox.yaml rename to docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox.yaml index 620d7328..bc84c6ac 100644 --- a/docs/examples/example2-krm-glue/metallb-ip-address-pool-netbox.yaml +++ b/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox.yaml @@ -1,17 +1,16 @@ apiVersion: kro.run/v1alpha1 kind: ResourceGraphDefinition metadata: - name: metallb-ip-address-pool-netbox + name: load-balancer-ip-pool-netbox spec: schema: apiVersion: v1alpha1 - kind: MetalLBIPAddressPoolNetBox + kind: LoadBalancerIPPoolNetBox spec: name: string tenant: string prefixLength: string parentPrefixSelector: - tenant: string environment: string family: string status: @@ -25,7 +24,6 @@ spec: metadata: name: ${schema.spec.name} spec: - tenant: ${schema.spec.tenant} prefixLength: ${schema.spec.prefixLength} parentPrefixSelector: ${schema.spec.parentPrefixSelector} diff --git a/docs/examples/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg b/docs/examples/example2-load-balancer-ip/metallb-ipaddresspool-netbox.drawio.svg similarity index 100% rename from docs/examples/example2-krm-glue/metallb-ipaddresspool-netbox.drawio.svg rename to docs/examples/example2-load-balancer-ip/metallb-ipaddresspool-netbox.drawio.svg diff --git a/docs/examples/example2-krm-glue/prepare-demo-env.sh b/docs/examples/example2-load-balancer-ip/prepare-demo-env.sh similarity index 92% rename from docs/examples/example2-krm-glue/prepare-demo-env.sh rename to docs/examples/example2-load-balancer-ip/prepare-demo-env.sh index cd615fb8..db5049e3 100755 --- a/docs/examples/example2-krm-glue/prepare-demo-env.sh +++ b/docs/examples/example2-load-balancer-ip/prepare-demo-env.sh @@ -39,4 +39,4 @@ while true; do sleep 5 fi done -kubectl apply --context $CONTEXT -f docs/examples/new/example2-krm-glue/metallb-ip-address-pool-netbox.yaml +kubectl apply --context $CONTEXT -f docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox.yaml diff --git a/docs/examples/example2-krm-glue/sample-deployment.yaml b/docs/examples/example2-load-balancer-ip/sample-deployment.yaml similarity index 100% rename from docs/examples/example2-krm-glue/sample-deployment.yaml rename to docs/examples/example2-load-balancer-ip/sample-deployment.yaml diff --git a/docs/examples/example2-krm-glue/kro-rdg-poolfromnetbox.yaml b/docs/examples/example2-load-balancer-ip/zurich-pool.yaml similarity index 88% rename from docs/examples/example2-krm-glue/kro-rdg-poolfromnetbox.yaml rename to docs/examples/example2-load-balancer-ip/zurich-pool.yaml index 3dd1cf57..0178ee87 100644 --- a/docs/examples/example2-krm-glue/kro-rdg-poolfromnetbox.yaml +++ b/docs/examples/example2-load-balancer-ip/zurich-pool.yaml @@ -1,5 +1,5 @@ apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +kind: LoadBalancerIPPoolNetBox metadata: name: zurich-pool spec: diff --git a/docs/examples/example5-multicluster/README.md b/docs/examples/example5-multicluster/README.md index a1689f45..bb5068ca 100644 --- a/docs/examples/example5-multicluster/README.md +++ b/docs/examples/example5-multicluster/README.md @@ -6,6 +6,11 @@ NetBox Operator uses NetBox to avoid IP overlaps. This means that we can use Net This example shows how to claim multiple prefixes from different clusters and make them available as metalLB ip address pools. +Navigate to 'docs/examples/example2-krm-glue' to run the following commands. + +0. Install kro and metallb with the installation script `docs/examples/example5-multicluster/prepare-demo-env.sh` +Then navigate to 'docs/examples/edocs/examples/example5-multicluster' to follow the steps below. + 1. Create ip address pools on the london cluster ```bash kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml @@ -14,13 +19,13 @@ kubectl apply --context kind-london -f docs/examples/example2-multicluster/londo ```bash kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml ``` -3. Look up the created prefix claims and metalLB ipaddresspools +3. Look up the created prefix claims ```bash -kubectl get --context kind-london pxc,ipaddresspools -A +kubectl get --context kind-london pxc -A ``` and ```bash -kubectl get --context kind-zurich pxc,ipaddresspools -A +kubectl get --context kind-zurich pxc -A ``` ![Example 2](multicluster.drawio.svg) \ No newline at end of file diff --git a/docs/examples/set-up/cluster-cfg.yaml b/docs/examples/example5-multicluster/cluster-cfg.yaml similarity index 100% rename from docs/examples/set-up/cluster-cfg.yaml rename to docs/examples/example5-multicluster/cluster-cfg.yaml diff --git a/docs/examples/set-up/create-kind-clusters.sh b/docs/examples/example5-multicluster/create-kind-clusters.sh similarity index 100% rename from docs/examples/set-up/create-kind-clusters.sh rename to docs/examples/example5-multicluster/create-kind-clusters.sh diff --git a/docs/examples/demo-setup.drawio.svg b/docs/examples/example5-multicluster/demo-setup.drawio.svg similarity index 100% rename from docs/examples/demo-setup.drawio.svg rename to docs/examples/example5-multicluster/demo-setup.drawio.svg diff --git a/docs/examples/set-up/kustomization.yaml b/docs/examples/example5-multicluster/kustomization.yaml similarity index 100% rename from docs/examples/set-up/kustomization.yaml rename to docs/examples/example5-multicluster/kustomization.yaml diff --git a/docs/examples/example5-multicluster/london-pools.yaml b/docs/examples/example5-multicluster/london-pools.yaml index 4012778c..36c9f0db 100644 --- a/docs/examples/example5-multicluster/london-pools.yaml +++ b/docs/examples/example5-multicluster/london-pools.yaml @@ -1,54 +1,54 @@ -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: london-network-1 + name: london-prefix-1 spec: - name: london-network-1 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod + familily: IPv4 --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: london-network-2 + name: london-prefix-2 spec: - name: london-network-2 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod + familily: IPv4 --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: london-network-3 + name: london-prefix-3 spec: - name: london-network-3 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod + familily: IPv4 --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: london-network-4 + name: london-prefix-4 spec: - name: london-network-4 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod + familily: IPv4 --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: london-network-5 + name: london-prefix-5 spec: - name: london-network-5 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod + familily: IPv4 diff --git a/docs/examples/set-up/netbox-l2advertisement.yaml b/docs/examples/example5-multicluster/netbox-l2advertisement.yaml similarity index 100% rename from docs/examples/set-up/netbox-l2advertisement.yaml rename to docs/examples/example5-multicluster/netbox-l2advertisement.yaml diff --git a/docs/examples/set-up/netbox-svc.yaml b/docs/examples/example5-multicluster/netbox-svc.yaml similarity index 100% rename from docs/examples/set-up/netbox-svc.yaml rename to docs/examples/example5-multicluster/netbox-svc.yaml diff --git a/docs/examples/set-up/prepare-demo-env.sh b/docs/examples/example5-multicluster/prepare-demo-env.sh similarity index 96% rename from docs/examples/set-up/prepare-demo-env.sh rename to docs/examples/example5-multicluster/prepare-demo-env.sh index dceb42cc..8b01bac9 100755 --- a/docs/examples/set-up/prepare-demo-env.sh +++ b/docs/examples/example5-multicluster/prepare-demo-env.sh @@ -57,4 +57,3 @@ while true; do sleep 5 fi done -kubectl apply --context $CONTEXT -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml diff --git a/docs/examples/example5-multicluster/zurich-pools.yaml b/docs/examples/example5-multicluster/zurich-pools.yaml index a9cff12f..27ebd7be 100644 --- a/docs/examples/example5-multicluster/zurich-pools.yaml +++ b/docs/examples/example5-multicluster/zurich-pools.yaml @@ -1,53 +1,49 @@ -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: zurich-network-1 + name: zurich-prefix-1 spec: - name: zurich-network-1 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod + familiy: IPv4 --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: zurich-network-2 + name: zurich-prefix-2 spec: - name: zurich-network-2 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: zurich-network-3 + name: zurich-prefix-3 spec: - name: zurich-network-3 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: zurich-network-4 + name: zurich-prefix-4 spec: - name: zurich-network-4 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: environment: prod --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: - name: zurich-network-5 + name: zurich-prefix-5 spec: - name: zurich-network-5 tenant: "MY_TENANT" prefixLength: "/30" parentPrefixSelector: From 5b3e3203eac52459cda7c86b12dd547f3bf568c8 Mon Sep 17 00:00:00 2001 From: Joel Studler Date: Thu, 27 Mar 2025 17:26:54 +0100 Subject: [PATCH 26/28] Update examples --- docs/examples/example3-restoration/README.md | 30 +++++++++++------- ...netbox1.yaml => prefixclaim-restore1.yaml} | 5 ++- ...netbox2.yaml => prefixclaim-restore2.yaml} | 5 ++- ...netbox3.yaml => prefixclaim-restore3.yaml} | 5 ++- .../restoration-large-font.drawio.png | Bin 0 -> 299697 bytes .../restoration-large-font.drawio.svg | 4 +++ .../restoration-simple.drawio.svg | 4 --- .../restoration.drawio.svg | 2 +- docs/examples/example4-exhaustion/README.md | 2 +- ...etbox.yaml => prefixclaim-exhaustion.yaml} | 15 ++++----- 10 files changed, 36 insertions(+), 36 deletions(-) rename docs/examples/example3-restoration/{kro-rdg-poolfromnetbox1.yaml => prefixclaim-restore1.yaml} (67%) rename docs/examples/example3-restoration/{kro-rdg-poolfromnetbox2.yaml => prefixclaim-restore2.yaml} (67%) rename docs/examples/example3-restoration/{kro-rdg-poolfromnetbox3.yaml => prefixclaim-restore3.yaml} (67%) create mode 100644 docs/examples/example3-restoration/restoration-large-font.drawio.png create mode 100644 docs/examples/example3-restoration/restoration-large-font.drawio.svg delete mode 100644 docs/examples/example3-restoration/restoration-simple.drawio.svg rename docs/examples/example4-exhaustion/{kro-rdg-poolfromnetbox.yaml => prefixclaim-exhaustion.yaml} (65%) diff --git a/docs/examples/example3-restoration/README.md b/docs/examples/example3-restoration/README.md index 9fce7748..5ad6883c 100644 --- a/docs/examples/example3-restoration/README.md +++ b/docs/examples/example3-restoration/README.md @@ -10,16 +10,16 @@ First, let's create some resources we want to restoration later. ```bash kubectl create ns restoration -echo "\n\nCreating kro-rdg-poolfromnetbox1.yaml" -kubectl -n restoration apply -f kro-rdg-poolfromnetbox1.yaml + +kubectl -n restoration apply -f prefixclaim-restore1.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims -echo "\n\nCreating kro-rdg-poolfromnetbox2.yaml" -kubectl -n restoration apply -f kro-rdg-poolfromnetbox2.yaml + +kubectl -n restoration apply -f prefixclaim-restore2.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims -echo "\n\nCreating kro-rdg-poolfromnetbox3.yaml" -kubectl -n restoration apply -f kro-rdg-poolfromnetbox3.yaml + +kubectl -n restoration apply -f prefixclaim-restore3.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims ``` @@ -44,18 +44,24 @@ Now apply the manifests again and verify they become ready. We apply the manifes ```bash kubectl create ns restoration -echo "\n\nCreating kro-rdg-poolfromnetbox3.yaml" -kubectl -n restoration apply -f kro-rdg-poolfromnetbox3.yaml + +kubectl -n restoration apply -f prefixclaim-restore3.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims -echo "\n\nCreating kro-rdg-poolfromnetbox2.yaml" -kubectl -n restoration apply -f kro-rdg-poolfromnetbox2.yaml + +kubectl -n restoration apply -f prefixclaim-restore2.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims -echo "\n\nCreating kro-rdg-poolfromnetbox1.yaml" -kubectl -n restoration apply -f kro-rdg-poolfromnetbox1.yaml + +kubectl -n restoration apply -f prefixclaim-restore1.yaml kubectl -n restoration wait --for=condition=Ready prefixclaims --all kubectl -n restoration get prefixclaims ``` +Delete Leases to speed up: + +```bash +kubectl -n netbox-operator-system get lease -oname | grep -v netbox | xargs -n1 kubectl -n netbox-operator-system delete +``` + Note that the assigned Prefixes are the same as before. You can also play around with this by just restoring single prefixes. If you're curious about how this is done, make sure to read [the "Restoration from NetBox" section in the main README.md](https://github.com/netbox-community/netbox-operator/tree/main?tab=readme-ov-file#restoration-from-netbox) and to check out the code. Also have a look at the "Netbox Restoration Hash" custom field in NetBox. diff --git a/docs/examples/example3-restoration/kro-rdg-poolfromnetbox1.yaml b/docs/examples/example3-restoration/prefixclaim-restore1.yaml similarity index 67% rename from docs/examples/example3-restoration/kro-rdg-poolfromnetbox1.yaml rename to docs/examples/example3-restoration/prefixclaim-restore1.yaml index d468113b..4f6dcf5b 100644 --- a/docs/examples/example3-restoration/kro-rdg-poolfromnetbox1.yaml +++ b/docs/examples/example3-restoration/prefixclaim-restore1.yaml @@ -1,10 +1,9 @@ --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: name: prefixclaim-restoration-sample-1 spec: - name: prefixclaim-restoration-sample-1 tenant: "Dunder-Mifflin, Inc." preserveInNetbox: true parentPrefixSelector: diff --git a/docs/examples/example3-restoration/kro-rdg-poolfromnetbox2.yaml b/docs/examples/example3-restoration/prefixclaim-restore2.yaml similarity index 67% rename from docs/examples/example3-restoration/kro-rdg-poolfromnetbox2.yaml rename to docs/examples/example3-restoration/prefixclaim-restore2.yaml index 495cd187..1c0c9b12 100644 --- a/docs/examples/example3-restoration/kro-rdg-poolfromnetbox2.yaml +++ b/docs/examples/example3-restoration/prefixclaim-restore2.yaml @@ -1,10 +1,9 @@ --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: name: prefixclaim-restoration-sample-2 spec: - name: prefixclaim-restoration-sample-2 tenant: "Dunder-Mifflin, Inc." preserveInNetbox: true parentPrefixSelector: diff --git a/docs/examples/example3-restoration/kro-rdg-poolfromnetbox3.yaml b/docs/examples/example3-restoration/prefixclaim-restore3.yaml similarity index 67% rename from docs/examples/example3-restoration/kro-rdg-poolfromnetbox3.yaml rename to docs/examples/example3-restoration/prefixclaim-restore3.yaml index 2c995760..9c384150 100644 --- a/docs/examples/example3-restoration/kro-rdg-poolfromnetbox3.yaml +++ b/docs/examples/example3-restoration/prefixclaim-restore3.yaml @@ -1,10 +1,9 @@ --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: name: prefixclaim-restoration-sample-3 spec: - name: prefixclaim-restoration-sample-3 tenant: "Dunder-Mifflin, Inc." preserveInNetbox: true parentPrefixSelector: diff --git a/docs/examples/example3-restoration/restoration-large-font.drawio.png b/docs/examples/example3-restoration/restoration-large-font.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..857818cb52c70690e9949e0b703055cd59f09af4 GIT binary patch literal 299697 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

NsC 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 P@Q=*NGg3*Cx^Djqnq|Xq literal 0 HcmV?d00001 diff --git a/docs/examples/example3-restoration/restoration-large-font.drawio.svg b/docs/examples/example3-restoration/restoration-large-font.drawio.svg new file mode 100644 index 00000000..2e5face7 --- /dev/null +++ b/docs/examples/example3-restoration/restoration-large-font.drawio.svg @@ -0,0 +1,4 @@ + + + +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-oper...
reconcile
reconcile
create & reconcile
create & reconcile
NetBox Operator
NetBox Operator
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
2.0.0.0/32
2.0.0.0/32
2.0.0.1/32
2.0.0.1/32
2.0.0.2/32
2.0.0.2/32
restore by looking up
restoration hash
restore by looking up...
PrefixClaim
PrefixClaim
ownerReference
ownerReference
Prefix
Prefix
PrefixClaim
PrefixClaim
ownerReference
ownerReference
Prefix
Prefix
PrefixClaim
PrefixClaim
ownerReference
ownerReference
Prefix
Prefix
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-restoration/restoration-simple.drawio.svg b/docs/examples/example3-restoration/restoration-simple.drawio.svg deleted file mode 100644 index 61ef35f1..00000000 --- a/docs/examples/example3-restoration/restoration-simple.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
k8s cluster
k8s cluster
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-op...
netbox-operator
netbox-operat...
create
create
User
w/ kubectl
Use...
NetBox REST API
NetBox REST API
Prefix 2.0.0.0/32
Prefix 2.0.0....
Prefix 2.0.0.1/32
Prefix 2.0.0....
Prefix 2.0.0.2/32
Prefix 2.0.0....
restore by looking up
restoration hash
restore by looking up...
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
PrefixClaim
PrefixClaim
IPAddressPool
IPAddressPool
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
PrefixClaim
PrefixClaim
IPAddressPool
IPAddressPool
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
PrefixClaim
PrefixClaim
IPAddressPool
IPAddressPool
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example3-restoration/restoration.drawio.svg b/docs/examples/example3-restoration/restoration.drawio.svg index 0c4de4cf..4bcdca6c 100644 --- a/docs/examples/example3-restoration/restoration.drawio.svg +++ b/docs/examples/example3-restoration/restoration.drawio.svg @@ -1,4 +1,4 @@ -
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
netbox-operator
netbox-operator
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 2.0.0.0/32
Prefix 2.0.0.0/32
Prefix 2.0.0.1/32
Prefix 2.0.0.1/32
Prefix 2.0.0.2/32
Prefix 2.0.0.2/32
restore by looking up
restoration hash
restore by looking up...
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
status:
  prefix: 2.0.0.0/32
kind: PrefixClaim...
kind: IPAddressPool
spec:
  addresses
  - 2.0.0.0/32
kind: IPAddressPool...
kro mapping
kro mapping
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
status:
  prefix: 2.0.0.0/32
kind: PrefixClaim...
kind: IPAddressPool
spec:
  addresses
  - 2.0.0.0/32
kind: IPAddressPool...
kro mapping
kro mapping
kind: MetalLBIPAddressPoolNetBox
kind: MetalLBIPAddressPoolNetBox
kind: PrefixClaim
spec:
  parentPrefixSelector:
    tenant: "Dunder-Mifflin"
  prefixLength: /32
status:
  prefix: 2.0.0.0/32
kind: PrefixClaim...
kind: IPAddressPool
spec:
  addresses
  - 2.0.0.0/32
kind: IPAddressPool...
kro mapping
kro mapping
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file +
k8s cluster kind-london
k8s cluster kind-london
namespace default
namespace default
namespace "advanced"
namespace "advanced"
namespace "netbox-operator"
namespace "netbox-operator"
reconcile
reconcile
create & reconcile
create & reconcile
netbox-operator
netbox-operator
create
create
User
w/ kubectl
User...
NetBox REST API
NetBox REST API
Prefix 2.0.0.0/32
Prefix 2.0.0.0/32
Prefix 2.0.0.1/32
Prefix 2.0.0.1/32
Prefix 2.0.0.2/32
Prefix 2.0.0.2/32
restore by looking up
restoration hash
restore by looking up...
kind: PrefixClaim
spec:
  parentPrefixSelector: 
     tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.0/32
kind: Prefix...
kind: PrefixClaim
spec:
  parentPrefixSelector: 
     tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.0/32
kind: Prefix...
kind: PrefixClaim
spec:
  parentPrefixSelector: 
     tenant: "Dunder-Mifflin"
  prefixLength: /32
kind: PrefixClaim...
ownerReference
ownerReference
kind: Prefix
spec:
  prefix: 2.0.0.0/32
kind: Prefix...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/examples/example4-exhaustion/README.md b/docs/examples/example4-exhaustion/README.md index 8fa33687..d4843fb5 100644 --- a/docs/examples/example4-exhaustion/README.md +++ b/docs/examples/example4-exhaustion/README.md @@ -16,7 +16,7 @@ Apply Resource and show PrefixClaims: ```bash kubectl create ns advanced -kubectl apply -f kro-rdg-poolfromnetbox.yaml +kubectl apply -f prefixclaim-exhaustion.yaml kubectl -n advanced get prefixclaims,prefixes ``` diff --git a/docs/examples/example4-exhaustion/kro-rdg-poolfromnetbox.yaml b/docs/examples/example4-exhaustion/prefixclaim-exhaustion.yaml similarity index 65% rename from docs/examples/example4-exhaustion/kro-rdg-poolfromnetbox.yaml rename to docs/examples/example4-exhaustion/prefixclaim-exhaustion.yaml index 344407fb..6885a1a0 100644 --- a/docs/examples/example4-exhaustion/kro-rdg-poolfromnetbox.yaml +++ b/docs/examples/example4-exhaustion/prefixclaim-exhaustion.yaml @@ -1,10 +1,9 @@ --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: name: prefixclaim-exhaustion-sample-1 spec: - name: prefixclaim-exhaustion-sample-1 tenant: "MY_TENANT" preserveInNetbox: true parentPrefixSelector: @@ -12,12 +11,11 @@ spec: family: IPv4 prefixLength: "/25" --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: name: prefixclaim-exhaustion-sample-2 spec: - name: prefixclaim-exhaustion-sample-2 tenant: "MY_TENANT" preserveInNetbox: true parentPrefixSelector: @@ -25,12 +23,11 @@ spec: family: IPv4 prefixLength: "/25" --- -apiVersion: kro.run/v1alpha1 -kind: MetalLBIPAddressPoolNetBox +apiVersion: netbox.dev/v1 +kind: PrefixClaim metadata: name: prefixclaim-exhaustion-sample-3 spec: - name: prefixclaim-exhaustion-sample-3 tenant: "MY_TENANT" preserveInNetbox: true parentPrefixSelector: From d0a7bb70ae5872190d8b37a17a2279505902f080 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Thu, 27 Mar 2025 17:40:24 +0100 Subject: [PATCH 27/28] add color code drawing for example 2 --- ...ancer-ip-pool-netbox-large-font.drawio.svg | 742 +----------------- 1 file changed, 4 insertions(+), 738 deletions(-) diff --git a/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg b/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg index 1a6f6cc7..ad17f4dc 100644 --- a/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg +++ b/docs/examples/example2-load-balancer-ip/load-balancer-ip-pool-netbox-large-font.drawio.svg @@ -1,738 +1,4 @@ - - - - - - - - - - - - - -
-
-
- - k8s cluster - -
-
-
-
- - k8s cluster - -
-
-
- - - - - - - - - - -
-
-
- - user namespace - -
-
-
-
- - user namespace - -
-
-
- - - - - - - - - - - -
-
-
- - kro namespace - -
-
-
-
- - kro namespace - -
-
-
- - - - - - - - - - - -
-
-
- - kubernetes resource orchstrator | kro - -
-
-
-
- - kubernetes resource... - -
-
-
- - - - - - - -
-
-
-
- - PrefixClaim CR - -
-
- - status - - - : - -
-
- - prefix: - - 2.0.0.0/28 - - -
-
-
-
-
- - PrefixClaim CR... - -
-
-
- - - - - - - - -
-
-
- - reconcile - -
-
-
-
- - reconcile - -
-
-
- - - - - - - - - - -
-
-
-
- - MetalLBIPAddress- - -
-
- - PoolNetBox CR - -
-
-
-
-
- - MetalLBIPAddress-... - -
-
-
- - - - - - - -
-
-
- - namespace metallb-system - -
-
-
-
- - namespace metallb-system - -
-
-
- - - - - - - -
-
-
-
- - IPAddressPool CR - -
-
- - spec: - -
-
- - ipaddresspools: - -
-
- - - - - 2.0.0.0/28 - - -
-
-
-
-
- - IPAddressPool CR... - -
-
-
- - - - -
-
-
- - read status - -
-
-
-
- - read status - -
-
-
- - - - - - - - -
-
-
- - create - -
-
-
-
- - create - -
-
-
- - - - - - - - -
-
-
- - consumer - -
-
-
-
- - consumer - -
-
-
- - - - - - - - -
-
-
- - User -
- w/ kubectl -
-
-
-
-
- - User... - -
-
-
- - - - - - - - -
-
-
- - GitOps -
- w/ Argo or Flux -
-
-
-
-
- - GitOps... - -
-
-
- - - - - - - -
-
-
- - and/or - -
-
-
-
- - and/or - -
-
-
- - - - -
-
-
- - create - -
-
-
-
- - create - -
-
-
- - - - - - - -
-
-
- - NetBox REST API - -
-
-
-
- - NetBox REST API - -
-
-
- - - - - - - -
-
-
- - namespace netbox-operator - -
-
-
-
- - namespace netbox-operator - -
-
-
- - - - - - - - -
-
-
- - create/update/delete - -
-
-
-
- - create/update/delete - -
-
-
- - - - - - - - -
-
-
- - get available prefixes - -
-
-
-
- - get available prefixes - -
-
-
- - - - - - - - - - - -
-
-
- - NetBox Operator - -
-
-
-
- - NetBox Operator - -
-
-
- - - - - - - -
-
-
- - Parent -
- Prefix -
-
-
-
-
- - Parent... - -
-
-
- - - - - - - -
-
-
- - Claimed Prefix - -
-
-
-
- - Claimed Prefix - -
-
-
- - - - - - - - - - -
-
-
- - Matching Prefixes - -
-
-
-
- - Matching Prefixes - -
-
-
- - - - -
-
-
- - get matching prefixes - -
-
-
-
- - get matching prefixes - -
-
-
- - - - - - - -
-
-
-
- - Prefix CR - -
-
-
-
-
- - Prefix CR - -
-
-
- - - - - - - - - - - - -
-
-
- - create - -
-
-
-
- - create - -
-
-
- - - - - - - - -
-
-
- - reconcile - -
-
-
-
- - reconcile - -
-
-
-
- - - - - Text is not SVG - cannot display - - - -
\ No newline at end of file + + + +
k8s cluster
k8s cluster
user namespace
user namespace
kro namespace
kro namespace
kro
kro
PrefixClaim CR
status:
  prefix: 2.0.0.0/28
PrefixClaim CR...
MetalLBIPAddress-
PoolNetBox CR
MetalLBIPAddress-...
namespace metallb-system
namespace metallb-system
IPAddressPool CR
spec: 
 ipaddresspools:
  - 2.0.0.0/28
IPAddressPool CR...
consumer
consumer
User
w/ kubectl
User...
GitOps
w/ Argo or Flux
GitOps...
and/or
and/or
create
create
NetBox REST API
NetBox REST API
namespace netbox-operator
namespace netbox-operator
create/update/delete
create/update/delete
get available prefixes
get available prefixes
NetBox Operator
NetBox Operator
Parent
Prefix
Parent...
Claimed Prefix
Claimed Prefix
Matching Prefixes
Matching Prefixes
get matching prefixes
get matching prefixes
Prefix CR
Prefix CR
create
create
reconcile
reconcile
reconcile
reconcile
create
create
read status
read status
create
create
reconcile
reconcile
Text is not SVG - cannot display
\ No newline at end of file From cf9bc3f7a32b6a1b1024d81bf291b0ceb5872da4 Mon Sep 17 00:00:00 2001 From: bruelea <166021996+bruelea@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:40:36 +0100 Subject: [PATCH 28/28] fix example 5 --- .../example1-getting-started/README.md | 2 +- .../example2-load-balancer-ip/README.md | 25 ++++++++++ docs/examples/example5-multicluster/README.md | 28 ++++++++--- .../create-kind-clusters.sh | 5 +- .../example5-multicluster/prepare-demo-env.sh | 48 ++++--------------- 5 files changed, 60 insertions(+), 48 deletions(-) diff --git a/docs/examples/example1-getting-started/README.md b/docs/examples/example1-getting-started/README.md index ad4721c1..944200b8 100644 --- a/docs/examples/example1-getting-started/README.md +++ b/docs/examples/example1-getting-started/README.md @@ -19,7 +19,7 @@ kubectl port-forward deploy/netbox 8080:8080 2. Open in your favorite browser and log in with the username `admin` and password `admin` 3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' -# 0.3 Navigate to the example +# 0.3 Navigate to the example folder Navigate to 'docs/examples/example1-getting-started' to run the examples below diff --git a/docs/examples/example2-load-balancer-ip/README.md b/docs/examples/example2-load-balancer-ip/README.md index ef102175..c365b518 100644 --- a/docs/examples/example2-load-balancer-ip/README.md +++ b/docs/examples/example2-load-balancer-ip/README.md @@ -6,6 +6,31 @@ So we have Prefixes represented as Kubernetes Resources. Now what can we do with We use kro.run to glue this to MetalLB IPAddressPools +### 0.1 Create a local cluster with nebox-installed + +1. use the 'create-kind' and 'deploy-kind' targets from the Makefile to create a kind cluster and deploy NetBox and NetBox Operator on it +```bash +make create-kind +make deploy-kind +``` + +### 0.2 Manually Create a Prefix in NetBox + +Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. + +1. Port-forward NetBox: +```bash +kubectl port-forward deploy/netbox 8080:8080 +``` +2. Open in your favorite browser and log in with the username `admin` and password `admin` +3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' + +### 0.3 Navigate to the example folder + +Navigate to 'docs/examples/example2-load-balancer-ip/' to run the examples below + +## Example Steps + 0. Install kro and metallb with the installation script `docs/examples/example2-load-balancer-ip/prepare-demo-env.sh` Then navigate to 'docs/examples/example2-load-balancer-ip' to follow the steps below. diff --git a/docs/examples/example5-multicluster/README.md b/docs/examples/example5-multicluster/README.md index bb5068ca..7ea53c64 100644 --- a/docs/examples/example5-multicluster/README.md +++ b/docs/examples/example5-multicluster/README.md @@ -1,4 +1,4 @@ -# Example 4: Advanced Feature Multi Cluster Support +# Example 5: Advanced Feature Multi Cluster Support ## Introduction @@ -6,18 +6,34 @@ NetBox Operator uses NetBox to avoid IP overlaps. This means that we can use Net This example shows how to claim multiple prefixes from different clusters and make them available as metalLB ip address pools. -Navigate to 'docs/examples/example2-krm-glue' to run the following commands. +### 0.1 Create a local cluster with nebox-installed -0. Install kro and metallb with the installation script `docs/examples/example5-multicluster/prepare-demo-env.sh` -Then navigate to 'docs/examples/edocs/examples/example5-multicluster' to follow the steps below. +1. set up your local environment to run the following examples with the set up script 'docs/examples/example5-multicluster/prepare-demo-env.sh' + +### 0.2 Manually Create a Prefix in NetBox + +Before prefixes and ip addresses can be claimed with the NetBox operator, a prefix has to be created in NetBox. + +1. Port-forward NetBox: +```bash +kubectl port-forward deploy/netbox 8080:8080 +``` +2. Open in your favorite browser and log in with the username `admin` and password `admin` +3. Create a new prefix '3.0.0.64/26' with custom field 'environment: prod' + +### 0.3 Navigate to the example folder + +Navigate to 'docs/examples/example5-multicluster/' to run the examples below + +## Example Steps 1. Create ip address pools on the london cluster ```bash -kubectl apply --context kind-london -f docs/examples/example2-multicluster/london-pools.yaml +kubectl apply --context kind-london -f docs/examples/example5-multicluster/london-pools.yaml ``` 2. Create ip address pool on the zurich cluster ```bash -kubectl create --context kind-zurich -f docs/examples/example2-multicluster/zurich-pools.yaml +kubectl create --context kind-zurich -f docs/examples/example5-multicluster/zurich-pools.yaml ``` 3. Look up the created prefix claims ```bash diff --git a/docs/examples/example5-multicluster/create-kind-clusters.sh b/docs/examples/example5-multicluster/create-kind-clusters.sh index f9db0288..5ff58bdd 100755 --- a/docs/examples/example5-multicluster/create-kind-clusters.sh +++ b/docs/examples/example5-multicluster/create-kind-clusters.sh @@ -20,7 +20,7 @@ i=0 # Loop to create the specified number of clusters for clustername in "$@"; do - config_file="docs/examples/set-up/cluster-cfg.yaml" + config_file="docs/examples/example5-multicluster/cluster-cfg.yaml" temp_config="tmp/cluster-$clustername-cfg.yaml" i=$((i + 1)) @@ -39,6 +39,9 @@ for clustername in "$@"; do fi kind create cluster --name $clustername --config $temp_config || { echo -e "${RED}Error: Failed to create cluster ${clustername}${NC}"; rm -f "$temp_config"; exit 1; } + # Install MetalLB + kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml + else echo -e "${RED}Error: Configuration file $config_file not found${NC}" exit 1 diff --git a/docs/examples/example5-multicluster/prepare-demo-env.sh b/docs/examples/example5-multicluster/prepare-demo-env.sh index 8b01bac9..67db95f6 100755 --- a/docs/examples/example5-multicluster/prepare-demo-env.sh +++ b/docs/examples/example5-multicluster/prepare-demo-env.sh @@ -1,59 +1,27 @@ #!/bin/bash set -e -# create the kind clusters zurich and london -./docs/examples/set-up/create-kind-clusters.sh zurich london +#create the kind clusters zurich and london +./docs/examples/example5-multicluster/create-kind-clusters.sh zurich london # install netbox in the london cluster and load demo data kubectl config use-context kind-london ./kind/deploy-netbox.sh london "4.1.8" default -kubectl apply -f docs/examples/set-up/netbox-svc.yaml -kubectl apply -f docs/examples/set-up/netbox-l2advertisement.yaml # install NetBox Operator kubectl config use-context kind-london kind load docker-image netbox-operator:build-local --name london kind load docker-image netbox-operator:build-local --name london # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image -kustomize build docs/examples/set-up/ | kubectl apply -f - - +kustomize build docs/examples/example5-multicluster/ | kubectl apply -f - kubectl config use-context kind-zurich kind load docker-image netbox-operator:build-local --name zurich kind load docker-image netbox-operator:build-local --name zurich # fixes an issue with podman where the image is not correctly tagged after the first kind load docker-image -kustomize build docs/examples/set-up/ | kubectl apply -f - +kustomize build docs/examples/example5-multicluster/ | kubectl apply -f - kind load docker-image curlimages/curl --name zurich kind load docker-image curlimages/curl --name zurich kubectl run curl --image curlimages/curl --image-pull-policy=Never -- sleep infinity -DEPLOYMENT_NAME=netbox-operator-controller-manager -NAMESPACE=netbox-operator-system -CONTEXT=kind-london - -while true; do - # Check if the deployment is ready - READY_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.readyReplicas}') - DESIRED_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.replicas}') - - if [[ "$READY_REPLICAS" == "$DESIRED_REPLICAS" ]] && [[ "$READY_REPLICAS" -gt 0 ]]; then - echo "Deployment $DEPLOYMENT_NAME in cluster $CONTEXT is ready." - break - else - echo "Waiting... Ready replicas in cluster $CONTEXT: $READY_REPLICAS / $DESIRED_REPLICAS" - sleep 5 - fi -done -kubectl apply --context $CONTEXT -f docs/examples/set-up/metallb-ip-address-pool-netbox.yaml - -CONTEXT=kind-zurich -while true; do - # Check if the deployment is ready - READY_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.readyReplicas}') - DESIRED_REPLICAS=$(kubectl --context $CONTEXT get deployment $DEPLOYMENT_NAME -n $NAMESPACE -o jsonpath='{.status.replicas}') - - if [[ "$READY_REPLICAS" == "$DESIRED_REPLICAS" ]] && [[ "$READY_REPLICAS" -gt 0 ]]; then - echo "Deployment $DEPLOYMENT_NAME in cluster $CONTEXT is ready." - break - else - echo "Waiting... Ready replicas in cluster $CONTEXT: $READY_REPLICAS / $DESIRED_REPLICAS" - sleep 5 - fi -done +# expose netbox service +kubectl config use-context kind-london +kubectl apply -f docs/examples/example5-multicluster/netbox-svc.yaml +kubectl apply -f docs/examples/example5-multicluster/netbox-l2advertisement.yaml