diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6faccd2..b691d7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,6 +110,124 @@ jobs: echo " ok $f" done + e2e: + name: End-to-end tests (Chainsaw) + runs-on: ubuntu-latest + env: + TASK_X_REMOTE_TASKFILES: "1" + # test-infra writes kubeconfig here when running outside the test-infra repo + KUBECONFIG: .test-infra/kubeconfig + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install kind + run: | + KIND_VERSION="v0.30.0" + curl -fsSL -o kind "https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64" + chmod +x kind + sudo mv kind /usr/local/bin/kind + + - name: Install kubectl + run: | + KUBECTL_VERSION="$(curl -sL https://dl.k8s.io/release/stable.txt)" + curl -fsSL -o kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/kubectl + + - name: Install kustomize + run: | + curl -sLo /tmp/kustomize.tgz https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz + tar -xzf /tmp/kustomize.tgz -C /tmp + sudo mv /tmp/kustomize /usr/local/bin/ + + - name: Install Helm + run: | + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Install Flux CLI + run: | + curl -fsSL https://fluxcd.io/install.sh | sudo bash + + - name: Install Chainsaw + run: | + CHAINSAW_VERSION="v0.2.12" + curl -fsSL -o /tmp/chainsaw.tgz \ + "https://github.com/kyverno/chainsaw/releases/download/${CHAINSAW_VERSION}/chainsaw_linux_amd64.tar.gz" + tar -xzf /tmp/chainsaw.tgz -C /tmp chainsaw + sudo mv /tmp/chainsaw /usr/local/bin/chainsaw + + - name: Spin up kind cluster (test-infra) + run: task --yes test-infra:cluster-up + + - name: Build IPAM container image + run: task --yes dev:build + + - name: Load image into kind + run: task --yes dev:load + + - name: Create control-plane-ca configmap + run: | + # The aggregated apiserver uses --requestheader-client-ca-file to verify + # the front proxy identity. In kind the cert lives in the + # extension-apiserver-authentication ConfigMap in kube-system. + kubectl create namespace ipam-system --dry-run=client -o yaml | kubectl apply -f - + kubectl get configmap extension-apiserver-authentication -n kube-system \ + -o jsonpath='{.data.requestheader-client-ca-file}' > /tmp/requestheader-ca.crt + kubectl create configmap control-plane-ca \ + -n ipam-system \ + --from-file=ca.crt=/tmp/requestheader-ca.crt \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy IPAM service + run: | + kubectl apply -k config/overlays/test-infra + # Wait for cert-manager to issue the TLS secret before the apiserver pods can mount it. + kubectl -n ipam-system wait certificate/ipam-tls \ + --for=condition=Ready --timeout=120s + kubectl -n ipam-system wait helmrelease/postgres \ + --for=condition=Ready --timeout=300s + kubectl -n ipam-system wait pod \ + -l app.kubernetes.io/name=postgresql \ + --for=condition=Ready --timeout=180s + kubectl wait --for=condition=Ready pod \ + -l app=ipam-apiserver -n ipam-system --timeout=180s + kubectl wait --for=condition=Available \ + apiservice/v1alpha1.ipam.miloapis.com --timeout=180s + + - name: Run Chainsaw e2e suites + run: chainsaw test test/e2e/ + + - name: Dump diagnostics on failure + if: failure() + run: | + echo "=== Pods ===" + kubectl get pods -A + echo "=== IPAM pod describe ===" + kubectl describe pods -n ipam-system -l app=ipam-apiserver || true + echo "=== IPAM apiserver logs ===" + kubectl logs -n ipam-system -l app=ipam-apiserver --all-containers --tail=100 || true + echo "=== CertificateRequests ===" + kubectl get certificaterequests -n ipam-system -o wide || true + echo "=== Events ===" + kubectl get events -n ipam-system --sort-by='.lastTimestamp' | tail -60 || true + echo "=== APIService ===" + kubectl get apiservice v1alpha1.ipam.miloapis.com -o yaml || true + + - name: Tear down kind cluster + if: always() + run: task --yes test-infra:cluster-down + observability: name: Verify observability artifacts runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index f3c1cd2..ce649f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Build stage. Debian-based so the race detector (which requires CGO + glibc) # can be enabled via --build-arg RACE=-race. -FROM golang:1.26-bookworm AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS builder # Build arguments for version injection ARG VERSION=dev diff --git a/cmd/ipam/main.go b/cmd/ipam/main.go index da84084..df464f7 100644 --- a/cmd/ipam/main.go +++ b/cmd/ipam/main.go @@ -24,9 +24,9 @@ func NewIPAMServerCommand() *cobra.Command { Short: "IPAM service apiserver", Long: `IPAM is a Kubernetes-native IP Address Management service. -It provides synchronous CIDR, IP, and ASN allocation through IPPrefix, -IPPrefixClaim, IPAddress, IPAddressClaim, ASNPool, and ASNClaim resources -exposed as an aggregated Kubernetes API server.`, +It provides synchronous CIDR and IP allocation through IPPool, IPClaim, +IPAllocation, ASNPool, and ASNClaim resources exposed as an aggregated +Kubernetes API server.`, } cmd.AddCommand(NewServeCommand()) diff --git a/config/components/k6-performance-tests/README.md b/config/components/k6-performance-tests/README.md index 87058fa..a71dc9c 100644 --- a/config/components/k6-performance-tests/README.md +++ b/config/components/k6-performance-tests/README.md @@ -21,7 +21,7 @@ task -t test/load/Taskfile.yaml k6:run TEST=throughput | TestRun | Script | Purpose | |------------------------|---------------------------------|----------------------------------------| | `ipam-perf-setup` | `setup-pools.js` | One-time pool/namespace provisioning | -| `ipam-perf-throughput` | `prefix-claim-throughput.js` | IPPrefixClaim p95 < 500ms | +| `ipam-perf-throughput` | `prefix-claim-throughput.js` | IPClaim p95 < 500ms | | `ipam-perf-asn-throughput` | `asn-claim-throughput.js` | ASNClaim p95 < 500ms | | `ipam-perf-exhaustion` | `pool-exhaustion.js` | Deny-path p95 < 200ms | | `ipam-perf-reads` | `read-latency.js` | List/get latency under load | diff --git a/config/components/k6-performance-tests/generated/concurrent-claims.js b/config/components/k6-performance-tests/generated/concurrent-claims.js index 4481a0a..eac6ea4 100644 --- a/config/components/k6-performance-tests/generated/concurrent-claims.js +++ b/config/components/k6-performance-tests/generated/concurrent-claims.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); +} + +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); +} + +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; +} + +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -474,41 +443,37 @@ export function listASNClaimsForProject(ns, projectID) { // concurrent-claims.js // -// Stress-tests the IPAM service's concurrency guarantee: concurrent -// IPPrefixClaim CREATE requests from multiple VUs must always produce -// non-overlapping CIDRs. A single /20 parent pool is used with /28 children -// (256 slots), so many rounds of concurrent claims can run without exhaustion. -// -// The key assertion is correctness under concurrency — the SELECT...FOR UPDATE -// pool-level lock must produce non-overlapping allocations even at high -// parallelism. This complements prefix-claim-throughput.js (which measures -// latency) by asserting allocation correctness. +// Stress-tests the IPAM service's concurrency guarantee: concurrent IPClaim +// CREATE requests must always produce non-overlapping CIDRs. // // Approach: -// - Burst scenario: each VU claims a /28 from perf-prefix-0, then deletes -// it inline so the pool stays available for subsequent iterations. This -// measures latency under contention. -// - Uniqueness scenario: a single VU drains the pool sequentially after the -// burst finishes, recording every status.allocatedCIDR and asserting each -// value is unique. Any duplicate increments ipam_duplicate_cidrs, which -// fails the run via a count==0 threshold. +// - burst scenario: constant-vus for DURATION. Each VU creates and deletes +// a /28 claim inline so the pool stays available for subsequent iterations. +// Measures p95 latency under SELECT...FOR UPDATE contention. +// - uniqueness scenario (single VU, runs after burst): +// Phase 1 — concurrent batch: fires VUS simultaneous creates via +// http.batch() and asserts all returned status.allocatedCIDR values are +// unique. http.batch() dispatches all requests in parallel; if +// SELECT...FOR UPDATE regresses, two requests could race to the same CIDR. +// This is the hard concurrent correctness check. +// Phase 2 — sequential drain: fills remaining pool capacity serially, +// asserting uniqueness of each successive allocation. // // SLO-aligned thresholds: // - p95 create latency < 500ms (same as prefix-claim-throughput) -// - success rate > 0.95 (errors or overlaps counted as failures) +// - success rate > 0.95 // - http_req_failed < 5% -// - ipam_duplicate_cidrs == 0 (allocations must never collide) -// - ipam_concurrent_missing_status == 0 (status.allocatedCIDR must be set) +// - ipam_duplicate_cidrs == 0 (hard gate — any duplicate fails the run) +// - ipam_concurrent_missing_status == 0 // -// Run setup-pools.js first (uses perf-prefix-0 pool from project 0). +// Run setup-pools.js first (uses perf-prefix-0 from project 0). // // Configuration: -// VUS - Concurrent virtual users (default 50) -// DURATION - Test duration (default 2m) +// VUS - Concurrent virtual users (default 50) +// DURATION - Burst duration (default 2m) // NAMESPACE_COUNT - Namespace pool size (default 10) -// IPAM_API_URL - Apiserver URL +// IPAM_API_URL - Apiserver URL -import { check, sleep } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; const VUS = parseInt(__ENV.VUS || '50'); const DURATION = __ENV.DURATION || '2m'; @@ -542,9 +507,8 @@ export const options = { exec: 'burst', }, uniqueness_check: { - // Drain the pool sequentially after the burst is done so we can - // assert non-overlapping CIDRs. The burst leaves the pool empty - // because every iteration deletes its own claim. + // Runs after burst: concurrent batch (http.batch) then sequential drain, + // both asserting strict CIDR uniqueness. executor: 'shared-iterations', vus: 1, iterations: 1, @@ -586,7 +550,7 @@ export function burst() { const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); const claimName = `concurrent-claim-${__VU}-${__ITER}`; - const createRes = createPrefixClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); + const createRes = createIPClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); if (createRes.status === 201) { concurrentCreated.add(1); @@ -596,12 +560,12 @@ export function burst() { if (extractCIDR(createRes) === null) { missingStatus.add(1); if (__ITER < 5) { - console.error(`prefix claim ${claimName} created without status.allocatedCIDR: ${createRes.body}`); + console.error(`IPClaim ${claimName} created without status.allocatedCIDR: ${createRes.body}`); } } // Immediately delete so the pool stays available for subsequent iterations. - const delRes = deletePrefixClaimForProject(ns, claimName, PROJECT); + const delRes = deleteIPClaimForProject(ns, claimName, PROJECT); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { concurrentErrors.add(1); } @@ -627,52 +591,107 @@ export function burst() { } } -// uniqueness drains the pool sequentially with a single VU, asserting every -// status.allocatedCIDR is unique. perf-prefix-0 is /16 with /28 children, so -// we'll cap the drain at 256 (slot count) plus a small slack. +// uniqueness runs two phases after the burst completes: // -// Modelled on ipaddress-claim-concurrent.js#uniqueness — same pattern, -// CIDR strings instead of IP strings. +// Phase 1 — concurrent batch: fires VUS simultaneous creates via http.batch() +// so requests contend for the SELECT...FOR UPDATE pool lock at the same time. +// All VUS responses are collected and their status.allocatedCIDR values are +// checked for duplicates. This is the hard concurrent correctness assertion: +// any concurrency regression that allows two requests to allocate the same CIDR +// will produce a duplicate here and fail the ipam_duplicate_cidrs threshold. +// +// Phase 2 — sequential drain: fills remaining pool capacity one-by-one, +// asserting every successive CIDR is unique. Confirms correctness under +// non-contended conditions as well. export function uniqueness() { const ns = nsFor(0); - const seen = {}; - const claims = []; - let dupCount = 0; - const maxIters = 256 + 16; + let totalDups = 0; + + // --- Phase 1: concurrent batch --- + const batchRequests = []; + for (let i = 0; i < VUS; i++) { + batchRequests.push(buildIPClaimRequest(ns, `concurrent-batch-${i}`, POOL_NAME, 28, PROJECT)); + } + const batchResponses = http.batch(batchRequests); + + const batchSeen = {}; + const batchClaims = []; + for (let i = 0; i < batchResponses.length; i++) { + const res = batchResponses[i]; + if (res.status === 507) { + // Pool unexpectedly exhausted from burst leftovers — log and skip. + console.warn(`batch slot ${i}: 507 — leftover claims from burst may be blocking`); + continue; + } + if (res.status !== 201) { + console.error(`batch slot ${i}: status=${res.status} body=${res.body}`); + continue; + } + const cidr = extractCIDR(res); + if (cidr === null) { + missingStatus.add(1); + batchClaims.push(`concurrent-batch-${i}`); + continue; + } + if (batchSeen[cidr]) { + totalDups++; + console.error( + `DUPLICATE CIDR ${cidr}: concurrent-batch-${batchSeen[cidr]} and concurrent-batch-${i}`, + ); + } else { + batchSeen[cidr] = i; + uniqueAllocated.add(1); + } + batchClaims.push(`concurrent-batch-${i}`); + } + console.log( + `concurrent batch: ${batchClaims.length}/${VUS} claims, ${Object.keys(batchSeen).length} unique CIDRs, ${totalDups} duplicates`, + ); + + // Clean up batch claims before sequential drain. + for (const name of batchClaims) { + deleteIPClaimForProject(ns, name, PROJECT); + } + + // --- Phase 2: sequential drain --- + const seenSeq = {}; + const seqClaims = []; + let seqDups = 0; + const maxIters = 256 + 16; // /16 with /28 children = 256 slots for (let i = 0; i < maxIters; i++) { const claimName = `concurrent-unique-${i}`; - const res = createPrefixClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); + const res = createIPClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); if (res.status === 507) break; if (res.status !== 201) { - console.error(`uniqueness create ${i}: status=${res.status} body=${res.body}`); + console.error(`sequential drain ${i}: status=${res.status} body=${res.body}`); continue; } const cidr = extractCIDR(res); if (cidr === null) { missingStatus.add(1); - claims.push(claimName); + seqClaims.push(claimName); continue; } - if (seen[cidr]) { - dupCount++; - console.error(`DUPLICATE CIDR ${cidr} returned for both ${seen[cidr]} and ${claimName}`); + if (seenSeq[cidr]) { + seqDups++; + console.error(`DUPLICATE CIDR ${cidr} returned for both ${seenSeq[cidr]} and ${claimName}`); } else { - seen[cidr] = claimName; + seenSeq[cidr] = claimName; uniqueAllocated.add(1); } - claims.push(claimName); + seqClaims.push(claimName); } - if (dupCount > 0) { - duplicateCIDRs.add(dupCount); + totalDups += seqDups; + if (totalDups > 0) { + duplicateCIDRs.add(totalDups); } console.log( - `uniqueness scenario: ${claims.length} claims, ${Object.keys(seen).length} unique CIDRs, ${dupCount} duplicates`, + `sequential drain: ${seqClaims.length} claims, ${Object.keys(seenSeq).length} unique CIDRs, ${seqDups} duplicates`, ); - // Drain so a follow-up run starts clean. - for (const name of claims) { - deletePrefixClaimForProject(ns, name, PROJECT); + for (const name of seqClaims) { + deleteIPClaimForProject(ns, name, PROJECT); } } diff --git a/config/components/k6-performance-tests/generated/cross-project-claim-throughput.js b/config/components/k6-performance-tests/generated/cross-project-claim-throughput.js index 481fe56..75347bc 100644 --- a/config/components/k6-performance-tests/generated/cross-project-claim-throughput.js +++ b/config/components/k6-performance-tests/generated/cross-project-claim-throughput.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); } +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -474,12 +443,11 @@ export function listASNClaimsForProject(ns, projectID) { // cross-project-claim-throughput.js // -// Dedicated cross-project IPPrefixClaim throughput test. Each VU acts as a +// Dedicated cross-project IPClaim throughput test. Each VU acts as a // non-owner project (any project N != 0) claiming a /28 from project 0's // shared pool (`perf-shared-prefix`). The claim spec carries a -// `prefixRef.projectRef` pointing at project 0, and the request itself -// carries the caller's project identity in the X-Remote-Extra parent -// headers. +// `poolRef.projectRef` pointing at project 0, and the request itself carries +// the caller's project identity in the X-Remote-Extra parent headers. // // This is the slow path that exercises whatever cross-project authorization // (SubjectAccessReview or similar) the server adds — thresholds are wider @@ -537,7 +505,7 @@ export default function () { const callerProject = projectIDFor(callerIdx); const claimName = `xclaim-${__VU}-${__ITER}`; - const createRes = createCrossProjectPrefixClaim( + const createRes = createCrossProjectIPClaim( ns, claimName, SHARED_PREFIX, @@ -565,7 +533,7 @@ export default function () { } if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, callerProject); + const delRes = deleteIPClaimForProject(ns, claimName, callerProject); crossProjectDelete.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { crossProjectErrors.add(1); diff --git a/config/components/k6-performance-tests/generated/host-prefix-claim-concurrent.js b/config/components/k6-performance-tests/generated/host-prefix-claim-concurrent.js new file mode 100644 index 0000000..e22bc4d --- /dev/null +++ b/config/components/k6-performance-tests/generated/host-prefix-claim-concurrent.js @@ -0,0 +1,655 @@ +// Code generated by hack/bundle-k6.sh. DO NOT EDIT. +// Source: test/load/src/host-prefix-claim-concurrent.js +// Lib: test/load/lib/ipam-client.js + +// Shared HTTP client for the IPAM apiserver. Provides typed helpers for the +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. +// +// Configuration via environment variables: +// IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) +// IPAM_TOKEN - Explicit bearer token (overrides in-cluster SA token) +// IPAM_TOKEN_FILE - Path to a file containing a bearer token (default: SA token path) +// K6_INSECURE_SKIP_TLS_VERIFY - Skip TLS verification (default: true) +// +// When running inside the k6 operator, the test pod's ServiceAccount token +// is mounted at /var/run/secrets/kubernetes.io/serviceaccount/token. The +// client reads it automatically at init time if IPAM_TOKEN isn't set. + +import http from 'k6/http'; + +export const BASE_URL = __ENV.IPAM_API_URL || 'http://localhost:8001'; +export const API_GROUP = 'ipam.miloapis.com'; +export const API_VERSION = 'v1alpha1'; +export const API_BASE = `${BASE_URL}/apis/${API_GROUP}/${API_VERSION}`; + +const DEFAULT_TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'; + +function loadToken() { + if (__ENV.IPAM_TOKEN) { + return __ENV.IPAM_TOKEN; + } + const path = __ENV.IPAM_TOKEN_FILE || DEFAULT_TOKEN_PATH; + try { + return open(path).trim(); + } catch (e) { + return ''; + } +} + +const TOKEN = loadToken(); + +export function defaultHeaders() { + const h = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + if (TOKEN) { + h['Authorization'] = `Bearer ${TOKEN}`; + } + return h; +} + +function defaultParams(tag) { + return { + headers: defaultHeaders(), + tags: { operation: tag }, + }; +} + +// Returns k6 params with Milo tenant headers identifying the calling project. +// Merges with defaultHeaders() so auth + content-type are preserved. +export function withProject(projectID) { + return { + headers: { + ...defaultHeaders(), + 'X-Remote-Extra-Iam.Miloapis.Com.Parent-Api-Group': 'resourcemanager.miloapis.com', + 'X-Remote-Extra-Iam.Miloapis.Com.Parent-Type': 'Project', + 'X-Remote-Extra-Iam.Miloapis.Com.Parent-Name': projectID, + }, + tags: {}, + }; +} + +// withProjectTagged is like withProject but also sets an operation tag. +export function withProjectTagged(projectID, tag) { + const p = withProject(projectID); + p.tags = { operation: tag }; + return p; +} + +// --- Generic helpers --- + +export function ipamGet(path, tag) { + return http.get(`${API_BASE}${path}`, defaultParams(tag || 'get')); +} + +export function ipamPost(path, body, tag) { + return http.post(`${API_BASE}${path}`, JSON.stringify(body), defaultParams(tag || 'post')); +} + +export function ipamDelete(path, tag) { + return http.del(`${API_BASE}${path}`, null, defaultParams(tag || 'delete')); +} + +export function ipamList(path, tag) { + return http.get(`${API_BASE}${path}`, defaultParams(tag || 'list')); +} + +// --- Path helpers --- + +export function nsFor(n) { + return `ipam-perf-${n}`; +} + +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { + return name + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; +} + +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { + return name + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; +} + +export function asnClaimPath(ns, name) { + return name + ? `/namespaces/${ns}/asnclaims/${name}` + : `/namespaces/${ns}/asnclaims`; +} + +export function asnPoolPath(name) { + return name ? `/asnpools/${name}` : '/asnpools'; +} + +export function asnPoolClassPath(name) { + return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; +} + +// --- Resource builders --- + +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { + return { + apiVersion: `${API_GROUP}/${API_VERSION}`, + kind: 'IPPool', + metadata: { name }, + spec: { + cidr, + ipFamily, + visibility, + allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, + }, + }; +} + +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { + return { + apiVersion: `${API_GROUP}/${API_VERSION}`, + kind: 'IPClaim', + metadata: { name, namespace: ns }, + spec: { + ipFamily, + prefixLength, + poolRef: { name: poolName }, + reclaimPolicy, + }, + }; +} + +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { + return { + apiVersion: `${API_GROUP}/${API_VERSION}`, + kind: 'IPClaim', + metadata: { name, namespace: ns }, + spec: { + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', + }, + }; +} + +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { + return { + apiVersion: `${API_GROUP}/${API_VERSION}`, + kind: 'ASNPoolClass', + metadata: { name }, + spec: { visibility }, + }; +} + +export function asnPool(name, ranges, classRef) { + return { + apiVersion: `${API_GROUP}/${API_VERSION}`, + kind: 'ASNPool', + metadata: { name }, + spec: { ranges, classRef: { name: classRef } }, + }; +} + +export function asnClaim(ns, name, poolRef) { + return { + apiVersion: `${API_GROUP}/${API_VERSION}`, + kind: 'ASNClaim', + metadata: { name, namespace: ns }, + spec: { poolRef: { name: poolRef } }, + }; +} + +export function asnClaimWithClassRef(ns, name, classRefName) { + return { + apiVersion: `${API_GROUP}/${API_VERSION}`, + kind: 'ASNClaim', + metadata: { name, namespace: ns }, + spec: { classRef: { name: classRefName } }, + }; +} + +// --- Typed helper functions --- + +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); +} + +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); +} + +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); +} + +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); +} + +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); +} + +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); +} + +// ASN helpers. +export function createASNClaim(ns, name, poolRef) { + return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); +} + +export function deleteASNClaim(ns, name) { + return ipamDelete(asnClaimPath(ns, name), 'asn_claim_delete'); +} + +export function getASNClaim(ns, name) { + return ipamGet(asnClaimPath(ns, name), 'asn_claim_get'); +} + +export function listASNClaims(ns) { + return ipamList(asnClaimPath(ns), 'asn_claim_list'); +} + +export function createASNPoolClass(name, opts) { + return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); +} + +export function createASNPool(name, ranges, classRef) { + return ipamPost(asnPoolPath(), asnPool(name, ranges, classRef), 'asn_pool_create'); +} + +// --- Namespace helpers (core API) --- + +export function createNamespace(name) { + const body = { + apiVersion: 'v1', + kind: 'Namespace', + metadata: { name }, + }; + return http.post(`${BASE_URL}/api/v1/namespaces`, JSON.stringify(body), defaultParams('ns_create')); +} + +export function deleteNamespace(name) { + return http.del(`${BASE_URL}/api/v1/namespaces/${name}`, null, defaultParams('ns_delete')); +} + +// --- RBAC helpers (core API, used by setup) --- + +export function createClusterRole(name, rules) { + const body = { + apiVersion: 'rbac.authorization.k8s.io/v1', + kind: 'ClusterRole', + metadata: { name }, + rules, + }; + return http.post( + `${BASE_URL}/apis/rbac.authorization.k8s.io/v1/clusterroles`, + JSON.stringify(body), + defaultParams('cluster_role_create'), + ); +} + +export function createClusterRoleBinding(name, roleName, subjects) { + const body = { + apiVersion: 'rbac.authorization.k8s.io/v1', + kind: 'ClusterRoleBinding', + metadata: { name }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: roleName, + }, + subjects, + }; + return http.post( + `${BASE_URL}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`, + JSON.stringify(body), + defaultParams('cluster_role_binding_create'), + ); +} + +// --- Multi-tenant helpers --- + +// projectIDFor returns the perf project ID for index n. +export function projectIDFor(n) { + return `ipam-perf-${n}`; +} + +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; +} + +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); +} + +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); +} + +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); +} + +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); +} + +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); +} + +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); +} + +export function createASNClaimForProject(ns, name, poolRef, projectID) { + const body = asnClaim(ns, name, poolRef); + const params = withProjectTagged(projectID, 'asn_claim_create'); + return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); +} + +export function deleteASNClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'asn_claim_delete'); + return http.del(`${API_BASE}${asnClaimPath(ns, name)}`, null, params); +} + +// createASNClaimWithClassRefForProject posts an ASNClaim that references a +// class (not a pool). +export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { + const body = asnClaimWithClassRef(ns, name, classRefName); + const params = withProjectTagged(projectID, 'asn_claim_create'); + return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); +} + +// LIST helpers used by the read-latency scenarios. All accept the project +// tenant headers so reads stay scoped to the requesting tenant. +export function listASNPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'asn_pool_list'); + return http.get(`${API_BASE}${asnPoolPath()}`, params); +} + +export function listASNClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'asn_claim_list'); + return http.get(`${API_BASE}${asnClaimPath(ns)}`, params); +} + +// host-prefix-claim-concurrent.js +// +// Measures the throughput and concurrency safety of host-route allocation: +// IPClaim creates with prefixLength: 32 (IPv4 /32) against a dedicated /24 +// pool. Single-address allocation via IPClaim replaced the former +// IPAddressClaim resource. +// +// Approach: +// - setup() creates a dedicated /24 IPPool (10.60.0.0/24, 256 addresses). +// - Each VU iteration creates a /32 IPClaim and deletes it inline so the +// pool stays available for subsequent iterations. +// - All returned status.allocatedCIDR values must be unique; the +// SELECT...FOR UPDATE pool-row lock guarantees this. +// - teardown() removes all claims and the pool. +// +// Thresholds (matches prefix-claim-throughput.js): +// - p95 create latency < 500ms, p99 < 2000ms (success phase) +// - success rate > 0.95 +// - http_req_failed < 5% +// - ipam_host_missing_status == 0 (status.allocatedCIDR must be populated) +// - ipam_host_duplicate == 0 (no two claims may share a CIDR) +// +// Configuration: +// VUS - Concurrent virtual users (default 10) +// DURATION - Test duration (default 2m) +// NAMESPACE_COUNT - Namespace pool size (default 10, must match setup-pools.js) +// POOL_CIDR - Parent CIDR for the dedicated pool (default 10.60.0.0/24) +// IPAM_API_URL - Apiserver URL + +import { check } from 'k6'; +import { Counter, Rate, Trend } from 'k6/metrics'; +const VUS = parseInt(__ENV.VUS || '10'); +const DURATION = __ENV.DURATION || '2m'; +const NAMESPACE_COUNT = parseInt(__ENV.NAMESPACE_COUNT || '10'); +const POOL_CIDR = __ENV.POOL_CIDR || '10.60.0.0/24'; + +const POOL_NAME = 'perf-host-claim-pool'; +const PROJECT = projectIDFor(0); + +// /24 = 256 host addresses. Each VU releases its slot inline so we stay well +// under pool capacity for the full DURATION burst. +const POOL_SIZE = 256; + +const createLatency = new Trend('ipam_host_create_latency_ms', true); +const deleteLatency = new Trend('ipam_host_delete_latency_ms', true); +const successRate = new Rate('ipam_host_success_rate'); +const created = new Counter('ipam_host_created'); +const denied = new Counter('ipam_host_denied'); +const errors = new Counter('ipam_host_errors'); +const missingStatus = new Counter('ipam_host_missing_status'); +const duplicates = new Counter('ipam_host_duplicate'); + +export const options = { + insecureSkipTLSVerify: __ENV.K6_INSECURE_SKIP_TLS_VERIFY !== 'false', + scenarios: { + concurrent_burst: { + executor: 'constant-vus', + vus: VUS, + duration: DURATION, + tags: { scenario: 'concurrent' }, + exec: 'concurrent', + }, + uniqueness_check: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + maxDuration: '5m', + // Run after the burst finishes so the pool is empty. + startTime: DURATION, + tags: { scenario: 'uniqueness' }, + exec: 'uniqueness', + }, + }, + thresholds: { + 'ipam_host_create_latency_ms{phase:success}': ['p(95)<500', 'p(99)<2000'], + 'ipam_host_success_rate': ['rate>0.95'], + 'http_req_failed': ['rate<0.05'], + // Hard guards: missing status or duplicate CIDRs fail the run. + 'ipam_host_missing_status': ['count==0'], + 'ipam_host_duplicate': ['count==0'], + }, +}; + +// setup creates the dedicated /24 IPPool. Idempotent — 409 is OK. +export function setup() { + const poolRes = createIPPool(POOL_NAME, POOL_CIDR, { + ipFamily: 'IPv4', + visibility: 'consumer', + minLen: 32, + maxLen: 32, + strategy: 'FirstFit', + }); + if (poolRes.status !== 201 && poolRes.status !== 409) { + throw new Error(`host pool create failed: ${poolRes.status} ${poolRes.body}`); + } + + console.log( + `setup complete: pool=${POOL_NAME} cidr=${POOL_CIDR} (${POOL_SIZE} host addresses)`, + ); + return { poolName: POOL_NAME }; +} + +function extractAllocatedCIDR(res) { + let body; + try { + body = JSON.parse(res.body); + } catch (_e) { + return null; + } + if (!body || !body.status) return null; + const cidr = body.status.allocatedCIDR; + if (!cidr || cidr === '') return null; + return cidr; +} + +// concurrent is the burst loop: many VUs CREATE a /32 claim then DELETE it +// inline. Each iteration releases its slot so the pool stays unsaturated. +export function concurrent() { + const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); + const claimName = `host-concurrent-${__VU}-${__ITER}`; + + const createRes = createIPClaimForProject(ns, claimName, POOL_NAME, 32, PROJECT); + + if (createRes.status === 201) { + created.add(1); + createLatency.add(createRes.timings.duration, { phase: 'success' }); + successRate.add(1); + + if (extractAllocatedCIDR(createRes) === null) { + missingStatus.add(1); + if (__ITER < 5) { + console.error( + `host claim ${claimName} created without status.allocatedCIDR: ${createRes.body}`, + ); + } + } + + const delRes = deleteIPClaimForProject(ns, claimName, PROJECT); + deleteLatency.add(delRes.timings.duration); + if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { + errors.add(1); + } + } else if (createRes.status === 507) { + denied.add(1); + createLatency.add(createRes.timings.duration, { phase: 'denied' }); + successRate.add(0); + } else { + errors.add(1); + createLatency.add(createRes.timings.duration, { phase: 'error' }); + successRate.add(0); + if (__ITER < 5) { + console.error(`VU ${__VU} iter ${__ITER}: unexpected ${createRes.status}: ${createRes.body}`); + } + } +} + +// uniqueness drains the pool sequentially from a single VU, recording every +// status.allocatedCIDR and reporting any duplicates. Cleans up after itself. +export function uniqueness() { + const ns = nsFor(0); + const seen = {}; + const claims = []; + let dupCount = 0; + + for (let i = 0; i < POOL_SIZE + 16; i++) { + const claimName = `host-unique-${i}`; + const res = createIPClaimForProject(ns, claimName, POOL_NAME, 32, PROJECT); + if (res.status === 507) break; + if (res.status !== 201) { + console.error(`uniqueness create ${i}: status=${res.status} body=${res.body}`); + continue; + } + const cidr = extractAllocatedCIDR(res); + if (cidr === null) { + missingStatus.add(1); + continue; + } + if (seen[cidr]) { + dupCount++; + console.error(`DUPLICATE CIDR ${cidr} returned for both ${seen[cidr]} and ${claimName}`); + } else { + seen[cidr] = claimName; + } + claims.push(claimName); + } + + if (dupCount > 0) { + duplicates.add(dupCount); + } + console.log( + `uniqueness scenario: ${claims.length} claims, ${Object.keys(seen).length} unique /32 CIDRs, ${dupCount} duplicates`, + ); + + // Release all slots so teardown can delete the pool cleanly. + for (const name of claims) { + deleteIPClaimForProject(ns, name, PROJECT); + } +} + +// teardown removes the pool. The burst scenario frees its claims inline; the +// uniqueness scenario drains its own. A leftover claim will block the pool +// delete and surface the leak in the logs. +export function teardown(data) { + if (!data) return; + const poolRes = deleteIPPool(data.poolName); + if (poolRes.status !== 200 && poolRes.status !== 202 && poolRes.status !== 404) { + console.error( + `teardown: pool delete ${data.poolName} status=${poolRes.status} body=${poolRes.body}`, + ); + } + console.log('host-prefix-claim-concurrent teardown complete'); +} diff --git a/config/components/k6-performance-tests/generated/ipv6-claim-throughput.js b/config/components/k6-performance-tests/generated/ipv6-claim-throughput.js index 8ff5a6e..d1f41e0 100644 --- a/config/components/k6-performance-tests/generated/ipv6-claim-throughput.js +++ b/config/components/k6-performance-tests/generated/ipv6-claim-throughput.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); +} + +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); +} + +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); +} + +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -474,11 +443,11 @@ export function listASNClaimsForProject(ns, projectID) { // ipv6-claim-throughput.js // -// PRIMARY PRIORITY load test for the IPAM platform: IPv6 prefix-claim -// throughput. The platform allocates primarily IPv6 — this script is the -// canonical proof that the hot path holds the same SLO under IPv6 as under -// IPv4, with the additional correctness gate that no two simultaneous -// allocations may overlap. +// PRIMARY PRIORITY load test for the IPAM platform: IPv6 IPClaim throughput. +// The platform allocates primarily IPv6 — this script is the canonical proof +// that the hot path holds the same SLO under IPv6 as under IPv4, with the +// additional correctness gate that no two simultaneous allocations may +// overlap. // // Topology (provisioned by setup-pools.js): // - Per-project IPv6 /32 pool `perf-ipv6-prefix-` (fd:::/32) @@ -561,7 +530,7 @@ export const options = { }, }, thresholds: { - // SLO: same envelope as the IPv4 prefix-claim path. + // SLO: same envelope as the IPv4 IPClaim path. 'ipam_ipv6_claim_create_latency_ms{phase:success}': ['p(95)<500', 'p(99)<2000'], 'ipam_ipv6_claim_success_rate': ['rate>0.95'], 'http_req_failed': ['rate<0.05'], @@ -642,11 +611,6 @@ function containsCIDR(parent, child) { return maskAddr(child.addr, parent.prefixLen) === maskAddr(parent.addr, parent.prefixLen); } -// Two CIDRs collide iff one contains the other. -function cidrsOverlap(a, b) { - return containsCIDR(a, b) || containsCIDR(b, a); -} - // Per-pool reference for containment checks. Parsed once at module load. const POOL_CIDR = {}; POOL_CIDR[SHARED_IPV6_POOL] = parseCIDR('fd00:f000::/28'); @@ -662,9 +626,9 @@ for (let n = 0; n < PROJECT_COUNT; n++) { // ---- Duplicate-CIDR detection ---- // // k6 VUs each run in their own goja runtime, so we cannot share a single -// JS Set across VUs. We rely on the server's invariant: an IPPrefixClaim -// CREATE must never return an overlapping CIDR. For an in-script signal we -// keep a per-VU registry; a duplicate within ONE VU would also be a bug. +// JS Set across VUs. We rely on the server's invariant: an IPClaim CREATE +// must never return an overlapping CIDR. For an in-script signal we keep a +// per-VU registry; a duplicate within ONE VU would also be a bug. // Cross-VU duplicates are detectable via the e2e suite and the count of // 201s vs distinct CIDRs in the json-out, both of which are tracked. const seenCIDRs = new Set(); @@ -744,18 +708,18 @@ function recordCreate(res, mode, poolName) { // Direct HTTP wrapper — the lib helpers default to IPv4, so we post our own // IPv6 body with the project tenant header in a single round-trip. -function postIPv6Claim(ns, name, prefixRef, projectID) { - const body = ipPrefixClaim(ns, name, prefixRef, CLAIM_PREFIX_LENGTH, { ipFamily: 'IPv6' }); - const params = withProjectTagged(projectID, 'ipv6_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +function postIPv6Claim(ns, name, poolName, projectID) { + const body = ipClaim(ns, name, poolName, CLAIM_PREFIX_LENGTH, { ipFamily: 'IPv6' }); + const params = withProjectTagged(projectID, 'ipv6_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } function postCrossProjectIPv6Claim(ns, name, poolName, sourceProjectID, callerProjectID) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, CLAIM_PREFIX_LENGTH, { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, CLAIM_PREFIX_LENGTH, { ipFamily: 'IPv6', }); - const params = withProjectTagged(callerProjectID, 'ipv6_cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); + const params = withProjectTagged(callerProjectID, 'ipv6_cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } export default function () { @@ -785,7 +749,7 @@ export default function () { const ok = recordCreate(res, mode, poolName); if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, callerProject); + const delRes = deleteIPClaimForProject(ns, claimName, callerProject); claimDeleteLatency.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { claimErrors.add(1, { mode, phase: 'delete' }); diff --git a/config/components/k6-performance-tests/generated/mixed-load.js b/config/components/k6-performance-tests/generated/mixed-load.js index 0bffa62..6e92c1c 100644 --- a/config/components/k6-performance-tests/generated/mixed-load.js +++ b/config/components/k6-performance-tests/generated/mixed-load.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); } +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -505,7 +474,7 @@ const claimsCreated = new Counter('ipam_claims_created'); const claimsDenied = new Counter('ipam_claims_denied'); const claimErrors = new Counter('ipam_claim_errors'); -const prefixListLatency = new Trend('ipam_prefix_list_ms', true); +const poolListLatency = new Trend('ipam_prefix_list_ms', true); const claimGetLatency = new Trend('ipam_claim_get_ms', true); const clusterListLatency = new Trend('ipam_cluster_list_ms', true); const readSuccessRate = new Rate('ipam_read_success_rate'); @@ -604,7 +573,7 @@ function recordCreate(res) { // --- Exported scenario functions --- -// writeScenario: create a /28 prefix claim then delete it. Used by both +// writeScenario: create a /28 IPClaim then delete it. Used by both // write_steady (baseline) and write_burst (spike) scenarios. export function writeScenario() { const projectIdx = pickProjectIdx(); @@ -613,11 +582,11 @@ export function writeScenario() { const poolName = `perf-prefix-${projectIdx}`; const claimName = `mixed-${__VU}-${__ITER}`; - const createRes = createPrefixClaimForProject(ns, claimName, poolName, 28, projectID); + const createRes = createIPClaimForProject(ns, claimName, poolName, 28, projectID); const ok = recordCreate(createRes); if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, projectID); + const delRes = deleteIPClaimForProject(ns, claimName, projectID); claimDeleteLatency.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { claimErrors.add(1); @@ -627,9 +596,9 @@ export function writeScenario() { // readScenario: randomly picks one of three read operations weighted to match // real operator traffic patterns. Used by both read_steady and read_spike. -// 60% — cluster-scoped prefix list (pool utilisation check) -// 20% — namespace-scoped prefix claim list (operator reconcile) -// 20% — single prefix GET (get allocated CIDR for a specific pool) +// 60% — cluster-scoped IPPool list (pool utilisation check) +// 20% — namespace-scoped IPClaim list (operator reconcile) +// 20% — single IPPool GET (read pool state for a specific pool) export function readScenario() { const projectIdx = pickProjectIdx(); const projectID = projectIDFor(projectIdx); @@ -637,14 +606,14 @@ export function readScenario() { let res; if (r < 0.6) { - res = listPrefixesForProject(projectID); + res = listIPPoolsForProject(projectID); clusterListLatency.add(res.timings.duration); } else if (r < 0.8) { const ns = pickNs(); - res = listPrefixClaimsForProject(ns, projectID); - prefixListLatency.add(res.timings.duration); + res = listIPClaimsForProject(ns, projectID); + poolListLatency.add(res.timings.duration); } else { - res = getPrefixForProject(`perf-prefix-${projectIdx}`, projectID); + res = getIPPoolForProject(`perf-prefix-${projectIdx}`, projectID); claimGetLatency.add(res.timings.duration); } diff --git a/config/components/k6-performance-tests/generated/pool-exhaustion.js b/config/components/k6-performance-tests/generated/pool-exhaustion.js index bdb549b..61981f1 100644 --- a/config/components/k6-performance-tests/generated/pool-exhaustion.js +++ b/config/components/k6-performance-tests/generated/pool-exhaustion.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); +} + +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); +} + +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); +} + +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -481,13 +450,13 @@ export function listASNClaimsForProject(ns, projectID) { // shared pool, which is also exhausted). // // Setup phase: -// - Create perf-exhaust-class (visibility: shared, /30 only) -// - Create perf-exhaust-pool (192.168.100.0/28) owned by project 0 +// - Create perf-exhaust-pool (192.168.100.0/28, /30 only, visibility=shared) +// owned by project 0 // - Bind perf-exhaust-pool-user role to all other perf projects // - Fill the pool with 4 /30 claims (project 0 identity) // Main phase: hammer additional claim requests from both same-project and // cross-project callers. -// Teardown: delete the 4 fill claims. +// Teardown: delete the 4 fill claims, then the pool. // // Configuration: // VUS - Concurrent virtual users (default 20) @@ -502,11 +471,9 @@ const PROJECT_COUNT = parseInt(__ENV.PROJECT_COUNT || '5'); const VUS = parseInt(__ENV.VUS || '20'); const DURATION = __ENV.DURATION || '1m'; const POOL_NAME = 'perf-exhaust-pool'; -const CLASS_NAME = 'perf-exhaust-class'; const EXHAUST_USER_ROLE = 'perf-exhaust-pool-user'; -// Visibility for the cross-project pool. The server accepts any string for -// Visibility (plain string field with no enum validation), so 'shared' is -// accepted today and matches the documented intent. +// Visibility for the cross-project pool. The apiserver enum is +// platform|consumer|shared; 'shared' enables cross-project claiming. const SHARED_VISIBILITY = __ENV.SHARED_VISIBILITY || 'shared'; const FILL_NAMESPACE = nsFor(0); const OWNER_PROJECT = projectIDFor(0); @@ -543,26 +510,23 @@ export const options = { }; export function setup() { - const c = createPrefixClass(CLASS_NAME, { + const p = createIPPool(POOL_NAME, '192.168.100.0/28', { + ipFamily: 'IPv4', visibility: SHARED_VISIBILITY, minLen: 30, maxLen: 30, strategy: 'FirstFit', }); - if (c.status !== 201 && c.status !== 409) { - throw new Error(`class create failed: ${c.status} ${c.body}`); - } - - const p = createPrefix(POOL_NAME, '192.168.100.0/28', CLASS_NAME, { minLen: 30, maxLen: 30 }); if (p.status !== 201 && p.status !== 409) { throw new Error(`pool create failed: ${p.status} ${p.body}`); } // ClusterRole + bindings so cross-project callers can issue use claims. + // CanUsePool targets the ippools resource. const role = createClusterRole(EXHAUST_USER_ROLE, [ { apiGroups: ['ipam.miloapis.com'], - resources: ['ipprefixes'], + resources: ['ippools'], resourceNames: [POOL_NAME], verbs: ['use'], }, @@ -586,7 +550,7 @@ export function setup() { const fillNames = []; for (let i = 0; i < 4; i++) { const name = `exhaust-fill-${i}`; - const r = createPrefixClaimForProject(FILL_NAMESPACE, name, POOL_NAME, 30, OWNER_PROJECT); + const r = createIPClaimForProject(FILL_NAMESPACE, name, POOL_NAME, 30, OWNER_PROJECT); if (r.status === 201) { fillNames.push(name); } else { @@ -608,7 +572,7 @@ function record(res, mode, ns, name, callerProject) { successes.add(1, { mode }); successLatency.add(res.timings.duration, { mode }); denyRate.add(0); - deletePrefixClaimForProject(ns, name, callerProject); + deleteIPClaimForProject(ns, name, callerProject); } else { errors.add(1, { mode }); denyRate.add(0); @@ -624,20 +588,20 @@ export default function () { // Alternate same-project (project 0) and cross-project (project 1) probes. if (__ITER % 2 === 0) { - const r = createPrefixClaimForProject(ns, name, POOL_NAME, 30, OWNER_PROJECT); + const r = createIPClaimForProject(ns, name, POOL_NAME, 30, OWNER_PROJECT); record(r, 'same', ns, name, OWNER_PROJECT); } else { const callerIdx = 1 + (__VU % Math.max(1, PROJECT_COUNT - 1)); const callerProject = projectIDFor(callerIdx); - const r = createCrossProjectPrefixClaim(ns, name, POOL_NAME, OWNER_PROJECT, callerProject, 30); + const r = createCrossProjectIPClaim(ns, name, POOL_NAME, OWNER_PROJECT, callerProject, 30); record(r, 'cross', ns, name, callerProject); } } export function teardown(data) { for (const name of data.fillNames || []) { - deletePrefixClaimForProject(FILL_NAMESPACE, name, OWNER_PROJECT); + deleteIPClaimForProject(FILL_NAMESPACE, name, OWNER_PROJECT); } - deletePrefix(POOL_NAME); + deleteIPPool(POOL_NAME); console.log('teardown complete'); } diff --git a/config/components/k6-performance-tests/generated/pool-scale.js b/config/components/k6-performance-tests/generated/pool-scale.js index 71d0a9d..84e894e 100644 --- a/config/components/k6-performance-tests/generated/pool-scale.js +++ b/config/components/k6-performance-tests/generated/pool-scale.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); } +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -479,7 +448,7 @@ export function listASNClaimsForProject(ns, projectID) { // latency. Tags every metric with {depth: N} so we can compare across steps. // // All requests are scoped to project 0 (`ipam-perf-0`) and target project 0's -// per-project pool `perf-prefix-0` (10.0.0.0/16). The /16 size keeps the +// per-project IPPool `perf-prefix-0` (10.0.0.0/16). The /16 size keeps the // sweep bounded while still letting us walk /20 -> /28 densities. // // Asserts (informally, via thresholds) that p95 latency does not increase @@ -558,7 +527,7 @@ function fillStep(prefixLen) { const samples = latenciesByDepth[prefixLen] || (latenciesByDepth[prefixLen] = []); for (let i = 0; i < target; i++) { const name = `scale-d${prefixLen}-${i}`; - const r = createPrefixClaimForProject(FILL_NS, name, PARENT_PREFIX, prefixLen, PROJECT); + const r = createIPClaimForProject(FILL_NS, name, PARENT_PREFIX, prefixLen, PROJECT); if (r.status === 201) { created.push(name); createLatency.add(r.timings.duration, { depth: String(prefixLen) }); @@ -573,7 +542,7 @@ function fillStep(prefixLen) { // Cleanup so the next step gets fresh capacity for (const name of created) { - deletePrefixClaimForProject(FILL_NS, name, PROJECT); + deleteIPClaimForProject(FILL_NS, name, PROJECT); } return created.length; } diff --git a/config/components/k6-performance-tests/generated/prefix-claim-throughput.js b/config/components/k6-performance-tests/generated/prefix-claim-throughput.js index aa3871f..4e71a0e 100644 --- a/config/components/k6-performance-tests/generated/prefix-claim-throughput.js +++ b/config/components/k6-performance-tests/generated/prefix-claim-throughput.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); } +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -474,12 +443,12 @@ export function listASNClaimsForProject(ns, projectID) { // prefix-claim-throughput.js // -// Measures the hot path of the IPAM service: IPPrefixClaim creation throughput -// and latency under sustained load, with a multi-tenant traffic mix. +// Measures the hot path of the IPAM service: IPClaim creation throughput and +// latency under sustained load, with a multi-tenant traffic mix. // // 90% of iterations: same-project claim — VU picks a random project N, sends -// a claim against perf-prefix-N with the project N tenant -// headers (no projectRef in spec). +// an IPClaim against perf-prefix-N with the project N +// tenant headers (no projectRef in spec). // 10% of iterations: cross-project claim — VU picks a random project N != 0 // and claims from project 0's shared pool (perf-shared-prefix) // using its own project identity in headers and projectRef @@ -571,7 +540,7 @@ export default function () { // Pick any project except project 0 (which owns the shared pool). const callerIdx = 1 + Math.floor(Math.random() * Math.max(1, PROJECT_COUNT - 1)); callerProject = projectIDFor(callerIdx); - createRes = createCrossProjectPrefixClaim( + createRes = createCrossProjectIPClaim( ns, claimName, SHARED_PREFIX, @@ -584,13 +553,13 @@ export default function () { const projectIdx = Math.floor(Math.random() * PROJECT_COUNT); callerProject = projectIDFor(projectIdx); const poolName = `perf-prefix-${projectIdx}`; - createRes = createPrefixClaimForProject(ns, claimName, poolName, 28, callerProject); + createRes = createIPClaimForProject(ns, claimName, poolName, 28, callerProject); } const ok = recordCreate(createRes, mode); if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, callerProject); + const delRes = deleteIPClaimForProject(ns, claimName, callerProject); claimDeleteLatency.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { claimErrors.add(1); diff --git a/config/components/k6-performance-tests/generated/read-latency.js b/config/components/k6-performance-tests/generated/read-latency.js index d9b381e..2f5e647 100644 --- a/config/components/k6-performance-tests/generated/read-latency.js +++ b/config/components/k6-performance-tests/generated/read-latency.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); +} + +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); +} + +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); +} + +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -475,16 +444,16 @@ export function listASNClaimsForProject(ns, projectID) { // read-latency.js // // Measures read-path latency under several workload shapes: -// - steady (10 VUs, 3m): 60% cluster-list IPPrefix, 20% ns list IPPrefixClaims, 20% single GET +// - steady (10 VUs, 3m): 60% cluster-list IPPool, 20% ns list IPClaims, 20% single GET // - ramp (0->20->50->0 VUs over 3m): same workload mix // - spike (0->100->0 VUs over 30s): list-heavy // -// Coverage extension scenarios (audit Task #11): assert read latency for the -// other listable resources matches the IPPrefix list envelope. Each runs in -// parallel with the original three so the operator gets a unified summary. -// - addr_list: constant LIST ipaddresses (namespaced) -// - asnpool_list: constant LIST asnpools (cluster scope) -// - asnclaim_list: constant LIST asnclaims (namespaced) +// Coverage extension scenarios: assert read latency for the other listable +// resources matches the IPPool list envelope. Each runs in parallel with the +// original three so the operator gets a unified summary. +// - alloc_list: namespaced LIST ipallocations +// - asnpool_list: constant LIST asnpools (cluster scope) +// - asnclaim_list: namespaced LIST asnclaims // // Every iteration picks a random perf project and scopes all reads to that // project's tenant context (X-Remote-Extra parent headers). @@ -501,15 +470,13 @@ import { Rate, Trend } from 'k6/metrics'; const NAMESPACE_COUNT = parseInt(__ENV.NAMESPACE_COUNT || '10'); const PROJECT_COUNT = parseInt(__ENV.PROJECT_COUNT || '5'); -const prefixListLatency = new Trend('ipam_prefix_list_ms', true); +const poolListLatency = new Trend('ipam_prefix_list_ms', true); const claimGetLatency = new Trend('ipam_claim_get_ms', true); const clusterListLatency = new Trend('ipam_cluster_list_ms', true); -// New per-resource list trends for the audit-expansion scenarios. Tagged the -// same way as the existing prefix-list trend so dashboards can plot them +// Per-resource list trends for the audit-expansion scenarios. Tagged the +// same way as the existing pool-list trend so dashboards can plot them // side-by-side. -const ipAddressListLatency = new Trend('ipam_ipaddress_list_ms', true); -const asnPoolListLatency = new Trend('ipam_asnpool_list_ms', true); -const asnClaimListLatency = new Trend('ipam_asnclaim_list_ms', true); +const ipAllocationListLatency = new Trend('ipam_ipallocation_list_ms', true); const readSuccessRate = new Rate('ipam_read_success_rate'); export const options = { @@ -547,36 +514,22 @@ export const options = { // -- Coverage extension: dedicated list-only scenarios for the resources // that previously had no read-latency coverage. Each runs against a // modest VU pool for the full steady duration so we get stable p95s. - addr_list: { - executor: 'constant-vus', - vus: 5, - duration: '3m', - tags: { scenario: 'addr_list' }, - exec: 'ipAddressList', - }, - asnpool_list: { - executor: 'constant-vus', - vus: 5, - duration: '3m', - tags: { scenario: 'asnpool_list' }, - exec: 'asnPoolList', - }, - asnclaim_list: { + alloc_list: { executor: 'constant-vus', vus: 5, duration: '3m', - tags: { scenario: 'asnclaim_list' }, - exec: 'asnClaimList', + tags: { scenario: 'alloc_list' }, + exec: 'ipAllocationList', }, + // NOTE: asnpool_list / asnclaim_list scenarios disabled — ASNPool/ASNClaim + // resources are not yet implemented in this branch (see commit 86aceec). }, thresholds: { 'ipam_prefix_list_ms': ['p(95)<200'], 'ipam_claim_get_ms': ['p(95)<100'], 'ipam_cluster_list_ms': ['p(95)<500'], - // Audit gap-fill thresholds: same envelope as the IPPrefix list path. - 'ipam_ipaddress_list_ms': ['p(95)<200'], - 'ipam_asnpool_list_ms': ['p(95)<200'], - 'ipam_asnclaim_list_ms': ['p(95)<200'], + // Audit gap-fill threshold: same envelope as the IPPool list path. + 'ipam_ipallocation_list_ms': ['p(95)<200'], 'ipam_read_success_rate': ['rate>0.99'], }, }; @@ -603,17 +556,17 @@ function doWork() { let res; switch (w) { case 'cluster_list': - res = listPrefixesForProject(projectID); + res = listIPPoolsForProject(projectID); clusterListLatency.add(res.timings.duration); break; case 'ns_list': { const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - res = listPrefixClaimsForProject(ns, projectID); - prefixListLatency.add(res.timings.duration); + res = listIPClaimsForProject(ns, projectID); + poolListLatency.add(res.timings.duration); break; } case 'single_get': - res = getPrefixForProject(`perf-prefix-${projectIdx}`, projectID); + res = getIPPoolForProject(`perf-prefix-${projectIdx}`, projectID); claimGetLatency.add(res.timings.duration); break; } @@ -629,44 +582,28 @@ export function spike() { const r = Math.random(); let res; if (r < 0.7) { - res = listPrefixesForProject(projectID); + res = listIPPoolsForProject(projectID); clusterListLatency.add(res.timings.duration); } else { const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - res = listPrefixClaimsForProject(ns, projectID); - prefixListLatency.add(res.timings.duration); + res = listIPClaimsForProject(ns, projectID); + poolListLatency.add(res.timings.duration); } const ok = check(res, { 'read ok': (r) => r.status === 200 }); readSuccessRate.add(ok ? 1 : 0); } -// ipAddressList: namespaced LIST against a random perf namespace, scoped to -// a random project's tenant context. -export function ipAddressList() { +// ipAllocationList: namespaced LIST against a random perf namespace, scoped +// to a random project's tenant context. +export function ipAllocationList() { const projectID = pickProject(); const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - const res = listIPAddressesForProject(ns, projectID); - ipAddressListLatency.add(res.timings.duration); - const ok = check(res, { 'ipaddress list ok': (r) => r.status === 200 }); - readSuccessRate.add(ok ? 1 : 0); -} - -// asnPoolList: cluster-scoped LIST. ASNPools are global; the project headers -// are still applied so the auth path matches production traffic. -export function asnPoolList() { - const projectID = pickProject(); - const res = listASNPoolsForProject(projectID); - asnPoolListLatency.add(res.timings.duration); - const ok = check(res, { 'asnpool list ok': (r) => r.status === 200 }); + const res = listIPAllocationsForProject(ns, projectID); + ipAllocationListLatency.add(res.timings.duration); + const ok = check(res, { 'ipallocation list ok': (r) => r.status === 200 }); readSuccessRate.add(ok ? 1 : 0); } -// asnClaimList: namespaced LIST against a random perf namespace. -export function asnClaimList() { - const projectID = pickProject(); - const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - const res = listASNClaimsForProject(ns, projectID); - asnClaimListLatency.add(res.timings.duration); - const ok = check(res, { 'asnclaim list ok': (r) => r.status === 200 }); - readSuccessRate.add(ok ? 1 : 0); -} +// ASN list scenarios removed — ASNPool/ASNClaim resources are not implemented +// on this branch. Restore once `asnpools.ipam.miloapis.com` / `asnclaims.ipam.miloapis.com` +// are served. diff --git a/config/components/k6-performance-tests/generated/setup-pools.js b/config/components/k6-performance-tests/generated/setup-pools.js index cfac399..e8bbb53 100644 --- a/config/components/k6-performance-tests/generated/setup-pools.js +++ b/config/components/k6-performance-tests/generated/setup-pools.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); } +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -478,23 +447,20 @@ export function listASNClaimsForProject(ns, projectID) { // // Layout produced: // Platform-level (kept for backwards compatibility with older tests): -// - IPPrefixClass `perf-private` (visibility: consumer) -// - IPPrefix `perf-prefix` (10.0.0.0/8, /20-/28) -// - ASNPoolClass `perf-asn` -// - ASNPool `perf-asn-pool` (4200000000-4200099999) +// - IPPool `perf-prefix` (10.0.0.0/8, /20-/28, visibility=consumer) +// - ASNPoolClass `perf-asn` +// - ASNPool `perf-asn-pool` (4200000000-4200099999) // // Per-project (one set per perf project, n in [0, PROJECT_COUNT)): -// - IPPrefix `perf-prefix-` covering 10..0.0/16 (/20-/28) -// - IPPrefix `perf-ipv6-prefix-` covering fd:::/32 (/40-/56) -// - ASNPool `perf-asn-pool-` each spanning 20k ASNs +// - IPPool `perf-prefix-` covering 10..0.0/16 (/20-/28) +// - IPPool `perf-ipv6-prefix-` covering fd:::/32 (/40-/56) +// - ASNPool `perf-asn-pool-` each spanning 20k ASNs // // Shared cross-project pool (owned by project 0): -// - IPPrefixClass `perf-shared` (visibility: shared, IPv4) -// - IPPrefix `perf-shared-prefix` (172.16.0.0/12, /24-/28) -// - IPPrefixClass `perf-ipv6-shared-class` (visibility: shared, IPv6) -// - IPPrefix `perf-ipv6-shared` (fd00:ffff::/28, /40-/56) -// - ClusterRole `perf-shared-pool-user` (use on perf-shared-prefix) -// - ClusterRole `perf-ipv6-shared-pool-user` (use on perf-ipv6-shared) +// - IPPool `perf-shared-prefix` (172.16.0.0/12, /24-/28, visibility=shared) +// - IPPool `perf-ipv6-shared` (fd00:f000::/28, /40-/56, visibility=shared) +// - ClusterRole `perf-shared-pool-user` (use on perf-shared-prefix) +// - ClusterRole `perf-ipv6-shared-pool-user` (use on perf-ipv6-shared) // - ClusterRoleBinding per project [1..N) granting use of each shared pool // // Namespaces: `ipam-perf-` for n in [0, NAMESPACE_COUNT) @@ -510,17 +476,14 @@ import { check, sleep } from 'k6'; const NAMESPACE_COUNT = parseInt(__ENV.NAMESPACE_COUNT || '10'); const PROJECT_COUNT = parseInt(__ENV.PROJECT_COUNT || '5'); const SETUP_VUS = parseInt(__ENV.SETUP_VUS || '1'); -// IPPrefixClass.spec.visibility for the cross-project pool. The server -// accepts any string for Visibility (plain string field with no enum -// validation), so 'shared' is accepted today and matches the documented -// intent. +// IPPool.spec.visibility for the cross-project pool. The apiserver enum is +// platform|consumer|shared; 'shared' is the value cross-project tests use. const SHARED_VISIBILITY = __ENV.SHARED_VISIBILITY || 'shared'; // Each per-project ASN pool spans 20k ASNs starting at this base. const ASN_BASE = 4200000000; const ASN_PER_PROJECT = 20000; -const SHARED_CLASS_NAME = 'perf-shared'; const SHARED_PREFIX_NAME = 'perf-shared-prefix'; const SHARED_POOL_USER_ROLE = 'perf-shared-pool-user'; @@ -535,7 +498,6 @@ const SHARED_POOL_USER_ROLE = 'perf-shared-pool-user'; // // minPrefixLength=40 corresponds to a SMALLER prefix length number (LARGER // block), maxPrefixLength=56 a LARGER number (SMALLER block). -const SHARED_IPV6_CLASS_NAME = 'perf-ipv6-shared-class'; const SHARED_IPV6_PREFIX_NAME = 'perf-ipv6-shared'; const IPV6_POOL_USER_ROLE = 'perf-ipv6-shared-pool-user'; const IPV6_MIN_LEN = 40; @@ -553,28 +515,20 @@ export const options = { }, }; -function okOrConflict(name) { +function okOrConflict() { return (res) => res.status === 201 || res.status === 409; } export default function () { // ---- Platform-level pool (legacy / compatibility) ---- - let r = createPrefixClass('perf-private', { - requiresVerification: false, - visibility: 'consumer', - minLen: 20, - maxLen: 28, - strategy: 'FirstFit', - }); - check(r, { 'perf-private class created or exists': okOrConflict() }); - - r = createPrefix('perf-prefix', '10.0.0.0/8', 'perf-private', { + let r = createIPPool('perf-prefix', '10.0.0.0/8', { ipFamily: 'IPv4', + visibility: 'consumer', minLen: 20, maxLen: 28, strategy: 'FirstFit', }); - check(r, { 'perf-prefix created or exists': okOrConflict() }); + check(r, { 'perf-prefix pool created or exists': okOrConflict() }); r = createASNPoolClass('perf-asn', { requiresVerification: false, visibility: 'consumer' }); check(r, { 'perf-asn class created or exists': okOrConflict() }); @@ -595,45 +549,47 @@ export default function () { const sliceStart = vuIndex * sliceSize; const sliceEnd = Math.min(sliceStart + sliceSize, PROJECT_COUNT); - let projectPrefixes = 0; + let projectPools = 0; let projectASNPools = 0; - let projectIPv6Prefixes = 0; + let projectIPv6Pools = 0; for (let n = sliceStart; n < sliceEnd; n++) { - const prefixName = `perf-prefix-${n}`; + const poolName = `perf-prefix-${n}`; // CIDR: projects 0-255 → 10.0.x.x/16, 256-511 → 10.1.x.x/16, etc. // Uses octets 10-13 (covering 0-1023 projects within RFC1918 space). const cidr = `${10 + Math.floor(n / 256)}.${n % 256}.0.0/16`; - const pres = createPrefix(prefixName, cidr, 'perf-private', { + const pres = createIPPool(poolName, cidr, { ipFamily: 'IPv4', + visibility: 'consumer', minLen: 20, maxLen: 28, strategy: 'FirstFit', }); if (pres.status === 201 || pres.status === 409) { - projectPrefixes++; + projectPools++; } else { - console.error(`per-project prefix ${prefixName} create failed: ${pres.status} ${pres.body}`); + console.error(`per-project pool ${poolName} create failed: ${pres.status} ${pres.body}`); } // Per-project IPv6 pool. fd:::/32 with HH = n>>8, LLLL = n&0xff. // Project 0 → fd00:0000::/32, project 1 → fd00:0001::/32, ... // Up to 65536 perf projects fit in fd00::/16 without collisions. - const v6Prefix = `perf-ipv6-prefix-${n}`; + const v6PoolName = `perf-ipv6-prefix-${n}`; const hi = (n >> 8) & 0xff; const lo = n & 0xff; const v6Cidr = `fd${hi.toString(16).padStart(2, '0')}:` + `${lo.toString(16).padStart(4, '0')}::/32`; - const v6Res = createPrefix(v6Prefix, v6Cidr, 'perf-private', { + const v6Res = createIPPool(v6PoolName, v6Cidr, { ipFamily: 'IPv6', + visibility: 'consumer', minLen: IPV6_MIN_LEN, maxLen: IPV6_MAX_LEN, strategy: 'FirstFit', }); if (v6Res.status === 201 || v6Res.status === 409) { - projectIPv6Prefixes++; + projectIPv6Pools++; } else { - console.error(`per-project IPv6 prefix ${v6Prefix} create failed: ${v6Res.status} ${v6Res.body}`); + console.error(`per-project IPv6 pool ${v6PoolName} create failed: ${v6Res.status} ${v6Res.body}`); } const asnPoolName = `perf-asn-pool-${n}`; @@ -646,33 +602,26 @@ export default function () { console.error(`per-project ASN pool ${asnPoolName} create failed: ${ares.status} ${ares.body}`); } } - check(projectPrefixes, { 'per-vu prefixes created': (n) => n === sliceEnd - sliceStart }); - check(projectIPv6Prefixes, { 'per-vu IPv6 prefixes created': (n) => n === sliceEnd - sliceStart }); + check(projectPools, { 'per-vu pools created': (n) => n === sliceEnd - sliceStart }); + check(projectIPv6Pools, { 'per-vu IPv6 pools created': (n) => n === sliceEnd - sliceStart }); check(projectASNPools, { 'per-vu ASN pools created': (n) => n === sliceEnd - sliceStart }); // ---- Shared cross-project pool (owned by project 0) ---- - r = createPrefixClass(SHARED_CLASS_NAME, { - requiresVerification: false, - visibility: SHARED_VISIBILITY, - minLen: 24, - maxLen: 28, - strategy: 'FirstFit', - }); - check(r, { 'perf-shared class created or exists': okOrConflict() }); - - r = createPrefix(SHARED_PREFIX_NAME, '172.16.0.0/12', SHARED_CLASS_NAME, { + r = createIPPool(SHARED_PREFIX_NAME, '172.16.0.0/12', { ipFamily: 'IPv4', + visibility: SHARED_VISIBILITY, minLen: 24, maxLen: 28, strategy: 'FirstFit', }); - check(r, { 'perf-shared-prefix created or exists': okOrConflict() }); + check(r, { 'perf-shared-prefix pool created or exists': okOrConflict() }); - // ClusterRole granting the `use` verb on the shared pool + // ClusterRole granting the `use` verb on the shared pool. The CanUsePool + // check targets the `ippools` resource. r = createClusterRole(SHARED_POOL_USER_ROLE, [ { apiGroups: ['ipam.miloapis.com'], - resources: ['ipprefixes'], + resources: ['ippools'], resourceNames: [SHARED_PREFIX_NAME], verbs: ['use'], }, @@ -707,27 +656,19 @@ export default function () { // fd00:f000::/28 sits above the per-project /32s (which use lo bytes 0..ff // in the second 16-bit group), so it can never overlap with a per-project // pool no matter how PROJECT_COUNT grows. - r = createPrefixClass(SHARED_IPV6_CLASS_NAME, { - requiresVerification: false, - visibility: SHARED_VISIBILITY, - minLen: IPV6_MIN_LEN, - maxLen: IPV6_MAX_LEN, - strategy: 'FirstFit', - }); - check(r, { 'perf-ipv6-shared-class created or exists': okOrConflict() }); - - r = createPrefix(SHARED_IPV6_PREFIX_NAME, 'fd00:f000::/28', SHARED_IPV6_CLASS_NAME, { + r = createIPPool(SHARED_IPV6_PREFIX_NAME, 'fd00:f000::/28', { ipFamily: 'IPv6', + visibility: SHARED_VISIBILITY, minLen: IPV6_MIN_LEN, maxLen: IPV6_MAX_LEN, strategy: 'FirstFit', }); - check(r, { 'perf-ipv6-shared created or exists': okOrConflict() }); + check(r, { 'perf-ipv6-shared pool created or exists': okOrConflict() }); r = createClusterRole(IPV6_POOL_USER_ROLE, [ { apiGroups: ['ipam.miloapis.com'], - resources: ['ipprefixes'], + resources: ['ippools'], resourceNames: [SHARED_IPV6_PREFIX_NAME], verbs: ['use'], }, @@ -768,8 +709,8 @@ export default function () { sleep(2); console.log( - `setup complete: platform pool perf-prefix(/8), ${projectPrefixes}/${PROJECT_COUNT} per-project /16 prefixes, ` + - `${projectIPv6Prefixes}/${PROJECT_COUNT} per-project IPv6 /32 prefixes, ` + + `setup complete: platform pool perf-prefix(/8), ${projectPools}/${PROJECT_COUNT} per-project /16 IPv4 pools, ` + + `${projectIPv6Pools}/${PROJECT_COUNT} per-project IPv6 /32 pools, ` + `${projectASNPools}/${PROJECT_COUNT} per-project ASN pools, shared pool perf-shared-prefix(/12), ` + `shared IPv6 pool ${SHARED_IPV6_PREFIX_NAME}(/28), ` + `${bindings}/${PROJECT_COUNT - 1} v4 bindings, ${v6Bindings}/${PROJECT_COUNT - 1} v6 bindings, ` + diff --git a/config/components/k6-performance-tests/generated/watch-latency.js b/config/components/k6-performance-tests/generated/watch-latency.js index 2ae2804..edcd0cf 100644 --- a/config/components/k6-performance-tests/generated/watch-latency.js +++ b/config/components/k6-performance-tests/generated/watch-latency.js @@ -3,7 +3,8 @@ // Lib: test/load/lib/ipam-client.js // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -101,16 +102,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -119,14 +127,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -135,76 +135,72 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { requiresVerification = false, visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - requiresVerification, - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } -export function asnPoolClass(name, { requiresVerification = false, visibility = 'consumer' } = {}) { +export function asnPoolClass(name, { visibility = 'consumer' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, kind: 'ASNPoolClass', metadata: { name }, - spec: { requiresVerification, visibility }, + spec: { visibility }, }; } @@ -226,9 +222,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -240,30 +233,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); } +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -280,26 +289,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -365,61 +354,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers +// for callerProjectID, targeting a pool owned by sourceProjectID. +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers -// for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); +} + +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { + return { + method: 'POST', + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), + }; } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -434,34 +422,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); @@ -474,9 +443,9 @@ export function listASNClaimsForProject(ns, projectID) { // watch-latency.js // -// SLO probe for the IPPrefixClaim watch pipeline (LISTEN ipam_changelog + -// polling cursor): how long after a CREATE commits does the server start -// streaming the ADDED event to a watcher? +// SLO probe for the IPClaim watch pipeline (LISTEN ipam_changelog + polling +// cursor): how long after a CREATE commits does the server start streaming +// the ADDED event to a watcher? // // Implementation note: k6's HTTP client buffers the entire response body — // there is no true streaming. So we cannot timestamp individual events as @@ -489,8 +458,8 @@ export function listASNClaimsForProject(ns, projectID) { // // Scenario: // - Two interleaved single-VU loops via shared-iterations: -// - listAndCreate: lists current RV, creates one IPPrefixClaim with -// a `created-at-ms` label, deletes it, sleeps, repeats. +// - listAndCreate: lists current RV, creates one IPClaim with a +// `created-at-ms` label, deletes it, sleeps, repeats. // - watch: in lockstep, opens a watch with resourceVersion= // and timeoutSeconds=W. Computes lag = TTFB-anchored arrival time of // the first ADDED event minus the createdAt label value. @@ -550,11 +519,11 @@ export const options = { }, }; -// Issue a GET against the IPPrefixClaim list to obtain the current +// Issue a GET against the IPClaim list to obtain the current // resourceVersion. Returned as a string (k8s RVs are opaque). function currentResourceVersion() { - const params = withProjectTagged(PROJECT, 'list_prefix_claims_rv'); - const res = http.get(`${API_BASE}${prefixClaimPath(NS)}?limit=1`, params); + const params = withProjectTagged(PROJECT, 'list_ipclaims_rv'); + const res = http.get(`${API_BASE}${ipClaimPath(NS)}?limit=1`, params); if (res.status !== 200) { return ''; } @@ -572,13 +541,13 @@ function currentResourceVersion() { // pinpoints when the server started emitting events for our resourceVersion // cursor — which is when our committed CREATE became visible to the watch. function watchOnce(rv, expectedCreatedAtMs) { - const params = withProjectTagged(PROJECT, 'watch_prefix_claims'); + const params = withProjectTagged(PROJECT, 'watch_ipclaims'); // Buffer the connection generously so the server can drive timeoutSeconds // without us cutting it off early. params.timeout = `${WATCH_TIMEOUT_S + 30}s`; const url = - `${API_BASE}${prefixClaimPath(NS)}?watch=true` + + `${API_BASE}${ipClaimPath(NS)}?watch=true` + `&resourceVersion=${encodeURIComponent(rv)}` + `&timeoutSeconds=${WATCH_TIMEOUT_S}` + `&allowWatchBookmarks=true`; @@ -652,17 +621,17 @@ function createClaim(name, createdAtMs) { labels[CREATED_AT_LABEL] = String(createdAtMs); const body = { apiVersion: 'ipam.miloapis.com/v1alpha1', - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: NS, labels }, spec: { ipFamily: 'IPv4', prefixLength: 28, - prefixRef: { name: POOL_NAME }, + poolRef: { name: POOL_NAME }, reclaimPolicy: 'Delete', }, }; - const params = withProjectTagged(PROJECT, 'watch_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(NS)}`, JSON.stringify(body), params); + const params = withProjectTagged(PROJECT, 'watch_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(NS)}`, JSON.stringify(body), params); } export function probe() { @@ -689,7 +658,7 @@ export function probe() { // ADDED event as the first byte. watchOnce(rv, createdAtMs); // 4. Cleanup so the next iteration starts from a known state. - deletePrefixClaimForProject(NS, name, PROJECT); + deleteIPClaimForProject(NS, name, PROJECT); // Small spacing so consecutive probes don't pile up on the changelog. sleep(0.25); } diff --git a/config/components/k6-performance-tests/kustomization.yaml b/config/components/k6-performance-tests/kustomization.yaml index 5ae8f9a..5ee0ee0 100644 --- a/config/components/k6-performance-tests/kustomization.yaml +++ b/config/components/k6-performance-tests/kustomization.yaml @@ -17,7 +17,7 @@ configMapGenerator: - generated/pool-exhaustion.js - generated/read-latency.js - generated/pool-scale.js - - generated/ipaddress-claim-concurrent.js + - generated/host-prefix-claim-concurrent.js - generated/concurrent-claims.js - generated/cross-project-claim-throughput.js - generated/watch-latency.js diff --git a/config/components/k6-performance-tests/testruns/address-concurrent.yaml b/config/components/k6-performance-tests/testruns/address-concurrent.yaml index 97008f7..70744c2 100644 --- a/config/components/k6-performance-tests/testruns/address-concurrent.yaml +++ b/config/components/k6-performance-tests/testruns/address-concurrent.yaml @@ -10,7 +10,7 @@ spec: script: configMap: name: ipam-k6-test-scripts - file: ipaddress-claim-concurrent.js + file: host-prefix-claim-concurrent.js runner: image: grafana/k6:latest serviceAccountName: ipam-k6-runner diff --git a/config/components/observability/alerts/ipam-alerts.yaml b/config/components/observability/alerts/ipam-alerts.yaml index 0f0d3f9..8e39525 100644 --- a/config/components/observability/alerts/ipam-alerts.yaml +++ b/config/components/observability/alerts/ipam-alerts.yaml @@ -312,18 +312,14 @@ spec: # 1. `verb` is uppercase (LIST/GET) in apiserver_request_duration # _seconds. The original lowercase `list|get` matched zero # series and the alert was effectively disabled. - # 2. `ippools` is not a real resource on this apiserver; the pool - # parents are `ipprefixes`, which is already in the list. # End-to-end firing was confirmed by patching the live VMRule to - # threshold > 0.001 while running task test/load:reads — three - # alert instances (ipprefixes/LIST p95 ≈ 124ms, ipprefixes/GET ≈ 20ms, - # ipprefixclaims/LIST ≈ 20ms) transitioned from inactive→firing. + # threshold > 0.001 while running task test/load:reads. expr: | histogram_quantile(0.95, sum by (le, verb, resource) ( rate(apiserver_request_duration_seconds_bucket{ verb=~"LIST|GET", - resource=~"ipprefixes|ipprefixclaims|ipaddresses|ipaddressclaims|asnpools|asnclaims" + resource=~"ippools|ipclaims|ipallocations|asnpools|asnclaims" }[5m]) ) ) > 0.5 diff --git a/config/milo/rbac.yaml b/config/milo/rbac.yaml index ab3604a..78af8cf 100644 --- a/config/milo/rbac.yaml +++ b/config/milo/rbac.yaml @@ -26,10 +26,10 @@ subjects: name: iam.miloapis.com:platform-admin --- # ipam-provider — operator role for the team that manages address pools. -# Full CRUD on pool resources (IPPrefix, IPPrefixClass, ASNPool, -# ASNPoolClass), plus read access to claims so providers can see what's -# currently consuming each pool. Does NOT grant CRUD on claims; consumer -# projects own their own claims via the ipam-consumer role. +# Full CRUD on pool resources (IPPool, ASNPool, ASNPoolClass), plus read +# access to claims so providers can see what's currently consuming each pool. +# Does NOT grant CRUD on claims; consumer projects own their own claims via +# the ipam-consumer role. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -37,19 +37,16 @@ metadata: rules: - apiGroups: ["ipam.miloapis.com"] resources: - - ipprefixes - - ipprefixes/status - - ipprefixclasses - - ipaddresses - - ipaddresses/status + - ippools + - ippools/status - asnpools - asnpools/status - asnpoolclasses verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"] - apiGroups: ["ipam.miloapis.com"] resources: - - ipprefixclaims - - ipaddressclaims + - ipclaims + - ipallocations - asnclaims verbs: ["get", "list", "watch"] # "use" is the verb checked by PoolAccessChecker.CanUsePool when a @@ -57,7 +54,7 @@ rules: # implicitly authorises cross-project allocation; providers controlling # the pool grant this verb to the consumer groups they want to admit. - apiGroups: ["ipam.miloapis.com"] - resources: ["ipprefixes", "asnpools"] + resources: ["ippools", "asnpools"] verbs: ["use"] --- apiVersion: rbac.authorization.k8s.io/v1 @@ -83,15 +80,13 @@ metadata: rules: - apiGroups: ["ipam.miloapis.com"] resources: - - ipprefixclaims - - ipaddressclaims + - ipclaims - asnclaims verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["ipam.miloapis.com"] resources: - - ipprefixes - - ipaddresses - - ipprefixclasses + - ippools + - ipallocations - asnpools - asnpoolclasses verbs: ["get", "list", "watch"] diff --git a/config/overlays/dev/kustomization.yaml b/config/overlays/dev/kustomization.yaml index cf2f205..95cc285 100644 --- a/config/overlays/dev/kustomization.yaml +++ b/config/overlays/dev/kustomization.yaml @@ -16,7 +16,7 @@ components: - ../../components/observability images: - - name: ghcr.io/datum-cloud/ipam-apiserver + - name: ghcr.io/milo-os/ipam newName: ipam-apiserver newTag: dev diff --git a/config/overlays/test-infra/anonymous-rbac.yaml b/config/overlays/test-infra/anonymous-rbac.yaml new file mode 100644 index 0000000..a0cf704 --- /dev/null +++ b/config/overlays/test-infra/anonymous-rbac.yaml @@ -0,0 +1,15 @@ +--- +# Dev-only: grant anonymous users read access via the apiservice so kubectl +# proxy / curl loops work without bearer tokens. Do NOT apply outside dev. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ipam-dev-anonymous +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: system:anonymous diff --git a/config/overlays/test-infra/kustomization.yaml b/config/overlays/test-infra/kustomization.yaml index 3d7254c..1108888 100644 --- a/config/overlays/test-infra/kustomization.yaml +++ b/config/overlays/test-infra/kustomization.yaml @@ -5,20 +5,30 @@ namespace: ipam-system resources: - ../../base + - secret.yaml + - anonymous-rbac.yaml + - tls-certificate.yaml components: - ../../components/namespace - ../../components/api-registration - - ../../components/cert-manager-ca - ../../components/postgres images: - - name: ghcr.io/datum-cloud/ipam-apiserver + - name: ghcr.io/milo-os/ipam newName: ipam-apiserver newTag: dev patches: - path: patches/apiservice-patch.yaml + - path: patches/deployment-patch.yaml + target: + kind: Deployment + name: ipam-apiserver + - path: patches/tls-volume-patch.yaml + target: + kind: Deployment + name: ipam-apiserver labels: - includeSelectors: false diff --git a/config/overlays/test-infra/patches/deployment-patch.yaml b/config/overlays/test-infra/patches/deployment-patch.yaml new file mode 100644 index 0000000..06af168 --- /dev/null +++ b/config/overlays/test-infra/patches/deployment-patch.yaml @@ -0,0 +1,18 @@ +--- +# In kind, images are loaded via `kind load docker-image` and are never +# pulled from a registry. Set Never to prevent Kyverno or other admission +# controllers from mutating imagePullPolicy to Always, which would fail +# because ipam-apiserver:dev doesn't exist on any public registry. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ipam-apiserver +spec: + template: + spec: + initContainers: + - name: migrate + imagePullPolicy: Never + containers: + - name: apiserver + imagePullPolicy: Never diff --git a/config/overlays/test-infra/patches/tls-volume-patch.yaml b/config/overlays/test-infra/patches/tls-volume-patch.yaml new file mode 100644 index 0000000..3436058 --- /dev/null +++ b/config/overlays/test-infra/patches/tls-volume-patch.yaml @@ -0,0 +1,6 @@ +- op: replace + path: /spec/template/spec/volumes/0 + value: + name: tls-certs + secret: + secretName: ipam-tls diff --git a/config/overlays/test-infra/secret.yaml b/config/overlays/test-infra/secret.yaml new file mode 100644 index 0000000..86eabc2 --- /dev/null +++ b/config/overlays/test-infra/secret.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-credentials + namespace: ipam-system + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/component: database + app.kubernetes.io/part-of: ipam.miloapis.com +type: Opaque +stringData: + dsn: "postgres://ipam:devpassword@postgres-postgresql.ipam-system.svc.cluster.local:5432/ipam?sslmode=disable" + password: "devpassword" diff --git a/config/overlays/test-infra/tls-certificate.yaml b/config/overlays/test-infra/tls-certificate.yaml new file mode 100644 index 0000000..8ee6234 --- /dev/null +++ b/config/overlays/test-infra/tls-certificate.yaml @@ -0,0 +1,22 @@ +--- +# cert-manager Certificate (not CSI driver) so the built-in cert-manager +# approver auto-approves the CertificateRequest and writes the TLS secret. +# The CSI driver's CertificateRequests are not approved by the built-in +# approver, causing pods to hang in Init:0/1. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: ipam-tls + namespace: ipam-system +spec: + secretName: ipam-tls + duration: 24h + renewBefore: 1h + dnsNames: + - ipam-apiserver + - ipam-apiserver.ipam-system + - ipam-apiserver.ipam-system.svc + - ipam-apiserver.ipam-system.svc.cluster.local + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer diff --git a/go.mod b/go.mod index 01ae09f..0db413a 100644 --- a/go.mod +++ b/go.mod @@ -84,6 +84,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect + golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.20.0 // indirect @@ -91,6 +92,7 @@ require ( golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/grpc v1.80.0 // indirect @@ -100,6 +102,8 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.36.0 // indirect + k8s.io/code-generator v0.35.0 // indirect + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect k8s.io/kms v0.36.0 // indirect k8s.io/streaming v0.36.0 // indirect k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect diff --git a/go.sum b/go.sum index b403635..7543e59 100644 --- a/go.sum +++ b/go.sum @@ -222,6 +222,8 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJk golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -252,6 +254,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -286,8 +290,12 @@ k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE= k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho= k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= +k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo= k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kms v0.36.0 h1:DPy0VDWi6hCgFMgzV5cNuSDrIROMRcJpTZ1GnB+D368= diff --git a/internal/access/crossproject.go b/internal/access/crossproject.go index 80ddfdd..afae71d 100644 --- a/internal/access/crossproject.go +++ b/internal/access/crossproject.go @@ -27,14 +27,14 @@ import ( var ErrCrossProjectDenied = errors.New("ipam: cross-project pool not accessible") // AuthorizeCrossProjectPrefix enforces the gates that a cross-project -// IPPrefix-pool claim must clear before allocation: +// IPPool claim must clear before allocation: // // 1. A SAR-capable PoolAccessChecker must be configured. When checker // is nil (e.g. the apiserver was started without an authorizer, or // the authorizer is AlwaysAllow) cross-project claims fail closed — -// the visibility=shared marker on the IPPrefixClass is intent-only -// and is never sufficient on its own. -// 2. The source pool's IPPrefixClass must declare visibility=shared. +// the visibility=shared marker on the IPPool is intent-only and is +// never sufficient on its own. +// 2. The source IPPool must declare spec.visibility=shared. // 3. The caller must pass a "use" SubjectAccessReview against the pool. // // All lookups happen inside the supplied transaction so they share the @@ -42,33 +42,20 @@ var ErrCrossProjectDenied = errors.New("ipam: cross-project pool not accessible" // denial path it returns ErrCrossProjectDenied; on infrastructure errors // (DB read failure, SAR error) it returns the underlying error wrapped. // Callers translate the sentinel into a 400 "no pool matches" for -// selector lookups and a 403 Forbidden for direct prefixRef lookups. -// -// Used by both ipprefixclaim and ipaddressclaim AllocatingREST.Create — -// extracted here to keep the auth policy in one place rather than -// duplicated across claim packages. +// selector lookups and a 403 Forbidden for direct poolRef lookups. func AuthorizeCrossProjectPrefix(ctx context.Context, tx pgx.Tx, poolKey string, checker PoolAccessChecker) error { if checker == nil { return ErrCrossProjectDenied } - pool, err := loadPrefixPool(ctx, tx, poolKey) + pool, err := loadIPPool(ctx, tx, poolKey) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrCrossProjectDenied } return fmt.Errorf("load pool for access check: %w", err) } - - classKey := "/ipam.miloapis.com/ipprefixclasses/" + pool.Spec.ClassRef.Name - class, err := loadPrefixClass(ctx, tx, classKey) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrCrossProjectDenied - } - return fmt.Errorf("load class for access check: %w", err) - } - if class.Spec.Visibility != "shared" { + if pool.Spec.Visibility != "shared" { return ErrCrossProjectDenied } @@ -82,11 +69,11 @@ func AuthorizeCrossProjectPrefix(ctx context.Context, tx pgx.Tx, poolKey string, return nil } -// loadPrefixPool decodes the pool's IPPrefix object from ipam_objects +// loadIPPool decodes the IPPool object at poolKey from ipam_objects // without acquiring FOR UPDATE — the SELECT runs inside the same // transaction the allocator will reuse, so the row will be locked when // AllocatePrefix fires its own SELECT FOR UPDATE on the same key. -func loadPrefixPool(ctx context.Context, tx pgx.Tx, poolKey string) (*ipamv1alpha1.IPPrefix, error) { +func loadIPPool(ctx context.Context, tx pgx.Tx, poolKey string) (*ipamv1alpha1.IPPool, error) { var data []byte err := tx.QueryRow(ctx, `SELECT data FROM ipam_objects WHERE key = $1`, @@ -95,26 +82,9 @@ func loadPrefixPool(ctx context.Context, tx pgx.Tx, poolKey string) (*ipamv1alph if err != nil { return nil, fmt.Errorf("load pool object: %w", err) } - var pool ipamv1alpha1.IPPrefix + var pool ipamv1alpha1.IPPool if err := json.Unmarshal(data, &pool); err != nil { return nil, fmt.Errorf("decode pool: %w", err) } return &pool, nil } - -// loadPrefixClass decodes an IPPrefixClass object from ipam_objects. -func loadPrefixClass(ctx context.Context, tx pgx.Tx, classKey string) (*ipamv1alpha1.IPPrefixClass, error) { - var data []byte - err := tx.QueryRow(ctx, - `SELECT data FROM ipam_objects WHERE key = $1`, - classKey, - ).Scan(&data) - if err != nil { - return nil, fmt.Errorf("load class object: %w", err) - } - var class ipamv1alpha1.IPPrefixClass - if err := json.Unmarshal(data, &class); err != nil { - return nil, fmt.Errorf("decode class: %w", err) - } - return &class, nil -} diff --git a/internal/allocator/interface.go b/internal/allocator/interface.go index 8372b46..e0c935f 100644 --- a/internal/allocator/interface.go +++ b/internal/allocator/interface.go @@ -38,10 +38,6 @@ type PrefixAllocator interface { // pool identified by poolKey and returns its CIDR string. AllocatePrefix(ctx context.Context, tx pgx.Tx, poolKey string, prefixLen int, ipFamily string, claimKey string, ownerProject string) (string, error) - // AllocateSingleAddress reserves a single host address within the pool - // identified by poolKey and returns its IP string (without prefix). - AllocateSingleAddress(ctx context.Context, tx pgx.Tx, poolKey string, ipFamily string, claimKey string, ownerProject string) (string, error) - // InsertObject writes a generic API object row into ipam_objects inside // the supplied transaction and returns the assigned resource_version. // Callers use the returned rv to populate metadata.resourceVersion on @@ -49,11 +45,6 @@ type PrefixAllocator interface { // see on subsequent GETs. InsertObject(ctx context.Context, tx pgx.Tx, key, kind, namespace, name string, data []byte) (int64, error) - // InsertChildPrefix writes a child IPPrefix object row into ipam_objects - // inside the supplied transaction. Used when ChildPrefixTemplate is set so - // the child pool materialises atomically with the parent allocation. - InsertChildPrefix(ctx context.Context, tx pgx.Tx, key, namespace, name string, data []byte) error - // Release removes the prefix allocation record matching claimKey. Release(ctx context.Context, tx pgx.Tx, claimKey string) error diff --git a/internal/allocator/prefix.go b/internal/allocator/prefix.go index 18d720d..e50edb8 100644 --- a/internal/allocator/prefix.go +++ b/internal/allocator/prefix.go @@ -27,18 +27,18 @@ func tenantsFromPoolKey(poolKey string) (project, org string) { } // PostgresPrefixAllocator implements PrefixAllocator atop ipam_objects and -// ipam_prefix_allocations. It performs the synchronous allocation sequence +// ipam_cidr_allocations. It performs the synchronous allocation sequence // described in the architecture: // // BEGIN // SELECT data FROM ipam_objects WHERE key=$poolKey FOR UPDATE -// SELECT allocated_cidr FROM ipam_prefix_allocations WHERE pool_key=$poolKey +// SELECT allocated_cidr FROM ipam_cidr_allocations WHERE pool_key=$poolKey // -- in-Go: FindFirstAvailableBlock(parents, existing, prefixLen, strategy) -// INSERT INTO ipam_prefix_allocations (...) +// INSERT INTO ipam_cidr_allocations (...) // COMMIT // // The pool row's lock is what serialises concurrent claims; the -// ipam_prefix_allocations rows are not individually locked, so the work is +// ipam_cidr_allocations rows are not individually locked, so the work is // O(existing) per allocation rather than O(pool size). type PostgresPrefixAllocator struct{} @@ -49,7 +49,7 @@ func NewPostgresPrefixAllocator() *PostgresPrefixAllocator { // AllocatePrefix implements PrefixAllocator.AllocatePrefix. func (a *PostgresPrefixAllocator) AllocatePrefix(ctx context.Context, tx pgx.Tx, poolKey string, prefixLen int, ipFamily string, claimKey string, ownerProject string) (string, error) { - pool, err := lockAndDecodePool(ctx, tx, poolKey) + pool, err := lockAndDecodeIPPool(ctx, tx, poolKey) if err != nil { return "", err } @@ -64,12 +64,7 @@ func (a *PostgresPrefixAllocator) AllocatePrefix(ctx context.Context, tx pgx.Tx, return "", err } - strategy := allocation.Strategy(pool.Spec.Allocation.Strategy) - if strategy == "" { - strategy = allocation.FirstFit - } - - cidr, err := allocation.FindFirstAvailableBlock(parents, existing, prefixLen, strategy) + cidr, err := allocation.FindFirstAvailableBlock(parents, existing, prefixLen, allocation.Strategy(pool.Spec.Allocation.Strategy)) if err != nil { if errors.Is(err, allocation.ErrPoolExhausted) { return "", ErrPoolExhausted @@ -77,7 +72,7 @@ func (a *PostgresPrefixAllocator) AllocatePrefix(ctx context.Context, tx pgx.Tx, return "", fmt.Errorf("compute next prefix: %w", err) } - if err := insertPrefixAllocation(ctx, tx, poolKey, cidr.String(), claimKey, ipFamily, false, ownerProject); err != nil { + if err := insertPrefixAllocation(ctx, tx, poolKey, cidr.String(), claimKey, ipFamily, ownerProject); err != nil { return "", err } @@ -85,69 +80,20 @@ func (a *PostgresPrefixAllocator) AllocatePrefix(ctx context.Context, tx pgx.Tx, // set, so the post-allocation utilization can be computed from data // already in scope without an extra DB round-trip. updated := append(append([]net.IPNet(nil), existing...), *cidr) + if err := persistPoolCapacity(ctx, tx, pool, poolKey, parents, updated); err != nil { + return "", fmt.Errorf("update pool capacity after allocation: %w", err) + } publishPrefixUtilization(poolKey, ipFamily, parents, updated) klog.V(2).InfoS("Allocated prefix", "pool", poolKey, "cidr", cidr.String(), "claim", claimKey, "ownerProject", ownerProject) return cidr.String(), nil } -// AllocateSingleAddress implements PrefixAllocator.AllocateSingleAddress. -func (a *PostgresPrefixAllocator) AllocateSingleAddress(ctx context.Context, tx pgx.Tx, poolKey string, ipFamily string, claimKey string, ownerProject string) (string, error) { - pool, err := lockAndDecodePool(ctx, tx, poolKey) - if err != nil { - return "", err - } - - parents, err := parsePoolCIDR(pool) - if err != nil { - return "", err - } - - existing, err := loadExistingAllocations(ctx, tx, poolKey) - if err != nil { - return "", err - } - - hostBits := 32 - if ipFamily == "IPv6" { - hostBits = 128 - } - - strategy := allocation.Strategy(pool.Spec.Allocation.Strategy) - if strategy == "" { - strategy = allocation.FirstFit - } - - cidr, err := allocation.FindFirstAvailableBlock(parents, existing, hostBits, strategy) - if err != nil { - if errors.Is(err, allocation.ErrPoolExhausted) { - return "", ErrPoolExhausted - } - return "", fmt.Errorf("compute next address: %w", err) - } - - if err := insertPrefixAllocation(ctx, tx, poolKey, cidr.String(), claimKey, ipFamily, false, ownerProject); err != nil { - return "", err - } - - updated := append(append([]net.IPNet(nil), existing...), *cidr) - publishPrefixUtilization(poolKey, ipFamily, parents, updated) - - klog.V(2).InfoS("Allocated single address", "pool", poolKey, "addr", cidr.IP.String(), "claim", claimKey, "ownerProject", ownerProject) - return cidr.IP.String(), nil -} - // InsertObject implements PrefixAllocator.InsertObject. func (a *PostgresPrefixAllocator) InsertObject(ctx context.Context, tx pgx.Tx, key, kind, namespace, name string, data []byte) (int64, error) { return insertObject(ctx, tx, key, kind, namespace, name, data) } -// InsertChildPrefix implements PrefixAllocator.InsertChildPrefix. -func (a *PostgresPrefixAllocator) InsertChildPrefix(ctx context.Context, tx pgx.Tx, key, namespace, name string, data []byte) error { - _, err := insertObject(ctx, tx, key, "IPPrefix", namespace, name, data) - return err -} - // insertObject is the shared helper used by both PrefixAllocator and // ASNAllocator implementations. The RETURNING clause hands back the rv that // the sequence default assigned, so the caller can stamp it on the in-memory @@ -203,7 +149,7 @@ func labelsFromData(data []byte) []byte { // rows from RETURNING; in that case the gauge update is silently skipped. func (a *PostgresPrefixAllocator) Release(ctx context.Context, tx pgx.Tx, claimKey string) error { rows, err := tx.Query(ctx, - `DELETE FROM ipam_prefix_allocations WHERE claim_key = $1 + `DELETE FROM ipam_cidr_allocations WHERE claim_key = $1 RETURNING pool_key, ip_family`, claimKey, ) if err != nil { @@ -228,7 +174,7 @@ func (a *PostgresPrefixAllocator) Release(ctx context.Context, tx pgx.Tx, claimK } for _, r := range releases { - pool, perr := lockAndDecodePool(ctx, tx, r.poolKey) + pool, perr := lockAndDecodeIPPool(ctx, tx, r.poolKey) if perr != nil { // Pool already gone (cascading delete); nothing to publish. if errors.Is(perr, ErrPoolNotFound) { @@ -244,11 +190,45 @@ func (a *PostgresPrefixAllocator) Release(ctx context.Context, tx pgx.Tx, claimK if perr != nil { return fmt.Errorf("reload allocations after release: %w", perr) } + if perr := persistPoolCapacity(ctx, tx, pool, r.poolKey, parents, remaining); perr != nil { + return fmt.Errorf("update pool capacity after release: %w", perr) + } publishPrefixUtilization(r.poolKey, r.ipFamily, parents, remaining) } return nil } +// persistPoolCapacity recomputes Total/Allocated/Available for the pool and +// writes the updated pool object back to ipam_objects (+ MODIFIED changelog) +// within the current transaction. Must be called inside the transaction that +// inserted or deleted the allocation row so the capacity stays consistent. +func persistPoolCapacity(ctx context.Context, tx pgx.Tx, pool *ipamv1alpha1.IPPool, poolKey string, parents, allocations []net.IPNet) error { + var total, allocated int64 + for _, p := range parents { + total += allocation.CountAddresses(p) + } + for _, a := range allocations { + allocated += allocation.CountAddresses(a) + } + available := total - allocated + if available < 0 { + available = 0 + } + pool.Status.Capacity = ipamv1alpha1.PoolCapacity{ + Total: total, + Allocated: allocated, + Available: available, + } + data, err := json.Marshal(pool) + if err != nil { + return fmt.Errorf("marshal pool: %w", err) + } + if _, err := updateObject(ctx, tx, poolKey, data); err != nil { + return fmt.Errorf("write pool: %w", err) + } + return nil +} + // DeleteObject implements PrefixAllocator.DeleteObject. func (a *PostgresPrefixAllocator) DeleteObject(ctx context.Context, tx pgx.Tx, key string) (int64, error) { return deleteObject(ctx, tx, key) @@ -328,9 +308,11 @@ func deleteObject(ctx context.Context, tx pgx.Tx, key string) (int64, error) { // helpers // ---------------------------------------------------------------------------- -// lockAndDecodePool acquires a row-level lock on the pool row in ipam_objects -// and decodes its data column as an IPPrefix. -func lockAndDecodePool(ctx context.Context, tx pgx.Tx, poolKey string) (*ipamv1alpha1.IPPrefix, error) { +// lockAndDecodeIPPool acquires a row-level lock on the pool row in +// ipam_objects and decodes its data column as an IPPool. Status.AllocatedCIDR +// is preferred (populated for child pools after provisioning); Spec.CIDR is +// the fallback used by root pools whose CIDR is operator-supplied. +func lockAndDecodeIPPool(ctx context.Context, tx pgx.Tx, poolKey string) (*ipamv1alpha1.IPPool, error) { defer metrics.ObserveQuery("select_pool_for_update", time.Now()) var data []byte err := tx.QueryRow(ctx, @@ -344,20 +326,20 @@ func lockAndDecodePool(ctx context.Context, tx pgx.Tx, poolKey string) (*ipamv1a return nil, fmt.Errorf("lock pool row: %w", err) } - var pool ipamv1alpha1.IPPrefix + var pool ipamv1alpha1.IPPool if err := json.Unmarshal(data, &pool); err != nil { return nil, fmt.Errorf("decode pool object: %w", err) } return &pool, nil } -// parsePoolCIDR returns the parent CIDR (single-element slice). IPPrefix +// parsePoolCIDR returns the parent CIDR (single-element slice). IPPool // pools always have a single CIDR; the slice form matches // allocation.FindFirstAvailableBlock's parameter shape. -func parsePoolCIDR(pool *ipamv1alpha1.IPPrefix) ([]net.IPNet, error) { +func parsePoolCIDR(pool *ipamv1alpha1.IPPool) ([]net.IPNet, error) { cidrStr := pool.Spec.CIDR - if pool.Status.CIDR != "" { - cidrStr = pool.Status.CIDR + if pool.Status.AllocatedCIDR != "" { + cidrStr = pool.Status.AllocatedCIDR } _, ipnet, err := net.ParseCIDR(cidrStr) if err != nil { @@ -371,7 +353,7 @@ func loadExistingAllocations(ctx context.Context, tx pgx.Tx, poolKey string) ([] defer metrics.ObserveQuery("load_existing_allocations", time.Now()) rows, err := tx.Query(ctx, `SELECT host(allocated_cidr) || '/' || masklen(allocated_cidr) - FROM ipam_prefix_allocations + FROM ipam_cidr_allocations WHERE pool_key = $1`, poolKey, ) @@ -432,22 +414,22 @@ func publishPrefixUtilization(poolKey, ipFamily string, parents, allocated []net // distinguish small/full pools from large/half-full pools at a glance. // Float64 rather than int64: a /48 IPv6 pool has 2^80 addresses, well // past int64. - metrics.SetPoolCapacity(poolKey, ipFamily, "ipprefixes", project, org, totalF, usedF) + metrics.SetPoolCapacity(poolKey, ipFamily, "ippools", project, org, totalF, usedF) if total.Sign() == 0 { - metrics.SetPoolUtilization(poolKey, ipFamily, "ipprefixes", project, org, 0) + metrics.SetPoolUtilization(poolKey, ipFamily, "ippools", project, org, 0) return } - metrics.SetPoolUtilization(poolKey, ipFamily, "ipprefixes", project, org, usedF/totalF) + metrics.SetPoolUtilization(poolKey, ipFamily, "ippools", project, org, usedF/totalF) } // insertPrefixAllocation records a new allocation row. -func insertPrefixAllocation(ctx context.Context, tx pgx.Tx, poolKey, cidr, claimKey, ipFamily string, isChildPool bool, ownerProject string) error { +func insertPrefixAllocation(ctx context.Context, tx pgx.Tx, poolKey, cidr, claimKey, ipFamily string, ownerProject string) error { defer metrics.ObserveQuery("insert_allocation", time.Now()) _, err := tx.Exec(ctx, - `INSERT INTO ipam_prefix_allocations - (pool_key, allocated_cidr, claim_key, ip_family, is_child_pool, reclaim_policy, owner_project) - VALUES ($1, $2, $3, $4, $5, 'Delete', $6)`, - poolKey, cidr, claimKey, ipFamily, isChildPool, ownerProject, + `INSERT INTO ipam_cidr_allocations + (pool_key, allocated_cidr, claim_key, ip_family, reclaim_policy, owner_project) + VALUES ($1, $2, $3, $4, 'Delete', $5)`, + poolKey, cidr, claimKey, ipFamily, ownerProject, ) if err != nil { return fmt.Errorf("insert allocation: %w", err) diff --git a/internal/allocator/resolve.go b/internal/allocator/resolve.go index 6aad91e..a662b34 100644 --- a/internal/allocator/resolve.go +++ b/internal/allocator/resolve.go @@ -16,11 +16,10 @@ import ( ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" ) -// ResolvePrefixPool returns the storage key of an IPPrefix pool that -// satisfies the supplied label selector. It lists pools belonging to the -// caller's project (or the platform scope when ownerProject is empty), -// decodes each into an IPPrefix, applies the selector, and returns the first -// match by storage key. +// ResolveIPPool returns the storage key of an IPPool that satisfies the +// supplied label selector. It lists pools belonging to the caller's project +// (or the platform scope when ownerProject is empty), decodes each into an +// IPPool, applies the selector, and returns the first match by storage key. // // The first-match policy is deliberately simple: it is deterministic across // callers, requires no per-pool capacity probe, and lets operators steer @@ -33,23 +32,23 @@ import ( // the ipFamily comes from the resolved pool itself). // // Returns ErrPoolNotFound if no pool matches the selector. -func ResolvePrefixPool(ctx context.Context, tx pgx.Tx, selector *metav1.LabelSelector, ownerProject, ipFamily string) (string, error) { - defer metrics.ObserveQuery("resolve_prefix_pool", time.Now()) +func ResolveIPPool(ctx context.Context, tx pgx.Tx, selector *metav1.LabelSelector, ownerProject, ipFamily string) (string, error) { + defer metrics.ObserveQuery("resolve_ip_pool", time.Now()) sel, err := labelSelectorOrEverything(selector) if err != nil { return "", fmt.Errorf("compile label selector: %w", err) } - keys, datas, err := listPools(ctx, tx, "IPPrefix", ownerProject) + keys, datas, err := listPools(ctx, tx, "IPPool", ownerProject) if err != nil { return "", err } for i, key := range keys { - var pool ipamv1alpha1.IPPrefix + var pool ipamv1alpha1.IPPool if err := json.Unmarshal(datas[i], &pool); err != nil { - return "", fmt.Errorf("decode IPPrefix pool %q: %w", key, err) + return "", fmt.Errorf("decode IPPool %q: %w", key, err) } if ipFamily != "" && string(pool.Spec.IPFamily) != ipFamily { continue @@ -115,8 +114,8 @@ func labelSelectorOrEverything(selector *metav1.LabelSelector) (labels.Selector, // pluraliser in here. func plural(kind string) string { switch kind { - case "IPPrefix": - return "ipprefixes" + case "IPPool": + return "ippools" } // Conservative fallback — lowercase + "s" — never reached for the kinds // this resolver supports today, but defends against future kinds being diff --git a/internal/apiserver/apiserver.go b/internal/apiserver/apiserver.go index 3141d06..a4e073e 100644 --- a/internal/apiserver/apiserver.go +++ b/internal/apiserver/apiserver.go @@ -23,10 +23,9 @@ import ( _ "go.miloapis.com/ipam/internal/metrics" "go.miloapis.com/ipam/internal/access" "go.miloapis.com/ipam/internal/allocator" - "go.miloapis.com/ipam/internal/registry/ipam/ipaddress" - "go.miloapis.com/ipam/internal/registry/ipam/ipaddressclaim" - "go.miloapis.com/ipam/internal/registry/ipam/ipprefix" - "go.miloapis.com/ipam/internal/registry/ipam/ipprefixclaim" + "go.miloapis.com/ipam/internal/registry/ipam/ipallocation" + "go.miloapis.com/ipam/internal/registry/ipam/ipclaim" + "go.miloapis.com/ipam/internal/registry/ipam/ippool" "go.miloapis.com/ipam/pkg/apis/ipam/install" "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" ) @@ -58,12 +57,12 @@ func init() { // settings. type ExtraConfig struct { // PrefixAllocator drives synchronous CIDR/single-address allocation for - // IPPrefixClaim and IPAddressClaim creates. Required. + // IPClaim creates. Required. PrefixAllocator allocator.PrefixAllocator // AllocatorPool is the pgx pool the allocators commit against. The claim // REST handlers open transactions on this pool. Required. AllocatorPool *pgxpool.Pool - // PoolChecker authorises cross-project IPPrefixClaim creates via + // PoolChecker authorises cross-project IPClaim creates via // SubjectAccessReview. nil bypasses the check (e.g. when no authorizer // is configured). PoolChecker access.PoolAccessChecker @@ -118,8 +117,8 @@ func (c completedConfig) New() (*IPAMServer, error) { allocCodec := Codecs.LegacyCodec(v1alpha1.SchemeGroupVersion) // Watch exclusions are intentionally NOT configured on the postgres - // RESTOptionsGetter for the *claim resources (ipprefixclaims, - // ipaddressclaims). At first glance the AllocatingREST + // RESTOptionsGetter for the *claim resources (ipclaims, asnclaims). + // At first glance the AllocatingREST // pattern looks like it might double-emit watch events — Create writes // the claim row + ADDED changelog entry directly via // allocator.InsertObject (bypassing the embedded Store.Create), and @@ -145,55 +144,38 @@ func (c completedConfig) New() (*IPAMServer, error) { v1alpha1Storage := map[string]rest.Storage{} - // IPPrefixClass — cluster-scoped, no status subresource. - prefixClassStore, err := ipprefix.NewClassStorage(Scheme, c.GenericConfig.RESTOptionsGetter) - if err != nil { - return nil, fmt.Errorf("create IPPrefixClass storage: %w", err) - } - v1alpha1Storage["ipprefixclasses"] = prefixClassStore - - // IPPrefix — cluster-scoped, with status subresource, and (when allocator - // pool is configured) deletion protection that rejects deletes for prefixes - // with active allocations. - prefixStore, prefixStatusStore, err := ipprefix.NewPrefixStorage( + // IPPool — cluster-scoped, with status subresource. Root pools persist + // directly; child pools (with spec.parentPoolRef) allocate a sub-prefix + // from the parent pool synchronously inside Create. + ipPoolStore, ipPoolStatusStore, err := ippool.NewIPPoolStorage( Scheme, c.GenericConfig.RESTOptionsGetter, + c.ExtraConfig.PrefixAllocator, c.ExtraConfig.AllocatorPool, + allocCodec, ) if err != nil { - return nil, fmt.Errorf("create IPPrefix storage: %w", err) + return nil, fmt.Errorf("create IPPool storage: %w", err) } - v1alpha1Storage["ipprefixes"] = prefixStore - v1alpha1Storage["ipprefixes/status"] = prefixStatusStore + v1alpha1Storage["ippools"] = ipPoolStore + v1alpha1Storage["ippools/status"] = ipPoolStatusStore - // IPPrefixClaim — namespaced, with status subresource. - prefixClaimStore, prefixClaimStatusStore, err := ipprefixclaim.NewAllocatingStorage( + // IPAllocation — namespaced, simple CRUD. Rows are system-created by the + // IPClaim Create handler inside the allocation transaction, so this + // storage carries no allocator/db dependency. + ipAllocStore, ipAllocStatusStore, err := ipallocation.NewAllocationStorage( Scheme, c.GenericConfig.RESTOptionsGetter, - c.ExtraConfig.PrefixAllocator, - c.ExtraConfig.AllocatorPool, - allocCodec, - c.ExtraConfig.PoolChecker, ) if err != nil { - return nil, fmt.Errorf("create IPPrefixClaim storage: %w", err) + return nil, fmt.Errorf("create IPAllocation storage: %w", err) } - v1alpha1Storage["ipprefixclaims"] = prefixClaimStore - v1alpha1Storage["ipprefixclaims/status"] = prefixClaimStatusStore + v1alpha1Storage["ipallocations"] = ipAllocStore + v1alpha1Storage["ipallocations/status"] = ipAllocStatusStore - // IPAddress — namespaced, with status subresource. - addrStore, addrStatusStore, err := ipaddress.NewStorage(Scheme, c.GenericConfig.RESTOptionsGetter) - if err != nil { - return nil, fmt.Errorf("create IPAddress storage: %w", err) - } - v1alpha1Storage["ipaddresses"] = addrStore - v1alpha1Storage["ipaddresses/status"] = addrStatusStore - - // IPAddressClaim — namespaced, with status subresource. poolChecker - // is passed so cross-project allocation (prefixSelector.projectRef - // targeting another project) goes through the same SAR + visibility - // gate as IPPrefixClaim. - addrClaimStore, addrClaimStatusStore, err := ipaddressclaim.NewAllocatingStorage( + // IPClaim — namespaced, with status subresource. Synchronous allocation + // against an IPPool; produces an IPAllocation in the same transaction. + ipClaimStore, ipClaimStatusStore, err := ipclaim.NewAllocatingStorage( Scheme, c.GenericConfig.RESTOptionsGetter, c.ExtraConfig.PrefixAllocator, @@ -202,10 +184,10 @@ func (c completedConfig) New() (*IPAMServer, error) { c.ExtraConfig.PoolChecker, ) if err != nil { - return nil, fmt.Errorf("create IPAddressClaim storage: %w", err) + return nil, fmt.Errorf("create IPClaim storage: %w", err) } - v1alpha1Storage["ipaddressclaims"] = addrClaimStore - v1alpha1Storage["ipaddressclaims/status"] = addrClaimStatusStore + v1alpha1Storage["ipclaims"] = ipClaimStore + v1alpha1Storage["ipclaims/status"] = ipClaimStatusStore apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1Storage diff --git a/internal/fieldindex/fieldindex.go b/internal/fieldindex/fieldindex.go index bb3bc98..c47f605 100644 --- a/internal/fieldindex/fieldindex.go +++ b/internal/fieldindex/fieldindex.go @@ -15,7 +15,7 @@ type FieldIndex struct { IndexName string // Expression is the full CREATE INDEX body after "ON ipam_objects": // ((convert_from(data, 'UTF8')::jsonb -> 'spec' ->> 'ipFamily')) - // WHERE kind = 'IPPrefixClaim' + // WHERE kind = 'IPClaim' Expression string } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 1368114..a5fbab6 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -29,7 +29,7 @@ var ( // label. See docs/production-readiness.md for the cardinality discussion. // AllocationDuration tracks the latency of synchronous allocation - // transactions for IPPrefixClaim, IPAddressClaim, and ASNClaim. + // transactions for IPClaim and ASNClaim. // // METRIC NAMING NOTE: the spec (.claude/agents/observability.md) lists a // single `ipam_allocation_total` counter alongside the duration histogram. @@ -48,7 +48,7 @@ var ( Buckets: metrics.DefBuckets, StabilityLevel: metrics.ALPHA, }, - // resource: "ipprefixclaim" | "ipaddressclaim" | "asnclaim" + // resource: "ipclaims" | "asnclaims" // result: "success" | "exhausted" | "error" // ip_family: "IPv4" | "IPv6" | "ASN" — derived from the claim spec or // the resolved CIDR for prefix/address claims, hardcoded @@ -74,7 +74,7 @@ var ( Help: "Total number of allocation attempts (incremented at the top of the allocation path before any DB work)", StabilityLevel: metrics.ALPHA, }, - // resource: "ipprefixclaim" | "ipaddressclaim" | "asnclaim" + // resource: "ipclaims" | "asnclaims" // ip_family: "IPv4" | "IPv6" | "ASN" — sourced from the same handler // value used for ObserveAllocationDuration so attempts, // failures, and the latency histogram split identically. @@ -90,7 +90,7 @@ var ( Help: "Total number of allocation failures", StabilityLevel: metrics.ALPHA, }, - // resource: "ipprefixclaim" | "ipaddressclaim" | "asnclaim" + // resource: "ipclaims" | "asnclaims" // reason: "pool_exhausted" | "pool_not_found" | "verification_required" | "tx_error" | "internal" // ip_family: "IPv4" | "IPv6" | "ASN" — mirrors AllocationAttempts so // success-ratio = 1 - (failures / attempts) can be computed @@ -110,7 +110,7 @@ var ( Help: "Ratio of allocated to total capacity per pool", StabilityLevel: metrics.ALPHA, }, - // resource is the plural lowercase pool kind ("ipprefixes" | + // resource is the plural lowercase pool kind ("ippools" | // "asnpools"), kept here so dashboards can split prefix vs ASN // utilization without parsing pool_key — same shape used by // PoolCapacity and PoolAllocated. @@ -125,7 +125,7 @@ var ( // /28 with 8 free even though both are at 50%). // // Values are addresses for IPv4 / IPv6 prefix pools and ASN counts for - // ASN pools. resource is "ipprefixes" | "asnpools" so a single PromQL + // ASN pools. resource is "ippools" | "asnpools" so a single PromQL // can split prefix vs ASN capacity without parsing pool_key. PoolCapacity = metrics.NewGaugeVec( &metrics.GaugeOpts{ @@ -174,7 +174,7 @@ var ( // Suggested query_name values: // "select_pool_for_update" — SELECT data FROM ipam_objects ... FOR UPDATE // "load_existing_allocations" — SELECT existing CIDRs/ASNs for the pool - // "insert_allocation" — INSERT INTO ipam_prefix_allocations / ipam_asn_allocations + // "insert_allocation" — INSERT INTO ipam_cidr_allocations / ipam_asn_allocations // "insert_object" — INSERT INTO ipam_objects (claim row + child prefix) // "update_pool_status" — UPDATE ipam_objects ... when the pool status row is rewritten PostgresQueryDuration = metrics.NewHistogramVec( @@ -254,8 +254,8 @@ var ( // (predicate-rejected) entries are NOT counted — only events the watcher // actually hands off downstream. // - // kind: lowercase plural resource (ipprefixes, ipprefixclaims, - // ipaddresses, ipaddressclaims, asnpools, asnclaims, ...). + // kind: lowercase plural resource (ippools, ipclaims, ipallocations, + // asnpools, asnclaims, ...). // Derived from the storage key prefix; "unknown" if the key // does not match the expected /ipam.miloapis.com//... // layout (which would indicate a bug, not user input). @@ -309,7 +309,7 @@ var ( // extremely rare (transaction-only failure mode) and surface as // apiserver_request_total{verb="delete", code!~"2.."} already. // - // resource: "ipprefixclaim" | "ipaddressclaim" | "asnclaim". + // resource: "ipclaims" | "asnclaims". Releases = metrics.NewCounterVec( &metrics.CounterOpts{ Namespace: "ipam", @@ -359,7 +359,7 @@ func RecordDrainCycle(kind string, multiBatch bool) { } // RecordWatchEvent increments the watch_events_total counter for the given -// resource kind (lowercase plural, e.g. "ipprefixclaims") and event type +// resource kind (lowercase plural, e.g. "ipclaims") and event type // ("ADDED" | "MODIFIED" | "DELETED"). Called from the watcher's dispatch // path, immediately after an event is handed off to the subscriber channel. func RecordWatchEvent(kind, eventType string) { @@ -367,7 +367,7 @@ func RecordWatchEvent(kind, eventType string) { } // RecordRelease increments the releases_total counter for the given claim -// resource ("ipprefixclaim" | "ipaddressclaim" | "asnclaim"). Called from +// resource ("ipclaims" | "asnclaims"). Called from // the claim Delete handler immediately after the deletion transaction // commits successfully. func RecordRelease(resource string) { @@ -424,7 +424,7 @@ func ObservePgxpoolStat(stat PgxpoolStatLike) { // // start := time.Now() // defer func() { -// metrics.ObserveAllocationDuration("ipprefixclaim", result, ipFamily, project, org, start) +// metrics.ObserveAllocationDuration("ipclaims", result, ipFamily, project, org, start) // }() // // where the surrounding code mutates `result` ("success" | "exhausted" | @@ -461,7 +461,7 @@ func RecordAllocationFailure(resource, reason, ipFamily, project, org string) { // poolKey is the storage-layer key (the same key used as the FOR UPDATE // target in the allocation transaction); ipFamily is "IPv4", "IPv6", or // "ASN" for ASN pools. resource is the plural lowercase pool kind -// ("ipprefixes" | "asnpools") and matches the labels used by SetPoolCapacity +// ("ippools" | "asnpools") and matches the labels used by SetPoolCapacity // so all three pool gauges split identically. project / org carry the owning // tenant for org-level dashboards. Ratios outside [0, 1] are clamped — a // buggy capacity computation should not poison the dashboard. @@ -477,7 +477,7 @@ func SetPoolUtilization(poolKey, ipFamily, resource, project, org string, ratio // SetPoolCapacity publishes the absolute total / allocated counts for a pool // alongside the existing utilization ratio. Callers should invoke this in // the same place they invoke SetPoolUtilization so all three gauges advance -// together. resource is the plural lowercase resource name ("ipprefixes" | +// together. resource is the plural lowercase resource name ("ippools" | // "asnpools") so dashboards can split prefix vs ASN capacity without parsing // pool_key. // diff --git a/internal/registry/ipam/fieldindexes.go b/internal/registry/ipam/fieldindexes.go index aaf8418..e2a21cc 100644 --- a/internal/registry/ipam/fieldindexes.go +++ b/internal/registry/ipam/fieldindexes.go @@ -2,19 +2,17 @@ package ipamregistry import ( "go.miloapis.com/ipam/internal/fieldindex" - "go.miloapis.com/ipam/internal/registry/ipam/ipaddress" - "go.miloapis.com/ipam/internal/registry/ipam/ipaddressclaim" - "go.miloapis.com/ipam/internal/registry/ipam/ipprefix" - "go.miloapis.com/ipam/internal/registry/ipam/ipprefixclaim" + "go.miloapis.com/ipam/internal/registry/ipam/ipallocation" + "go.miloapis.com/ipam/internal/registry/ipam/ipclaim" + "go.miloapis.com/ipam/internal/registry/ipam/ippool" ) // AllFieldIndexes returns the combined set of SQL expression indexes for every // IPAM resource. Pass the result to fieldindex.SyncIndexes at startup. func AllFieldIndexes() []fieldindex.FieldIndex { var all []fieldindex.FieldIndex - all = append(all, ipprefixclaim.FieldIndexes...) - all = append(all, ipaddressclaim.FieldIndexes...) - all = append(all, ipaddress.FieldIndexes...) - all = append(all, ipprefix.FieldIndexes...) + all = append(all, ipclaim.FieldIndexes...) + all = append(all, ipallocation.FieldIndexes...) + all = append(all, ippool.FieldIndexes...) return all } diff --git a/internal/registry/ipam/ipaddress/storage.go b/internal/registry/ipam/ipaddress/storage.go deleted file mode 100644 index 0ce8887..0000000 --- a/internal/registry/ipam/ipaddress/storage.go +++ /dev/null @@ -1,75 +0,0 @@ -// Package ipaddress provides REST storage for the IPAddress resource. The -// storage is the standard genericregistry.Store backed by the postgres -// RESTOptionsGetter; allocator integration lives in the ipaddressclaim -// package because IPAddress objects are materialised by the IPAddressClaim -// allocating REST or created directly by an operator for fixed assignments. -package ipaddress - -import ( - "context" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/generic" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "k8s.io/apiserver/pkg/registry/rest" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/pkg/apis/ipam" - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" -) - -type IPAddressStorage struct { - *genericregistry.Store -} - -type IPAddressStatusStorage struct { - store *genericregistry.Store -} - -func (s *IPAddressStatusStorage) New() runtime.Object { return &ipam.IPAddress{} } -func (s *IPAddressStatusStorage) Destroy() {} - -func (s *IPAddressStatusStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return s.store.Get(ctx, name, options) -} - -func (s *IPAddressStatusStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) -} - -func (s *IPAddressStatusStorage) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return s.store.GetResetFields() -} - -func (s *IPAddressStatusStorage) ConvertToTable(ctx context.Context, obj runtime.Object, opts runtime.Object) (*metav1.Table, error) { - return s.store.ConvertToTable(ctx, obj, opts) -} - -func NewStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*IPAddressStorage, *IPAddressStatusStorage, error) { - strategy := NewStrategy(scheme) - statusStrategy := NewStatusStrategy(scheme) - - store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &ipam.IPAddress{} }, - NewListFunc: func() runtime.Object { return &ipam.IPAddressList{} }, - DefaultQualifiedResource: v1alpha1.Resource("ipaddresses"), - SingularQualifiedResource: v1alpha1.Resource("ipaddress"), - - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - - TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ipaddresses")), - } - - if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}); err != nil { - return nil, nil, err - } - - statusStore := *store - statusStore.UpdateStrategy = statusStrategy - statusStore.ResetFieldsStrategy = statusStrategy - - return &IPAddressStorage{store}, &IPAddressStatusStorage{store: &statusStore}, nil -} diff --git a/internal/registry/ipam/ipaddress/strategy.go b/internal/registry/ipam/ipaddress/strategy.go deleted file mode 100644 index fd3e0f3..0000000 --- a/internal/registry/ipam/ipaddress/strategy.go +++ /dev/null @@ -1,183 +0,0 @@ -package ipaddress - -import ( - "context" - "fmt" - "net" - - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/storage" - "k8s.io/apiserver/pkg/storage/names" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/internal/fieldindex" - "go.miloapis.com/ipam/pkg/apis/ipam" -) - -// FieldIndexes are the SQL expression indexes that back IPAddress field -// selectors declared in SelectableFields. Applied idempotently by SyncIndexes. -var FieldIndexes = []fieldindex.FieldIndex{ - { - IndexName: "idx_ipam_ipaddress_address", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'address')) WHERE kind = 'IPAddress'`, - }, - { - IndexName: "idx_ipam_ipaddress_ip_family", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) WHERE kind = 'IPAddress'`, - }, - { - IndexName: "idx_ipam_ipaddress_prefix_ref_name", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' -> 'prefixRef' ->> 'name')) WHERE kind = 'IPAddress'`, - }, -} - -type ipAddressStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -type ipAddressStatusStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -func NewStrategy(typer runtime.ObjectTyper) ipAddressStrategy { - return ipAddressStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func NewStatusStrategy(typer runtime.ObjectTyper) ipAddressStatusStrategy { - return ipAddressStatusStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func (ipAddressStrategy) NamespaceScoped() bool { return true } - -func (ipAddressStrategy) PrepareForCreate(_ context.Context, obj runtime.Object) { - a := obj.(*ipam.IPAddress) - a.Status = ipam.IPAddressStatus{} -} - -func (ipAddressStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPAddress) - o := old.(*ipam.IPAddress) - n.Status = o.Status -} - -func (ipAddressStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { - return validateIPAddress(obj.(*ipam.IPAddress)) -} - -func (ipAddressStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { return nil } -func (ipAddressStrategy) AllowCreateOnUpdate() bool { return false } -func (ipAddressStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipAddressStrategy) Canonicalize(_ runtime.Object) {} - -func (ipAddressStrategy) ValidateUpdate(_ context.Context, obj, old runtime.Object) field.ErrorList { - n := obj.(*ipam.IPAddress) - o := old.(*ipam.IPAddress) - allErrs := validateIPAddress(n) - if n.Spec.Address != o.Spec.Address { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "address"), "spec.address is immutable")) - } - if n.Spec.IPFamily != o.Spec.IPFamily { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ipFamily"), "spec.ipFamily is immutable")) - } - if n.Spec.PrefixRef != o.Spec.PrefixRef { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "prefixRef"), "spec.prefixRef is immutable")) - } - return allErrs -} - -func (ipAddressStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func validateIPAddress(a *ipam.IPAddress) field.ErrorList { - var allErrs field.ErrorList - specPath := field.NewPath("spec") - - var parsed net.IP - if a.Spec.Address == "" { - allErrs = append(allErrs, field.Required(specPath.Child("address"), "address is required")) - } else { - parsed = net.ParseIP(a.Spec.Address) - if parsed == nil { - allErrs = append(allErrs, field.Invalid(specPath.Child("address"), a.Spec.Address, "invalid IP address")) - } - } - if a.Spec.IPFamily != ipam.IPv4 && a.Spec.IPFamily != ipam.IPv6 { - allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), a.Spec.IPFamily, []string{string(ipam.IPv4), string(ipam.IPv6)})) - } - // Cross-check: an IPv4 address must be claimed as IPFamily=IPv4 and - // an IPv6-only address as IPFamily=IPv6. Without this check the - // allocator and consumers downstream would index by ipFamily and - // silently miss the address. net.ParseIP returns a 16-byte slice - // even for IPv4 addresses, so use To4() to discriminate. - if parsed != nil && a.Spec.IPFamily != "" { - isV4 := parsed.To4() != nil - switch { - case isV4 && a.Spec.IPFamily != ipam.IPv4: - allErrs = append(allErrs, field.Invalid(specPath.Child("ipFamily"), a.Spec.IPFamily, - fmt.Sprintf("address %q is IPv4 but ipFamily is %s", a.Spec.Address, a.Spec.IPFamily))) - case !isV4 && a.Spec.IPFamily != ipam.IPv6: - allErrs = append(allErrs, field.Invalid(specPath.Child("ipFamily"), a.Spec.IPFamily, - fmt.Sprintf("address %q is IPv6 but ipFamily is %s", a.Spec.Address, a.Spec.IPFamily))) - } - } - if a.Spec.PrefixRef.Name == "" { - allErrs = append(allErrs, field.Required(specPath.Child("prefixRef", "name"), "prefixRef.name is required")) - } - return allErrs -} - -func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { - a, ok := obj.(*ipam.IPAddress) - if !ok { - return nil, nil, fmt.Errorf("given object is not an IPAddress") - } - return a.Labels, SelectableFields(a), nil -} - -func SelectableFields(a *ipam.IPAddress) fields.Set { - objectMetaFields := generic.ObjectMetaFieldsSet(&a.ObjectMeta, true) - return generic.MergeFieldsSets(objectMetaFields, fields.Set{ - "spec.address": a.Spec.Address, - "spec.ipFamily": string(a.Spec.IPFamily), - "spec.prefixRef.name": a.Spec.PrefixRef.Name, - }) -} - -func MatchIPAddress(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { - return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetAttrs} -} - -func (ipAddressStatusStrategy) NamespaceScoped() bool { return true } - -func (ipAddressStatusStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPAddress) - o := old.(*ipam.IPAddress) - n.Spec = o.Spec - n.Labels = o.Labels - n.Annotations = o.Annotations -} - -func (ipAddressStatusStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { - return nil -} - -func (ipAddressStatusStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func (ipAddressStatusStrategy) AllowCreateOnUpdate() bool { return false } -func (ipAddressStatusStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipAddressStatusStrategy) Canonicalize(_ runtime.Object) {} - -func (ipAddressStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return map[fieldpath.APIVersion]*fieldpath.Set{ - "ipam.miloapis.com/v1alpha1": fieldpath.NewSet(fieldpath.MakePathOrDie("spec")), - } -} diff --git a/internal/registry/ipam/ipaddressclaim/storage.go b/internal/registry/ipam/ipaddressclaim/storage.go deleted file mode 100644 index 1d0a5ac..0000000 --- a/internal/registry/ipam/ipaddressclaim/storage.go +++ /dev/null @@ -1,417 +0,0 @@ -// Package ipaddressclaim provides REST storage for the IPAddressClaim -// resource. The exported AllocatingREST type wraps the standard storage -// with a synchronous Postgres-backed allocator that reserves a single -// host IP address from the parent IPPrefix pool. -package ipaddressclaim - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/jackc/pgx/v5/pgxpool" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/generic" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/apiserver/pkg/storage" - "k8s.io/klog/v2" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/internal/access" - "go.miloapis.com/ipam/internal/allocator" - "go.miloapis.com/ipam/internal/metrics" - "go.miloapis.com/ipam/internal/registry/ipam/registryerrors" - "go.miloapis.com/ipam/internal/tenant" - "go.miloapis.com/ipam/pkg/apis/ipam" - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" -) - -type IPAddressClaimStorage struct { - *genericregistry.Store -} - -type IPAddressClaimStatusStorage struct { - store *genericregistry.Store -} - -func (s *IPAddressClaimStatusStorage) New() runtime.Object { return &ipam.IPAddressClaim{} } -func (s *IPAddressClaimStatusStorage) Destroy() {} - -func (s *IPAddressClaimStatusStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return s.store.Get(ctx, name, options) -} - -func (s *IPAddressClaimStatusStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) -} - -func (s *IPAddressClaimStatusStorage) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return s.store.GetResetFields() -} - -func (s *IPAddressClaimStatusStorage) ConvertToTable(ctx context.Context, obj runtime.Object, opts runtime.Object) (*metav1.Table, error) { - return s.store.ConvertToTable(ctx, obj, opts) -} - -func newInnerStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*IPAddressClaimStorage, *IPAddressClaimStatusStorage, error) { - strategy := NewStrategy(scheme) - statusStrategy := NewStatusStrategy(scheme) - - store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &ipam.IPAddressClaim{} }, - NewListFunc: func() runtime.Object { return &ipam.IPAddressClaimList{} }, - DefaultQualifiedResource: v1alpha1.Resource("ipaddressclaims"), - SingularQualifiedResource: v1alpha1.Resource("ipaddressclaim"), - - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - - TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ipaddressclaims")), - } - - if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}); err != nil { - return nil, nil, err - } - - statusStore := *store - statusStore.UpdateStrategy = statusStrategy - statusStore.ResetFieldsStrategy = statusStrategy - - return &IPAddressClaimStorage{store}, &IPAddressClaimStatusStorage{store: &statusStore}, nil -} - -type AllocatingREST struct { - *IPAddressClaimStorage - allocator allocator.PrefixAllocator - db *pgxpool.Pool - strategy ipAddressClaimStrategy - poolChecker access.PoolAccessChecker - codec runtime.Codec -} - -// NewAllocatingStorage builds the IPAddressClaim REST storage with -// synchronous Postgres-backed allocation. poolChecker may be nil; when -// non-nil it authorises cross-project claims (prefixSelector.projectRef -// targeting another project) via SubjectAccessReview before allocation. -// When nil, cross-project allocation fails closed — the visibility=shared -// marker on the IPPrefixClass is intent-only and never sufficient on its -// own. Mirrors the IPPrefixClaim auth pattern (audit findings H1/H6, -// task #20). -func NewAllocatingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, alloc allocator.PrefixAllocator, db *pgxpool.Pool, codec runtime.Codec, poolChecker access.PoolAccessChecker) (*AllocatingREST, *IPAddressClaimStatusStorage, error) { - claimStore, statusStore, err := newInnerStorage(scheme, optsGetter) - if err != nil { - return nil, nil, err - } - return &AllocatingREST{ - IPAddressClaimStorage: claimStore, - allocator: alloc, - db: db, - strategy: NewStrategy(scheme), - poolChecker: poolChecker, - codec: codec, - }, statusStore, nil -} - -func (r *AllocatingREST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - claim, ok := obj.(*ipam.IPAddressClaim) - if !ok { - return nil, fmt.Errorf("expected *ipam.IPAddressClaim, got %T", obj) - } - // Extract tenant identity up front so the project / org labels are - // available to AllocationAttempts and the deferred AllocationDuration - // observation. project / org come from tenant.Identity helpers - // (iam.miloapis.com/parent-* extras); both are "" for platform-scoped - // requests, and org is "" today for project-scoped requests until Milo - // forwards the owning org alongside the project. - id := tenant.FromContext(ctx) - project := id.Project() - org := id.Org() - // ip_family is sourced from claim.Spec.IPFamily before any metric is - // recorded so AllocationAttempts, AllocationFailures, and the latency - // histogram all split identically. claim.Spec.IPFamily is set on every - // valid IPAddressClaim ("IPv4" or "IPv6"); pre-spec failures land in the - // empty-string family and are clearly distinguishable from the - // family-tagged successes. - ipFamily := string(claim.Spec.IPFamily) - metrics.AllocationAttempts.WithLabelValues("ipaddressclaim", ipFamily, project, org).Inc() - allocStart := time.Now() - result := "error" - defer func() { - metrics.ObserveAllocationDuration("ipaddressclaim", result, ipFamily, project, org, allocStart) - }() - - objectMeta, err := meta.Accessor(claim) - if err != nil { - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("get object metadata: %w", err) - } - rest.FillObjectMetaSystemFields(objectMeta) - - if err := rest.BeforeCreate(r.strategy, ctx, claim); err != nil { - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, err - } - if createValidation != nil { - if err := createValidation(ctx, claim.DeepCopyObject()); err != nil { - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, err - } - } - - if claim.Spec.PrefixRef == nil && claim.Spec.PrefixSelector == nil { - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, apierrors.NewBadRequest("synchronous allocation requires spec.prefixRef or spec.prefixSelector") - } - if claim.Spec.PrefixRef != nil && claim.Spec.PrefixSelector != nil { - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, apierrors.NewBadRequest("spec.prefixRef and spec.prefixSelector are mutually exclusive") - } - - if !id.IsPlatform() { - claim.Spec.OwnerRef = &ipam.ObjectRef{ - APIGroup: id.APIGroup, - Kind: id.Kind, - Name: id.Name, - } - } - - tx, err := r.db.Begin(ctx) - if err != nil { - metrics.RecordAllocationFailure("ipaddressclaim", "tx_error", ipFamily, project, org) - return nil, fmt.Errorf("begin allocation transaction: %w", err) - } - - // Resolve the target prefix pool. spec.prefixRef is a direct named - // lookup; spec.prefixSelector lists candidates and picks the first - // match (allocator.ResolvePrefixPool documents the strategy). Both - // paths support an optional cross-project ProjectRef pointing at a - // foreign project's pool; that branch sets isCrossProject so we can - // run the same SAR + visibility=shared gate as IPPrefixClaim before - // allocating (audit findings H1/H6 — task #20). - isCrossProject := false - var poolKey string - if claim.Spec.PrefixRef != nil { - isCrossProject = !id.IsPlatform() && - claim.Spec.PrefixRef.ProjectRef != nil && - claim.Spec.PrefixRef.ProjectRef.Name != id.Name - if isCrossProject { - poolKey = tenant.Identity{Name: claim.Spec.PrefixRef.ProjectRef.Name}.ResourceKey("ipprefixes", claim.Spec.PrefixRef.Name) - } else { - poolKey = id.ResourceKey("ipprefixes", claim.Spec.PrefixRef.Name) - } - } else { - ownerProject := id.Name - if claim.Spec.PrefixSelector.ProjectRef != nil { - ownerProject = claim.Spec.PrefixSelector.ProjectRef.Name - isCrossProject = !id.IsPlatform() && ownerProject != id.Name - } - resolved, rerr := allocator.ResolvePrefixPool(ctx, tx, claim.Spec.PrefixSelector.LabelSelector, ownerProject, string(claim.Spec.IPFamily)) - if rerr != nil { - _ = tx.Rollback(ctx) - if errors.Is(rerr, allocator.ErrPoolNotFound) { - metrics.RecordAllocationFailure("ipaddressclaim", "pool_not_found", ipFamily, project, org) - return nil, apierrors.NewBadRequest("no IPPrefix pool matches spec.prefixSelector") - } - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("resolve prefix pool: %w", rerr) - } - poolKey = resolved - } - claimKey := claimObjectKey(claim.Namespace, claim.Name) - - if isCrossProject { - if err := access.AuthorizeCrossProjectPrefix(ctx, tx, poolKey, r.poolChecker); err != nil { - _ = tx.Rollback(ctx) - if errors.Is(err, access.ErrCrossProjectDenied) { - // Mask the failure so the selector path can't be used to - // fingerprint another project's pools by trial labels — - // the response must be indistinguishable from "no pool - // matched the selector". Direct prefixRef lookups can - // return Forbidden because the caller already named the - // pool by hand, so revealing forbidden-vs-not-found - // reveals nothing new. - if claim.Spec.PrefixSelector != nil { - metrics.RecordAllocationFailure("ipaddressclaim", "pool_not_found", ipFamily, project, org) - return nil, apierrors.NewBadRequest("no IPPrefix pool matches spec.prefixSelector") - } - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, apierrors.NewForbidden( - v1alpha1.Resource("ipprefixes"), - poolKey, - fmt.Errorf("cross-project pool not accessible"), - ) - } - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, err - } - } - - addr, err := r.allocator.AllocateSingleAddress(ctx, tx, poolKey, string(claim.Spec.IPFamily), claimKey, id.Name) - if err != nil { - _ = tx.Rollback(ctx) - reason := allocationFailureReason(err) - metrics.RecordAllocationFailure("ipaddressclaim", reason, ipFamily, project, org) - if reason == "pool_exhausted" { - result = "exhausted" - } - return nil, mapAllocationError(err) - } - - claim.Status.Phase = ipam.ClaimBound - claim.Status.AllocatedIP = addr - - claimData, err := runtime.Encode(r.codec, claim) - if err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("encode claim: %w", err) - } - rv, err := r.allocator.InsertObject(ctx, tx, claimKey, "IPAddressClaim", claim.Namespace, claim.Name, claimData) - if err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipaddressclaim", "tx_error", ipFamily, project, org) - return nil, fmt.Errorf("persist claim: %w", err) - } - versioner := storage.APIObjectVersioner{} - if err := versioner.UpdateObject(claim, uint64(rv)); err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipaddressclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("set resource version: %w", err) - } - - if err := tx.Commit(ctx); err != nil { - metrics.RecordAllocationFailure("ipaddressclaim", "tx_error", ipFamily, project, org) - return nil, fmt.Errorf("commit allocation transaction: %w", err) - } - result = "success" - return claim, nil -} - -// allocationFailureReason maps an allocator error onto the canonical reason -// label used by ipam_allocation_failures_total. -func allocationFailureReason(err error) string { - switch { - case errors.Is(err, allocator.ErrPoolExhausted): - return "pool_exhausted" - case errors.Is(err, allocator.ErrPoolNotFound): - return "pool_not_found" - default: - return "tx_error" - } -} - -// Delete runs the claim teardown in two transactions so watchers can observe -// the intermediate phase=Releasing state before the object disappears. See -// the IPPrefixClaim Delete handler for the full rationale; this is the same -// pattern adapted to IPAddressClaim. -func (r *AllocatingREST) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - existing, err := r.Get(ctx, name, &metav1.GetOptions{}) - if err != nil { - return nil, false, err - } - claim, ok := existing.(*ipam.IPAddressClaim) - if !ok { - return nil, false, fmt.Errorf("expected *ipam.IPAddressClaim from Get, got %T", existing) - } - if deleteValidation != nil { - if err := deleteValidation(ctx, claim.DeepCopyObject()); err != nil { - return nil, false, err - } - } - claimKey := claimObjectKey(claim.Namespace, claim.Name) - - // TX1 — publish phase=Releasing. - releasing := claim.DeepCopy() - releasing.Status.Phase = ipam.ClaimReleasing - releasingData, err := runtime.Encode(r.codec, releasing) - if err != nil { - return nil, false, fmt.Errorf("encode releasing claim: %w", err) - } - tx1, err := r.db.Begin(ctx) - if err != nil { - return nil, false, fmt.Errorf("begin releasing transaction: %w", err) - } - rv, err := r.allocator.UpdateObject(ctx, tx1, claimKey, releasingData) - if err != nil { - _ = tx1.Rollback(ctx) - return nil, false, fmt.Errorf("publish releasing phase: %w", err) - } - versioner := storage.APIObjectVersioner{} - if err := versioner.UpdateObject(releasing, uint64(rv)); err != nil { - _ = tx1.Rollback(ctx) - return nil, false, fmt.Errorf("set releasing resource version: %w", err) - } - if err := tx1.Commit(ctx); err != nil { - return nil, false, fmt.Errorf("commit releasing transaction: %w", err) - } - klog.V(2).InfoS("claim entering Releasing phase", "claim", name) - - // TX2 — release the allocation and delete the object row, with retry. - var lastErr error - for attempt := 1; attempt <= deleteMaxAttempts; attempt++ { - lastErr = r.releaseAndDelete(ctx, claimKey) - if lastErr == nil { - break - } - klog.ErrorS(lastErr, "release-and-delete attempt failed", "claim", name, "attempt", attempt) - if attempt < deleteMaxAttempts { - time.Sleep(deleteRetryBackoff) - } - } - if lastErr != nil { - klog.ErrorS(lastErr, "claim stuck in Releasing after retries — manual intervention may be required", "claim", name, "attempts", deleteMaxAttempts) - return nil, false, fmt.Errorf("release allocation after %d attempts: %w", deleteMaxAttempts, lastErr) - } - - klog.V(2).InfoS("claim released and deleted", "claim", name) - metrics.RecordRelease("ipaddressclaim") - return releasing, true, nil -} - -// releaseAndDelete is a single attempt of TX2: release the allocation row(s) -// for claimKey and delete the object row, all inside one transaction. -func (r *AllocatingREST) releaseAndDelete(ctx context.Context, claimKey string) error { - tx, err := r.db.Begin(ctx) - if err != nil { - return fmt.Errorf("begin release transaction: %w", err) - } - if err := r.allocator.Release(ctx, tx, claimKey); err != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("release allocation: %w", err) - } - if _, err := r.allocator.DeleteObject(ctx, tx, claimKey); err != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("delete claim row: %w", err) - } - if err := tx.Commit(ctx); err != nil { - return fmt.Errorf("commit release transaction: %w", err) - } - return nil -} - -// deleteMaxAttempts and deleteRetryBackoff govern the TX2 retry loop. -const ( - deleteMaxAttempts = 3 - deleteRetryBackoff = 100 * time.Millisecond -) - -func claimObjectKey(namespace, name string) string { - return fmt.Sprintf("/ipam.miloapis.com/ipaddressclaims/%s/%s", namespace, name) -} - -func mapAllocationError(err error) error { - switch { - case errors.Is(err, allocator.ErrPoolExhausted): - return registryerrors.NewInsufficientStorage("address pool exhausted") - case errors.Is(err, allocator.ErrPoolNotFound): - return apierrors.NewBadRequest("address pool not found") - default: - return apierrors.NewInternalError(err) - } -} diff --git a/internal/registry/ipam/ipaddressclaim/strategy.go b/internal/registry/ipam/ipaddressclaim/strategy.go deleted file mode 100644 index 24daf8b..0000000 --- a/internal/registry/ipam/ipaddressclaim/strategy.go +++ /dev/null @@ -1,167 +0,0 @@ -package ipaddressclaim - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/storage" - "k8s.io/apiserver/pkg/storage/names" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/internal/fieldindex" - "go.miloapis.com/ipam/pkg/apis/ipam" -) - -// FieldIndexes are the SQL expression indexes that back IPAddressClaim field -// selectors declared in SelectableFields. Applied idempotently by SyncIndexes. -var FieldIndexes = []fieldindex.FieldIndex{ - { - IndexName: "idx_ipam_ipaddressclaim_ip_family", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) WHERE kind = 'IPAddressClaim'`, - }, - { - IndexName: "idx_ipam_ipaddressclaim_prefix_ref_name", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' -> 'prefixRef' ->> 'name')) WHERE kind = 'IPAddressClaim'`, - }, -} - -type ipAddressClaimStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -type ipAddressClaimStatusStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -func NewStrategy(typer runtime.ObjectTyper) ipAddressClaimStrategy { - return ipAddressClaimStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func NewStatusStrategy(typer runtime.ObjectTyper) ipAddressClaimStatusStrategy { - return ipAddressClaimStatusStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func (ipAddressClaimStrategy) NamespaceScoped() bool { return true } - -func (ipAddressClaimStrategy) PrepareForCreate(_ context.Context, obj runtime.Object) { - c := obj.(*ipam.IPAddressClaim) - c.Status = ipam.IPAddressClaimStatus{Phase: ipam.ClaimPending} -} - -func (ipAddressClaimStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPAddressClaim) - o := old.(*ipam.IPAddressClaim) - n.Status = o.Status -} - -func (ipAddressClaimStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { - return validateIPAddressClaim(obj.(*ipam.IPAddressClaim)) -} - -func (ipAddressClaimStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { - return nil -} - -func (ipAddressClaimStrategy) AllowCreateOnUpdate() bool { return false } -func (ipAddressClaimStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipAddressClaimStrategy) Canonicalize(_ runtime.Object) {} - -func (ipAddressClaimStrategy) ValidateUpdate(_ context.Context, obj, old runtime.Object) field.ErrorList { - n := obj.(*ipam.IPAddressClaim) - o := old.(*ipam.IPAddressClaim) - allErrs := validateIPAddressClaim(n) - if n.Spec.IPFamily != o.Spec.IPFamily { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ipFamily"), "ipFamily is immutable")) - } - if !equality.Semantic.DeepEqual(n.Spec.PrefixRef, o.Spec.PrefixRef) { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "prefixRef"), "prefixRef is immutable")) - } - if !equality.Semantic.DeepEqual(n.Spec.PrefixSelector, o.Spec.PrefixSelector) { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "prefixSelector"), "prefixSelector is immutable")) - } - return allErrs -} - -func (ipAddressClaimStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func validateIPAddressClaim(c *ipam.IPAddressClaim) field.ErrorList { - var allErrs field.ErrorList - specPath := field.NewPath("spec") - if c.Spec.IPFamily == "" { - allErrs = append(allErrs, field.Required(specPath.Child("ipFamily"), "ipFamily is required")) - } else if c.Spec.IPFamily != ipam.IPv4 && c.Spec.IPFamily != ipam.IPv6 { - allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), c.Spec.IPFamily, []string{string(ipam.IPv4), string(ipam.IPv6)})) - } - if c.Spec.PrefixRef == nil && c.Spec.PrefixSelector == nil { - allErrs = append(allErrs, field.Required(specPath, "exactly one of prefixRef or prefixSelector must be specified")) - } - if c.Spec.PrefixRef != nil && c.Spec.PrefixSelector != nil { - allErrs = append(allErrs, field.Forbidden(specPath, "prefixRef and prefixSelector are mutually exclusive")) - } - return allErrs -} - -func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { - c, ok := obj.(*ipam.IPAddressClaim) - if !ok { - return nil, nil, fmt.Errorf("given object is not an IPAddressClaim") - } - return c.Labels, SelectableFields(c), nil -} - -func SelectableFields(c *ipam.IPAddressClaim) fields.Set { - objectMetaFields := generic.ObjectMetaFieldsSet(&c.ObjectMeta, true) - // spec.prefixRef.name surfaces the targeted pool for filtered - // watches/lists (e.g. "show all address claims against this pool"). - // Empty for selector-based claims by design. - prefixRefName := "" - if c.Spec.PrefixRef != nil { - prefixRefName = c.Spec.PrefixRef.Name - } - return generic.MergeFieldsSets(objectMetaFields, fields.Set{ - "spec.ipFamily": string(c.Spec.IPFamily), - "spec.prefixRef.name": prefixRefName, - }) -} - -func MatchIPAddressClaim(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { - return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetAttrs} -} - -func (ipAddressClaimStatusStrategy) NamespaceScoped() bool { return true } - -func (ipAddressClaimStatusStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPAddressClaim) - o := old.(*ipam.IPAddressClaim) - n.Spec = o.Spec - n.Labels = o.Labels - n.Annotations = o.Annotations -} - -func (ipAddressClaimStatusStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { - return nil -} - -func (ipAddressClaimStatusStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func (ipAddressClaimStatusStrategy) AllowCreateOnUpdate() bool { return false } -func (ipAddressClaimStatusStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipAddressClaimStatusStrategy) Canonicalize(_ runtime.Object) {} - -func (ipAddressClaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return map[fieldpath.APIVersion]*fieldpath.Set{ - "ipam.miloapis.com/v1alpha1": fieldpath.NewSet(fieldpath.MakePathOrDie("spec")), - } -} diff --git a/internal/registry/ipam/ipallocation/storage.go b/internal/registry/ipam/ipallocation/storage.go new file mode 100644 index 0000000..6e4924b --- /dev/null +++ b/internal/registry/ipam/ipallocation/storage.go @@ -0,0 +1,86 @@ +// Package ipallocation provides REST storage for the namespaced IPAllocation +// resource. IPAllocation rows are system-created by the ipclaim handler in the +// same transaction as the claim that produced them; this storage exposes +// standard CRUD plus a /status subresource for read paths and selectors. +package ipallocation + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + + "go.miloapis.com/ipam/pkg/apis/ipam" + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" +) + +// IPAllocationStorage is the standard REST storage for IPAllocation. +type IPAllocationStorage struct { + *genericregistry.Store +} + +// IPAllocationStatusStorage exposes the /status subresource. +type IPAllocationStatusStorage struct { + store *genericregistry.Store +} + +func (s *IPAllocationStatusStorage) New() runtime.Object { return &ipam.IPAllocation{} } +func (s *IPAllocationStatusStorage) Destroy() {} + +func (s *IPAllocationStatusStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return s.store.Get(ctx, name, options) +} + +func (s *IPAllocationStatusStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) +} + +func (s *IPAllocationStatusStorage) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + return s.store.GetResetFields() +} + +func (s *IPAllocationStatusStorage) ConvertToTable(ctx context.Context, obj runtime.Object, opts runtime.Object) (*metav1.Table, error) { + return s.store.ConvertToTable(ctx, obj, opts) +} + +// NewAllocationStorage builds the IPAllocation REST storage and matching +// /status subresource. IPAllocation rows are always system-created (by the +// ipclaim Create handler) so this storage carries no allocator or db +// dependency — it is a thin CRUD shell on top of the generic registry store. +func NewAllocationStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*IPAllocationStorage, *IPAllocationStatusStorage, error) { + strategy := NewStrategy(scheme) + statusStrategy := NewStatusStrategy(scheme) + + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &ipam.IPAllocation{} }, + NewListFunc: func() runtime.Object { return &ipam.IPAllocationList{} }, + DefaultQualifiedResource: v1alpha1.Resource("ipallocations"), + SingularQualifiedResource: v1alpha1.Resource("ipallocation"), + + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ipallocations")), + } + + if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}); err != nil { + return nil, nil, err + } + + statusStore := *store + statusStore.UpdateStrategy = statusStrategy + statusStore.ResetFieldsStrategy = statusStrategy + + return &IPAllocationStorage{store}, &IPAllocationStatusStorage{store: &statusStore}, nil +} + +// Compile-time interface assertions. +var ( + _ rest.Storage = (*IPAllocationStorage)(nil) + _ rest.Storage = (*IPAllocationStatusStorage)(nil) +) diff --git a/internal/registry/ipam/ipallocation/strategy.go b/internal/registry/ipam/ipallocation/strategy.go new file mode 100644 index 0000000..46af0e6 --- /dev/null +++ b/internal/registry/ipam/ipallocation/strategy.go @@ -0,0 +1,168 @@ +package ipallocation + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/storage" + "k8s.io/apiserver/pkg/storage/names" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + + "go.miloapis.com/ipam/internal/fieldindex" + "go.miloapis.com/ipam/pkg/apis/ipam" +) + +// FieldIndexes are the SQL expression indexes backing IPAllocation field +// selectors declared in SelectableFields. Applied idempotently by SyncIndexes. +var FieldIndexes = []fieldindex.FieldIndex{ + { + IndexName: "idx_ipam_ipallocation_ip_family", + Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) WHERE kind = 'IPAllocation'`, + }, + { + IndexName: "idx_ipam_ipallocation_pool_ref_name", + Expression: `((ipam_data_to_jsonb(data) -> 'spec' -> 'poolRef' ->> 'name')) WHERE kind = 'IPAllocation'`, + }, +} + +type ipAllocationStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +type ipAllocationStatusStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +func NewStrategy(typer runtime.ObjectTyper) ipAllocationStrategy { + return ipAllocationStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} +} + +func NewStatusStrategy(typer runtime.ObjectTyper) ipAllocationStatusStrategy { + return ipAllocationStatusStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} +} + +func (ipAllocationStrategy) NamespaceScoped() bool { return true } + +func (ipAllocationStrategy) PrepareForCreate(_ context.Context, _ runtime.Object) {} + +func (ipAllocationStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { + n := obj.(*ipam.IPAllocation) + o := old.(*ipam.IPAllocation) + n.Status = o.Status +} + +func (ipAllocationStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { + return validateIPAllocation(obj.(*ipam.IPAllocation)) +} + +func (ipAllocationStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { + return nil +} +func (ipAllocationStrategy) AllowCreateOnUpdate() bool { return false } +func (ipAllocationStrategy) AllowUnconditionalUpdate() bool { return true } +func (ipAllocationStrategy) Canonicalize(_ runtime.Object) {} + +func (ipAllocationStrategy) ValidateUpdate(_ context.Context, obj, old runtime.Object) field.ErrorList { + n := obj.(*ipam.IPAllocation) + o := old.(*ipam.IPAllocation) + allErrs := validateIPAllocation(n) + specPath := field.NewPath("spec") + if n.Spec.IPFamily != o.Spec.IPFamily { + allErrs = append(allErrs, field.Forbidden(specPath.Child("ipFamily"), "spec.ipFamily is immutable")) + } + if n.Spec.PoolRef.Name != o.Spec.PoolRef.Name { + allErrs = append(allErrs, field.Forbidden(specPath.Child("poolRef"), "spec.poolRef is immutable")) + } + return allErrs +} + +func (ipAllocationStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { + return nil +} + +// ValidateDelete protects against direct user-initiated deletes of +// IPAllocation rows. The ipclaim Delete handler calls +// allocator.DeleteObject directly (bypassing strategy validation) when it +// tears down the claim, so this guard only fires for clients hitting the +// /ipallocations endpoint with `kubectl delete`. +func (ipAllocationStrategy) ValidateDelete(_ context.Context, obj runtime.Object) field.ErrorList { + a := obj.(*ipam.IPAllocation) + if a.Spec.PoolRef.Name != "" { + return field.ErrorList{field.Forbidden( + field.NewPath("spec", "poolRef"), + "IPAllocation is managed by its owning IPClaim; delete the claim instead", + )} + } + return nil +} + +func validateIPAllocation(a *ipam.IPAllocation) field.ErrorList { + var allErrs field.ErrorList + specPath := field.NewPath("spec") + + if a.Spec.PoolRef.Name == "" { + allErrs = append(allErrs, field.Required(specPath.Child("poolRef", "name"), "poolRef.name is required")) + } + if a.Spec.IPFamily == "" { + allErrs = append(allErrs, field.Required(specPath.Child("ipFamily"), "ipFamily is required")) + } else if a.Spec.IPFamily != ipam.IPv4 && a.Spec.IPFamily != ipam.IPv6 { + allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), a.Spec.IPFamily, []string{string(ipam.IPv4), string(ipam.IPv6)})) + } + return allErrs +} + +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + a, ok := obj.(*ipam.IPAllocation) + if !ok { + return nil, nil, fmt.Errorf("given object is not an IPAllocation") + } + return a.Labels, SelectableFields(a), nil +} + +func SelectableFields(a *ipam.IPAllocation) fields.Set { + objectMetaFields := generic.ObjectMetaFieldsSet(&a.ObjectMeta, true) + specific := fields.Set{ + "spec.ipFamily": string(a.Spec.IPFamily), + "spec.poolRef.name": a.Spec.PoolRef.Name, + } + return generic.MergeFieldsSets(objectMetaFields, specific) +} + +func Match(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { + return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetAttrs} +} + +func (ipAllocationStatusStrategy) NamespaceScoped() bool { return true } + +func (ipAllocationStatusStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { + n := obj.(*ipam.IPAllocation) + o := old.(*ipam.IPAllocation) + n.Spec = o.Spec + n.Labels = o.Labels + n.Annotations = o.Annotations +} + +func (ipAllocationStatusStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { + return nil +} + +func (ipAllocationStatusStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { + return nil +} + +func (ipAllocationStatusStrategy) AllowCreateOnUpdate() bool { return false } +func (ipAllocationStatusStrategy) AllowUnconditionalUpdate() bool { return true } +func (ipAllocationStatusStrategy) Canonicalize(_ runtime.Object) {} + +func (ipAllocationStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + return map[fieldpath.APIVersion]*fieldpath.Set{ + "ipam.miloapis.com/v1alpha1": fieldpath.NewSet(fieldpath.MakePathOrDie("spec")), + } +} diff --git a/internal/registry/ipam/ipclaim/storage.go b/internal/registry/ipam/ipclaim/storage.go new file mode 100644 index 0000000..521ebe0 --- /dev/null +++ b/internal/registry/ipam/ipclaim/storage.go @@ -0,0 +1,544 @@ +// Package ipclaim provides REST storage for the IPClaim resource. The +// exported AllocatingREST wraps the standard storage with a synchronous +// Postgres-backed allocator: Create reserves a free sub-prefix from the +// target IPPool inside a single transaction and atomically materialises the +// resulting IPAllocation row. Delete reverses both, releasing the allocation +// and removing the IPAllocation in the same transaction as the claim. +package ipclaim + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/storage" + "k8s.io/klog/v2" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + + "go.miloapis.com/ipam/internal/access" + "go.miloapis.com/ipam/internal/allocator" + "go.miloapis.com/ipam/internal/metrics" + "go.miloapis.com/ipam/internal/registry/ipam/registryerrors" + "go.miloapis.com/ipam/internal/tenant" + "go.miloapis.com/ipam/pkg/apis/ipam" + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" +) + +type IPClaimStorage struct { + *genericregistry.Store +} + +type IPClaimStatusStorage struct { + store *genericregistry.Store +} + +func (s *IPClaimStatusStorage) New() runtime.Object { return &ipam.IPClaim{} } +func (s *IPClaimStatusStorage) Destroy() {} + +func (s *IPClaimStatusStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return s.store.Get(ctx, name, options) +} + +func (s *IPClaimStatusStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) +} + +func (s *IPClaimStatusStorage) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + return s.store.GetResetFields() +} + +func (s *IPClaimStatusStorage) ConvertToTable(ctx context.Context, obj runtime.Object, opts runtime.Object) (*metav1.Table, error) { + return s.store.ConvertToTable(ctx, obj, opts) +} + +// newInnerStorage builds the underlying generic registry-backed REST storage +// for IPClaim. NewAllocatingStorage wraps the result to add synchronous +// allocation in the request path. +func newInnerStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*IPClaimStorage, *IPClaimStatusStorage, error) { + strategy := NewStrategy(scheme) + statusStrategy := NewStatusStrategy(scheme) + + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &ipam.IPClaim{} }, + NewListFunc: func() runtime.Object { return &ipam.IPClaimList{} }, + DefaultQualifiedResource: v1alpha1.Resource("ipclaims"), + SingularQualifiedResource: v1alpha1.Resource("ipclaim"), + + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ipclaims")), + } + + if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}); err != nil { + return nil, nil, err + } + + statusStore := *store + statusStore.UpdateStrategy = statusStrategy + statusStore.ResetFieldsStrategy = statusStrategy + + return &IPClaimStorage{store}, &IPClaimStatusStorage{store: &statusStore}, nil +} + +// AllocatingREST decorates the standard claim storage with a synchronous +// allocator. On Create it begins a Postgres transaction, asks the allocator +// to reserve a sub-prefix from the target IPPool, materialises an +// IPAllocation row, and returns the claim with status fully populated. On +// Delete it releases the allocation and removes the IPAllocation in the same +// transaction as the claim deletion. +type AllocatingREST struct { + *IPClaimStorage + allocator allocator.PrefixAllocator + db *pgxpool.Pool + strategy ipClaimStrategy + poolChecker access.PoolAccessChecker + codec runtime.Codec +} + +// NewAllocatingStorage builds the IPClaim REST storage with synchronous +// Postgres-backed allocation. db must be the same pool the allocator commits +// against; codec is used to serialise the synchronously-allocated claim and +// the generated IPAllocation into ipam_objects so subsequent GETs return +// fully-populated objects. poolChecker may be nil; when non-nil it +// authorises cross-project claims via SubjectAccessReview before allocation. +func NewAllocatingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, alloc allocator.PrefixAllocator, db *pgxpool.Pool, codec runtime.Codec, poolChecker access.PoolAccessChecker) (*AllocatingREST, *IPClaimStatusStorage, error) { + claimStore, statusStore, err := newInnerStorage(scheme, optsGetter) + if err != nil { + return nil, nil, err + } + return &AllocatingREST{ + IPClaimStorage: claimStore, + allocator: alloc, + db: db, + strategy: NewStrategy(scheme), + poolChecker: poolChecker, + codec: codec, + }, statusStore, nil +} + +// Create runs the standard create pipeline (system-metadata fill, strategy +// PrepareForCreate, validation), then drives the allocator inside a +// short-lived transaction. The transaction persists the claim row, the +// allocation row in ipam_cidr_allocations, and the IPAllocation API object +// together so the response body carries a CIDR that has already been +// reserved. +func (r *AllocatingREST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + claim, ok := obj.(*ipam.IPClaim) + if !ok { + return nil, fmt.Errorf("expected *ipam.IPClaim, got %T", obj) + } + + id := tenant.FromContext(ctx) + project := id.Project() + org := id.Org() + + ipFamily := string(claim.Spec.IPFamily) + metrics.AllocationAttempts.WithLabelValues("ipclaim", ipFamily, project, org).Inc() + allocStart := time.Now() + result := "error" + defer func() { + metrics.ObserveAllocationDuration("ipclaim", result, ipFamily, project, org, allocStart) + }() + + objectMeta, err := meta.Accessor(claim) + if err != nil { + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, fmt.Errorf("get object metadata: %w", err) + } + rest.FillObjectMetaSystemFields(objectMeta) + + if err := rest.BeforeCreate(r.strategy, ctx, claim); err != nil { + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, err + } + if createValidation != nil { + if err := createValidation(ctx, claim.DeepCopyObject()); err != nil { + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, err + } + } + + if claim.Spec.PoolRef == nil && claim.Spec.PoolSelector == nil { + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, apierrors.NewBadRequest("synchronous allocation requires spec.poolRef or spec.poolSelector") + } + if claim.Spec.PoolRef != nil && claim.Spec.PoolSelector != nil { + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, apierrors.NewBadRequest("spec.poolRef and spec.poolSelector are mutually exclusive") + } + + if !id.IsPlatform() { + // Overwrite client-supplied ownerRef — requestheader CA guarantees + // Extra authenticity, so the tenant identity is the source of truth. + claim.Spec.OwnerRef = &ipam.ObjectRef{ + APIGroup: id.APIGroup, + Kind: id.Kind, + Name: id.Name, + } + } + + tx, err := r.db.Begin(ctx) + if err != nil { + metrics.RecordAllocationFailure("ipclaim", "tx_error", ipFamily, project, org) + return nil, fmt.Errorf("begin allocation transaction: %w", err) + } + + // Resolve the target IPPool. spec.poolRef is a direct named lookup; + // spec.poolSelector lists candidate pools, filters by the supplied + // label selector, and picks the first match by storage key (see + // allocator.ResolveIPPool). IPPool is cluster-scoped, so the storage + // key always lives at the platform prefix regardless of the calling + // project's tenant identity. + isCrossProject := false + var poolKey, poolName string + if claim.Spec.PoolRef != nil { + poolName = claim.Spec.PoolRef.Name + isCrossProject = !id.IsPlatform() && + claim.Spec.PoolRef.ProjectRef != nil && + claim.Spec.PoolRef.ProjectRef.Name != id.Name + poolKey = poolStorageKey(poolName) + } else { + if claim.Spec.PoolSelector.ProjectRef != nil { + isCrossProject = !id.IsPlatform() && + claim.Spec.PoolSelector.ProjectRef.Name != id.Name + } + resolved, rerr := allocator.ResolveIPPool(ctx, tx, claim.Spec.PoolSelector.LabelSelector, "", string(claim.Spec.IPFamily)) + if rerr != nil { + _ = tx.Rollback(ctx) + if errors.Is(rerr, allocator.ErrPoolNotFound) { + metrics.RecordAllocationFailure("ipclaim", "pool_not_found", ipFamily, project, org) + return nil, apierrors.NewBadRequest("no IPPool matches spec.poolSelector") + } + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, fmt.Errorf("resolve IPPool: %w", rerr) + } + poolKey = resolved + poolName = poolKey[strings.LastIndex(poolKey, "/")+1:] + } + claimKey := claimObjectKey(claim.Namespace, claim.Name) + + if isCrossProject { + if err := r.authorizeCrossProject(ctx, tx, poolKey); err != nil { + _ = tx.Rollback(ctx) + if errors.Is(err, access.ErrCrossProjectDenied) { + // Selector-driven lookups must not distinguish "no pool + // matched the selector" from "a pool matched but you + // can't use it" — that distinction is a label/existence + // fingerprint into another project. Direct poolRef + // lookups can return Forbidden because the caller already + // named the pool by hand. + if claim.Spec.PoolSelector != nil { + metrics.RecordAllocationFailure("ipclaim", "pool_not_found", ipFamily, project, org) + return nil, apierrors.NewBadRequest("no IPPool matches spec.poolSelector") + } + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, apierrors.NewForbidden( + v1alpha1.Resource("ippools"), + poolKey, + fmt.Errorf("cross-project pool not accessible"), + ) + } + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, err + } + } + + cidr, err := r.allocator.AllocatePrefix(ctx, tx, poolKey, claim.Spec.PrefixLength, string(claim.Spec.IPFamily), claimKey, id.Name) + if err != nil { + _ = tx.Rollback(ctx) + reason := allocationFailureReason(err) + metrics.RecordAllocationFailure("ipclaim", reason, ipFamily, project, org) + if reason == "pool_exhausted" { + result = "exhausted" + } + return nil, mapAllocationError(err) + } + + // Build the IPAllocation object that records this binding. It lives in + // the claim's namespace; its name is a stable hash of the claim + // namespace/name so the Delete handler can recompute it deterministically. + allocationName := allocationNameFor(claim.Namespace, claim.Name) + allocationKey := allocationObjectKey(claim.Namespace, allocationName) + + alloc := &ipam.IPAllocation{ + ObjectMeta: metav1.ObjectMeta{ + Name: allocationName, + Namespace: claim.Namespace, + }, + Spec: ipam.IPAllocationSpec{ + IPFamily: claim.Spec.IPFamily, + PoolRef: ipam.LocalRef{Name: poolName}, + }, + Status: ipam.IPAllocationStatus{ + Phase: ipam.AllocationReady, + AllocatedCIDR: cidr, + }, + } + allocData, err := runtime.Encode(r.codec, alloc) + if err != nil { + _ = tx.Rollback(ctx) + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, fmt.Errorf("encode IPAllocation: %w", err) + } + if _, err := r.allocator.InsertObject(ctx, tx, allocationKey, "IPAllocation", claim.Namespace, allocationName, allocData); err != nil { + _ = tx.Rollback(ctx) + metrics.RecordAllocationFailure("ipclaim", "tx_error", ipFamily, project, org) + return nil, fmt.Errorf("persist IPAllocation: %w", err) + } + + // Populate the claim status with the allocated CIDR + reference back to + // the IPAllocation row that records it. Watchers see a single ADDED + // event with the terminal bound state. + claim.Status.Phase = ipam.ClaimBound + claim.Status.AllocatedCIDR = cidr + claim.Status.BoundAllocationRef = &ipam.LocalRef{Name: allocationName} + + claimData, err := runtime.Encode(r.codec, claim) + if err != nil { + _ = tx.Rollback(ctx) + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, fmt.Errorf("encode claim: %w", err) + } + rv, err := r.allocator.InsertObject(ctx, tx, claimKey, "IPClaim", claim.Namespace, claim.Name, claimData) + if err != nil { + _ = tx.Rollback(ctx) + metrics.RecordAllocationFailure("ipclaim", "tx_error", ipFamily, project, org) + return nil, fmt.Errorf("persist claim: %w", err) + } + versioner := storage.APIObjectVersioner{} + if err := versioner.UpdateObject(claim, uint64(rv)); err != nil { + _ = tx.Rollback(ctx) + metrics.RecordAllocationFailure("ipclaim", "internal", ipFamily, project, org) + return nil, fmt.Errorf("set resource version: %w", err) + } + + if err := tx.Commit(ctx); err != nil { + metrics.RecordAllocationFailure("ipclaim", "tx_error", ipFamily, project, org) + return nil, fmt.Errorf("commit allocation transaction: %w", err) + } + + result = "success" + return claim, nil +} + +// allocationFailureReason maps an allocator error onto the canonical reason +// label used by ipam_allocation_failures_total. +func allocationFailureReason(err error) string { + switch { + case errors.Is(err, allocator.ErrPoolExhausted): + return "pool_exhausted" + case errors.Is(err, allocator.ErrPoolNotFound): + return "pool_not_found" + default: + return "tx_error" + } +} + +// Delete runs the claim teardown in two transactions so watchers can observe +// the intermediate Releasing phase before the object disappears: +// +// TX1: UPDATE the claim row with status.phase=Releasing + MODIFIED changelog +// TX2: Release the allocation + DeleteObject(IPAllocation) + DeleteObject(claim) + DELETED changelogs +// +// TX2 is retried up to deleteMaxAttempts times with a short backoff so a +// transient PG hiccup does not strand the claim in Releasing forever. +func (r *AllocatingREST) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + existing, err := r.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + claim, ok := existing.(*ipam.IPClaim) + if !ok { + return nil, false, fmt.Errorf("expected *ipam.IPClaim from Get, got %T", existing) + } + if deleteValidation != nil { + if err := deleteValidation(ctx, claim.DeepCopyObject()); err != nil { + return nil, false, err + } + } + + claimKey := claimObjectKey(claim.Namespace, claim.Name) + + // TX1 — publish phase=Releasing. + releasing := claim.DeepCopy() + releasing.Status.Phase = ipam.ClaimReleasing + releasingData, err := runtime.Encode(r.codec, releasing) + if err != nil { + return nil, false, fmt.Errorf("encode releasing claim: %w", err) + } + tx1, err := r.db.Begin(ctx) + if err != nil { + return nil, false, fmt.Errorf("begin releasing transaction: %w", err) + } + rv, err := r.allocator.UpdateObject(ctx, tx1, claimKey, releasingData) + if err != nil { + _ = tx1.Rollback(ctx) + return nil, false, fmt.Errorf("publish releasing phase: %w", err) + } + versioner := storage.APIObjectVersioner{} + if err := versioner.UpdateObject(releasing, uint64(rv)); err != nil { + _ = tx1.Rollback(ctx) + return nil, false, fmt.Errorf("set releasing resource version: %w", err) + } + if err := tx1.Commit(ctx); err != nil { + return nil, false, fmt.Errorf("commit releasing transaction: %w", err) + } + klog.V(2).InfoS("claim entering Releasing phase", "claim", name) + + // TX2 — release the allocation, remove the IPAllocation row and the + // claim row in a single transaction. Retried on transient failures. + var lastErr error + for attempt := 1; attempt <= deleteMaxAttempts; attempt++ { + lastErr = r.releaseAndDelete(ctx, claim, claimKey) + if lastErr == nil { + break + } + klog.ErrorS(lastErr, "release-and-delete attempt failed", "claim", name, "attempt", attempt) + if attempt < deleteMaxAttempts { + time.Sleep(deleteRetryBackoff) + } + } + if lastErr != nil { + klog.ErrorS(lastErr, "claim stuck in Releasing after retries — manual intervention may be required", "claim", name, "attempts", deleteMaxAttempts) + return nil, false, fmt.Errorf("release allocation after %d attempts: %w", deleteMaxAttempts, lastErr) + } + + klog.V(2).InfoS("claim released and deleted", "claim", name) + metrics.RecordRelease("ipclaim") + return releasing, true, nil +} + +// DeleteCollection routes individual deletes through AllocatingREST.Delete +// so allocation rows are released when a namespace is bulk-terminated. The +// embedded Store's DeleteCollection would otherwise dispatch statically to +// Store.Delete and leak allocations. +func (r *AllocatingREST) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { + listObj, err := r.List(ctx, listOptions) + if err != nil { + return nil, fmt.Errorf("list claims for deletecollection: %w", err) + } + claimList, ok := listObj.(*ipam.IPClaimList) + if !ok { + return nil, fmt.Errorf("expected *ipam.IPClaimList from List, got %T", listObj) + } + + deletedList := &ipam.IPClaimList{} + var errs []error + for i := range claimList.Items { + deleted, _, err := r.Delete(ctx, claimList.Items[i].Name, deleteValidation, options.DeepCopy()) + if err != nil { + if !apierrors.IsNotFound(err) { + errs = append(errs, fmt.Errorf("delete claim %s: %w", claimList.Items[i].Name, err)) + } + continue + } + if c, ok := deleted.(*ipam.IPClaim); ok { + deletedList.Items = append(deletedList.Items, *c) + } + } + if len(errs) > 0 { + return deletedList, errors.Join(errs...) + } + return deletedList, nil +} + +// releaseAndDelete is a single attempt of TX2: release the allocation +// row(s) for claimKey, delete the IPAllocation row recorded on the claim, +// and delete the claim row — all inside one transaction. +func (r *AllocatingREST) releaseAndDelete(ctx context.Context, claim *ipam.IPClaim, claimKey string) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return fmt.Errorf("begin release transaction: %w", err) + } + if err := r.allocator.Release(ctx, tx, claimKey); err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("release allocation: %w", err) + } + if claim.Status.BoundAllocationRef != nil && claim.Status.BoundAllocationRef.Name != "" { + allocationKey := allocationObjectKey(claim.Namespace, claim.Status.BoundAllocationRef.Name) + if _, err := r.allocator.DeleteObject(ctx, tx, allocationKey); err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("delete IPAllocation row: %w", err) + } + } + if _, err := r.allocator.DeleteObject(ctx, tx, claimKey); err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("delete claim row: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("commit release transaction: %w", err) + } + return nil +} + +const ( + deleteMaxAttempts = 3 + deleteRetryBackoff = 100 * time.Millisecond +) + +func claimObjectKey(namespace, name string) string { + return fmt.Sprintf("/ipam.miloapis.com/ipclaims/%s/%s", namespace, name) +} + +// allocationObjectKey is the storage key for an IPAllocation. IPAllocation +// is namespace-scoped, so the key carries the namespace segment. +func allocationObjectKey(namespace, name string) string { + return fmt.Sprintf("/ipam.miloapis.com/ipallocations/%s/%s", namespace, name) +} + +// poolStorageKey is the storage key for a cluster-scoped IPPool — matches +// the key shape ippool/storage.go writes against. +func poolStorageKey(name string) string { + return fmt.Sprintf("/ipam.miloapis.com/ippools/%s", name) +} + +// allocationNameFor generates a stable, collision-resistant name for the +// IPAllocation produced by a given claim, using a truncated SHA-256 hash of +// the claim's namespace/name. The "alloc-" prefix makes system-generated +// names obvious and lets operators distinguish them at a glance. +func allocationNameFor(namespace, name string) string { + h := sha256.Sum256([]byte(namespace + "/" + name)) + return "alloc-" + hex.EncodeToString(h[:8]) +} + +// authorizeCrossProject delegates to the shared cross-project gate in +// internal/access. +func (r *AllocatingREST) authorizeCrossProject(ctx context.Context, tx pgx.Tx, poolKey string) error { + return access.AuthorizeCrossProjectPrefix(ctx, tx, poolKey, r.poolChecker) +} + +func mapAllocationError(err error) error { + switch { + case errors.Is(err, allocator.ErrPoolExhausted): + return registryerrors.NewInsufficientStorage("IPPool exhausted") + case errors.Is(err, allocator.ErrPoolNotFound): + return apierrors.NewBadRequest("IPPool not found") + default: + return apierrors.NewInternalError(err) + } +} + +// Compile-time interface assertions. +var ( + _ rest.Storage = (*AllocatingREST)(nil) + _ rest.Creater = (*AllocatingREST)(nil) + _ rest.GracefulDeleter = (*AllocatingREST)(nil) + _ rest.CollectionDeleter = (*AllocatingREST)(nil) + _ rest.Storage = (*IPClaimStatusStorage)(nil) +) diff --git a/internal/registry/ipam/ipclaim/strategy.go b/internal/registry/ipam/ipclaim/strategy.go new file mode 100644 index 0000000..5c5ab1d --- /dev/null +++ b/internal/registry/ipam/ipclaim/strategy.go @@ -0,0 +1,183 @@ +package ipclaim + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/storage" + "k8s.io/apiserver/pkg/storage/names" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + + "go.miloapis.com/ipam/internal/fieldindex" + "go.miloapis.com/ipam/pkg/apis/ipam" +) + +// FieldIndexes are the SQL expression indexes backing IPClaim field selectors +// declared in SelectableFields. Applied idempotently by SyncIndexes. +var FieldIndexes = []fieldindex.FieldIndex{ + { + IndexName: "idx_ipam_ipclaim_ip_family", + Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) WHERE kind = 'IPClaim'`, + }, + { + IndexName: "idx_ipam_ipclaim_pool_ref_name", + Expression: `((ipam_data_to_jsonb(data) -> 'spec' -> 'poolRef' ->> 'name')) WHERE kind = 'IPClaim'`, + }, +} + +type ipClaimStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +type ipClaimStatusStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +func NewStrategy(typer runtime.ObjectTyper) ipClaimStrategy { + return ipClaimStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} +} + +func NewStatusStrategy(typer runtime.ObjectTyper) ipClaimStatusStrategy { + return ipClaimStatusStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} +} + +func (ipClaimStrategy) NamespaceScoped() bool { return true } + +func (ipClaimStrategy) PrepareForCreate(_ context.Context, obj runtime.Object) { + c := obj.(*ipam.IPClaim) + c.Status = ipam.IPClaimStatus{Phase: ipam.ClaimPending} +} + +func (ipClaimStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { + n := obj.(*ipam.IPClaim) + o := old.(*ipam.IPClaim) + n.Status = o.Status +} + +func (ipClaimStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { + return validateIPClaim(obj.(*ipam.IPClaim)) +} + +func (ipClaimStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { + return nil +} + +func (ipClaimStrategy) AllowCreateOnUpdate() bool { return false } +func (ipClaimStrategy) AllowUnconditionalUpdate() bool { return true } +func (ipClaimStrategy) Canonicalize(_ runtime.Object) {} + +func (ipClaimStrategy) ValidateUpdate(_ context.Context, obj, old runtime.Object) field.ErrorList { + n := obj.(*ipam.IPClaim) + o := old.(*ipam.IPClaim) + allErrs := validateIPClaim(n) + + if n.Spec.IPFamily != o.Spec.IPFamily { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ipFamily"), "ipFamily is immutable")) + } + if n.Spec.PrefixLength != o.Spec.PrefixLength { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "prefixLength"), "prefixLength is immutable")) + } + if !equality.Semantic.DeepEqual(n.Spec.PoolRef, o.Spec.PoolRef) { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "poolRef"), "poolRef is immutable")) + } + if !equality.Semantic.DeepEqual(n.Spec.PoolSelector, o.Spec.PoolSelector) { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "poolSelector"), "poolSelector is immutable")) + } + return allErrs +} + +func (ipClaimStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { + return nil +} + +func validateIPClaim(c *ipam.IPClaim) field.ErrorList { + var allErrs field.ErrorList + specPath := field.NewPath("spec") + + if c.Spec.IPFamily == "" { + allErrs = append(allErrs, field.Required(specPath.Child("ipFamily"), "ipFamily is required")) + } else if c.Spec.IPFamily != ipam.IPv4 && c.Spec.IPFamily != ipam.IPv6 { + allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), c.Spec.IPFamily, []string{string(ipam.IPv4), string(ipam.IPv6)})) + } + if c.Spec.PrefixLength <= 0 { + allErrs = append(allErrs, field.Invalid(specPath.Child("prefixLength"), c.Spec.PrefixLength, "prefixLength must be greater than 0")) + } + maxLen := 32 + if c.Spec.IPFamily == ipam.IPv6 { + maxLen = 128 + } + if c.Spec.PrefixLength > maxLen { + allErrs = append(allErrs, field.Invalid(specPath.Child("prefixLength"), c.Spec.PrefixLength, fmt.Sprintf("prefixLength must not exceed %d for %s", maxLen, c.Spec.IPFamily))) + } + if c.Spec.PoolRef == nil && c.Spec.PoolSelector == nil { + allErrs = append(allErrs, field.Required(specPath, "exactly one of poolRef or poolSelector must be specified")) + } + if c.Spec.PoolRef != nil && c.Spec.PoolSelector != nil { + allErrs = append(allErrs, field.Forbidden(specPath, "poolRef and poolSelector are mutually exclusive")) + } + return allErrs +} + +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + c, ok := obj.(*ipam.IPClaim) + if !ok { + return nil, nil, fmt.Errorf("given object is not an IPClaim") + } + return c.Labels, SelectableFields(c), nil +} + +func SelectableFields(c *ipam.IPClaim) fields.Set { + objectMetaFields := generic.ObjectMetaFieldsSet(&c.ObjectMeta, true) + // spec.poolRef.name lets clients filter watches/lists by the targeted + // pool. Empty when the claim used a poolSelector instead, which is the + // right behaviour (no fixed pool to filter by). + poolRefName := "" + if c.Spec.PoolRef != nil { + poolRefName = c.Spec.PoolRef.Name + } + specific := fields.Set{ + "spec.ipFamily": string(c.Spec.IPFamily), + "spec.poolRef.name": poolRefName, + } + return generic.MergeFieldsSets(objectMetaFields, specific) +} + +func MatchIPClaim(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { + return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetAttrs} +} + +func (ipClaimStatusStrategy) NamespaceScoped() bool { return true } + +func (ipClaimStatusStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { + n := obj.(*ipam.IPClaim) + o := old.(*ipam.IPClaim) + n.Spec = o.Spec + n.Labels = o.Labels + n.Annotations = o.Annotations +} + +func (ipClaimStatusStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { + return nil +} + +func (ipClaimStatusStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { + return nil +} + +func (ipClaimStatusStrategy) AllowCreateOnUpdate() bool { return false } +func (ipClaimStatusStrategy) AllowUnconditionalUpdate() bool { return true } +func (ipClaimStatusStrategy) Canonicalize(_ runtime.Object) {} + +func (ipClaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + return map[fieldpath.APIVersion]*fieldpath.Set{ + "ipam.miloapis.com/v1alpha1": fieldpath.NewSet(fieldpath.MakePathOrDie("spec")), + } +} diff --git a/internal/registry/ipam/ippool/storage.go b/internal/registry/ipam/ippool/storage.go new file mode 100644 index 0000000..026c9bd --- /dev/null +++ b/internal/registry/ipam/ippool/storage.go @@ -0,0 +1,293 @@ +package ippool + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/jackc/pgx/v5/pgxpool" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/storage" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + + "go.miloapis.com/ipam/internal/allocation" + "go.miloapis.com/ipam/internal/allocator" + "go.miloapis.com/ipam/internal/registry/ipam/registryerrors" + "go.miloapis.com/ipam/pkg/apis/ipam" + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" +) + +// IPPoolStatusStorage implements the /status subresource. The standard +// generic registry update path is reused; the only difference vs the main +// store is that the status strategy resets spec fields on update. +type IPPoolStatusStorage struct { + store *genericregistry.Store +} + +func (s *IPPoolStatusStorage) New() runtime.Object { return &ipam.IPPool{} } +func (s *IPPoolStatusStorage) Destroy() {} + +func (s *IPPoolStatusStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return s.store.Get(ctx, name, options) +} + +func (s *IPPoolStatusStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) +} + +func (s *IPPoolStatusStorage) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + return s.store.GetResetFields() +} + +func (s *IPPoolStatusStorage) ConvertToTable(ctx context.Context, obj runtime.Object, opts runtime.Object) (*metav1.Table, error) { + return s.store.ConvertToTable(ctx, obj, opts) +} + +// AllocatingIPPoolREST is the registered storage for IPPool. The embedded +// *genericregistry.Store handles root-pool CRUD and list/watch unchanged; +// the Create override only diverts when ParentPoolRef is set, in which case +// it runs a single allocation transaction against the parent pool. Delete +// rejects any pool that still has rows in ipam_cidr_allocations so callers +// see a deterministic 409. +type AllocatingIPPoolREST struct { + *genericregistry.Store + allocator allocator.PrefixAllocator + db *pgxpool.Pool + strategy ipPoolStrategy + codec runtime.Codec +} + +// NewIPPoolStorage builds the AllocatingIPPoolREST and the matching +// /status subresource storage. alloc + db are required — synchronous child +// allocation has no usable fallback. codec serialises the in-memory IPPool +// into the wire form persisted in ipam_objects, matching what subsequent +// GETs decode. +func NewIPPoolStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, alloc allocator.PrefixAllocator, db *pgxpool.Pool, codec runtime.Codec) (*AllocatingIPPoolREST, *IPPoolStatusStorage, error) { + strategy := NewStrategy(scheme) + statusStrategy := NewStatusStrategy(scheme) + + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &ipam.IPPool{} }, + NewListFunc: func() runtime.Object { return &ipam.IPPoolList{} }, + DefaultQualifiedResource: v1alpha1.Resource("ippools"), + SingularQualifiedResource: v1alpha1.Resource("ippool"), + + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ippools")), + } + + if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}); err != nil { + return nil, nil, err + } + + statusStore := *store + statusStore.UpdateStrategy = statusStrategy + statusStore.ResetFieldsStrategy = statusStrategy + + return &AllocatingIPPoolREST{ + Store: store, + allocator: alloc, + db: db, + strategy: strategy, + codec: codec, + }, &IPPoolStatusStorage{store: &statusStore}, nil +} + +// Create routes root pools through the embedded Store (no allocation +// required) and child pools through a single allocation transaction that +// reserves a sub-prefix from the named parent pool, populates the new pool's +// Status, and inserts the object row atomically. +func (r *AllocatingIPPoolREST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + pool, ok := obj.(*ipam.IPPool) + if !ok { + return nil, fmt.Errorf("expected *ipam.IPPool, got %T", obj) + } + + if pool.Spec.ParentPoolRef == nil { + // Root pool — strategy.PrepareForCreate already populated status. + return r.Store.Create(ctx, obj, createValidation, options) + } + + objectMeta, err := meta.Accessor(pool) + if err != nil { + return nil, fmt.Errorf("get object metadata: %w", err) + } + rest.FillObjectMetaSystemFields(objectMeta) + + if err := rest.BeforeCreate(r.strategy, ctx, pool); err != nil { + return nil, err + } + if createValidation != nil { + if err := createValidation(ctx, pool.DeepCopyObject()); err != nil { + return nil, err + } + } + + parentName := pool.Spec.ParentPoolRef.Name + parentKey := poolStorageKey(parentName) + childKey := poolStorageKey(pool.Name) + + // Resolve the parent pool's IPFamily before entering the transaction so + // the explicit value can be passed to AllocatePrefix. IPFamily is + // immutable, so reading it outside the transaction is safe. + parentObj, err := r.Get(ctx, parentName, &metav1.GetOptions{}) + if err != nil { + return nil, apierrors.NewBadRequest("parent IPPool not found") + } + parentPool, ok := parentObj.(*ipam.IPPool) + if !ok { + return nil, fmt.Errorf("unexpected parent pool type %T", parentObj) + } + ipFamily := string(parentPool.Spec.IPFamily) + + tx, err := r.db.Begin(ctx) + if err != nil { + return nil, fmt.Errorf("begin child-pool allocation transaction: %w", err) + } + + cidr, err := r.allocator.AllocatePrefix(ctx, tx, parentKey, pool.Spec.PrefixLength, ipFamily, childKey, "") + if err != nil { + _ = tx.Rollback(ctx) + return nil, mapAllocationError(err) + } + + pool.Status.AllocatedCIDR = cidr + pool.Status.Phase = ipam.PoolReady + pool.Status.Conditions = []metav1.Condition{{ + Type: "Allocated", + Status: metav1.ConditionTrue, + Reason: "AllocationSucceeded", + Message: fmt.Sprintf("CIDR %s allocated from %s", cidr, parentName), + LastTransitionTime: metav1.Now(), + }} + if _, ipnet, perr := net.ParseCIDR(cidr); perr == nil { + total := allocation.CountAddresses(*ipnet) + pool.Status.Capacity = ipam.PoolCapacity{Total: total, Available: total} + } else { + pool.Status.Capacity = ipam.PoolCapacity{} + } + + data, err := runtime.Encode(r.codec, pool) + if err != nil { + _ = tx.Rollback(ctx) + return nil, fmt.Errorf("encode pool: %w", err) + } + rv, err := r.allocator.InsertObject(ctx, tx, childKey, "IPPool", "", pool.Name, data) + if err != nil { + _ = tx.Rollback(ctx) + return nil, fmt.Errorf("persist child pool: %w", err) + } + versioner := storage.APIObjectVersioner{} + if err := versioner.UpdateObject(pool, uint64(rv)); err != nil { + _ = tx.Rollback(ctx) + return nil, fmt.Errorf("set resource version: %w", err) + } + + if err := tx.Commit(ctx); err != nil { + return nil, fmt.Errorf("commit child-pool allocation transaction: %w", err) + } + + return pool, nil +} + +// Delete rejects any pool — root or child — that still has allocations +// recorded in ipam_cidr_allocations. For child pools with zero +// allocations the row in ipam_cidr_allocations representing the child's +// own reservation against its parent must also be released, in the same +// transaction as the object delete. +func (r *AllocatingIPPoolREST) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + existing, err := r.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + pool, ok := existing.(*ipam.IPPool) + if !ok { + return nil, false, fmt.Errorf("expected *ipam.IPPool from Get, got %T", existing) + } + if deleteValidation != nil { + if err := deleteValidation(ctx, pool.DeepCopyObject()); err != nil { + return nil, false, err + } + } + + poolKey := poolStorageKey(name) + var count int + if err := r.db.QueryRow(ctx, + `SELECT COUNT(*) FROM ipam_cidr_allocations WHERE pool_key = $1`, + poolKey, + ).Scan(&count); err != nil { + return nil, false, fmt.Errorf("count active allocations for %q: %w", name, err) + } + if count > 0 { + return nil, false, apierrors.NewConflict( + schema.GroupResource{Group: v1alpha1.GroupName, Resource: "ippools"}, + name, + fmt.Errorf("cannot delete IPPool with %d active allocation(s); release all claims and child pools first", count), + ) + } + + if pool.Spec.ParentPoolRef == nil { + // Root pool with zero allocations — delegate to the standard delete. + return r.Store.Delete(ctx, name, deleteValidation, options) + } + + // Child pool — release its own reservation against the parent and + // delete the object row in a single transaction. + tx, err := r.db.Begin(ctx) + if err != nil { + return nil, false, fmt.Errorf("begin child-pool delete transaction: %w", err) + } + if err := r.allocator.Release(ctx, tx, poolKey); err != nil { + _ = tx.Rollback(ctx) + return nil, false, fmt.Errorf("release child-pool allocation: %w", err) + } + if _, err := r.allocator.DeleteObject(ctx, tx, poolKey); err != nil { + _ = tx.Rollback(ctx) + return nil, false, fmt.Errorf("delete child-pool row: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return nil, false, fmt.Errorf("commit child-pool delete transaction: %w", err) + } + + return pool, true, nil +} + +// poolStorageKey is the canonical ipam_objects key for a cluster-scoped +// IPPool. Matches the key shape used by allocator.AllocatePrefix and the +// FOR UPDATE lock on the pool row. +func poolStorageKey(name string) string { + return fmt.Sprintf("/ipam.miloapis.com/ippools/%s", name) +} + +// mapAllocationError translates allocator sentinel errors into the matching +// HTTP-shaped registry errors. Pool exhaustion is HTTP 507; unknown pool is +// a client error (the named parent does not exist). +func mapAllocationError(err error) error { + switch { + case errors.Is(err, allocator.ErrPoolExhausted): + return registryerrors.NewInsufficientStorage("parent pool exhausted") + case errors.Is(err, allocator.ErrPoolNotFound): + return apierrors.NewBadRequest("parent IPPool not found") + default: + return apierrors.NewInternalError(err) + } +} + +// Compile-time interface assertions to catch storage contract drift. +var ( + _ rest.Storage = (*AllocatingIPPoolREST)(nil) + _ rest.Creater = (*AllocatingIPPoolREST)(nil) + _ rest.GracefulDeleter = (*AllocatingIPPoolREST)(nil) + _ rest.Storage = (*IPPoolStatusStorage)(nil) +) diff --git a/internal/registry/ipam/ippool/strategy.go b/internal/registry/ipam/ippool/strategy.go new file mode 100644 index 0000000..1b5ec94 --- /dev/null +++ b/internal/registry/ipam/ippool/strategy.go @@ -0,0 +1,278 @@ +// Package ippool provides REST storage for the cluster-scoped IPPool +// resource. Root pools are persisted directly by the underlying store; child +// pools (pools whose CIDR is sub-allocated out of a parent IPPool) are created +// synchronously through the AllocatingIPPoolREST wrapper so the response +// carries the assigned status.allocatedCIDR. +package ippool + +import ( + "context" + "fmt" + "net" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/storage" + "k8s.io/apiserver/pkg/storage/names" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + + "go.miloapis.com/ipam/internal/allocation" + "go.miloapis.com/ipam/internal/fieldindex" + "go.miloapis.com/ipam/pkg/apis/ipam" +) + +// FieldIndexes are the SQL expression indexes backing IPPool field selectors. +// Applied idempotently by SyncIndexes. +var FieldIndexes = []fieldindex.FieldIndex{ + { + IndexName: "idx_ipam_ippool_ip_family", + Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) WHERE kind = 'IPPool'`, + }, + { + IndexName: "idx_ipam_ippool_parent_pool_ref_name", + Expression: `((ipam_data_to_jsonb(data) -> 'spec' -> 'parentPoolRef' ->> 'name')) WHERE kind = 'IPPool'`, + }, +} + +type ipPoolStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +type ipPoolStatusStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +func NewStrategy(typer runtime.ObjectTyper) ipPoolStrategy { + return ipPoolStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} +} + +func NewStatusStrategy(typer runtime.ObjectTyper) ipPoolStatusStrategy { + return ipPoolStatusStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} +} + +func (ipPoolStrategy) NamespaceScoped() bool { return false } + +// PrepareForCreate seeds Status based on whether this is a root pool +// (CIDR + IPFamily on the spec) or a child pool (parentPoolRef set). +// Root pools become Ready immediately — the apiserver allocates +// synchronously, so there is no controller that would later transition them +// from Pending. Child pools stay Pending until the Create handler runs the +// allocation transaction and overwrites Status with the assigned CIDR. +func (ipPoolStrategy) PrepareForCreate(_ context.Context, obj runtime.Object) { + p := obj.(*ipam.IPPool) + if p.Spec.Allocation.Strategy == "" { + p.Spec.Allocation.Strategy = ipam.FirstFit + } + + if p.Spec.ParentPoolRef != nil { + // Child pool — Create handler populates status after allocation. + p.Status = ipam.IPPoolStatus{Phase: ipam.PoolPending} + return + } + + // Root pool — compute canonical CIDR + total capacity now. + p.Status = ipam.IPPoolStatus{Phase: ipam.PoolPending} + if p.Spec.CIDR == "" { + return + } + _, ipnet, err := net.ParseCIDR(p.Spec.CIDR) + if err != nil { + return + } + p.Status.AllocatedCIDR = ipnet.String() + // Use CountAddresses so the initial Total uses the same unit as the + // post-allocation persistPoolCapacity refresh; seed Available so callers + // can observe "available decreased" after the first allocation. + total := allocation.CountAddresses(*ipnet) + p.Status.Capacity = ipam.PoolCapacity{Total: total, Available: total} + p.Status.Phase = ipam.PoolReady + p.Status.Conditions = []metav1.Condition{{ + Type: "Allocated", + Status: metav1.ConditionTrue, + Reason: "AllocationSucceeded", + Message: "IPPool is ready for allocation", + LastTransitionTime: metav1.Now(), + }} +} + +func (ipPoolStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { + n := obj.(*ipam.IPPool) + o := old.(*ipam.IPPool) + n.Status = o.Status +} + +func (ipPoolStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { + return validateIPPool(obj.(*ipam.IPPool)) +} + +func (ipPoolStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { return nil } +func (ipPoolStrategy) AllowCreateOnUpdate() bool { return false } +func (ipPoolStrategy) AllowUnconditionalUpdate() bool { return true } +func (ipPoolStrategy) Canonicalize(_ runtime.Object) {} + +func (ipPoolStrategy) ValidateUpdate(_ context.Context, obj, old runtime.Object) field.ErrorList { + n := obj.(*ipam.IPPool) + o := old.(*ipam.IPPool) + allErrs := validateIPPool(n) + specPath := field.NewPath("spec") + if n.Spec.CIDR != o.Spec.CIDR { + allErrs = append(allErrs, field.Forbidden(specPath.Child("cidr"), "spec.cidr is immutable")) + } + if n.Spec.IPFamily != o.Spec.IPFamily { + allErrs = append(allErrs, field.Forbidden(specPath.Child("ipFamily"), "spec.ipFamily is immutable")) + } + if !localRefEqual(n.Spec.ParentPoolRef, o.Spec.ParentPoolRef) { + allErrs = append(allErrs, field.Forbidden(specPath.Child("parentPoolRef"), "spec.parentPoolRef is immutable")) + } + if n.Spec.PrefixLength != o.Spec.PrefixLength { + allErrs = append(allErrs, field.Forbidden(specPath.Child("prefixLength"), "spec.prefixLength is immutable")) + } + return allErrs +} + +func (ipPoolStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { + return nil +} + +func validateIPPool(p *ipam.IPPool) field.ErrorList { + var allErrs field.ErrorList + specPath := field.NewPath("spec") + + isChild := p.Spec.ParentPoolRef != nil + hasChildLen := p.Spec.PrefixLength != 0 + hasRootCIDR := p.Spec.CIDR != "" + hasRootFamily := p.Spec.IPFamily != "" + + switch { + case isChild: + // Child pool — root fields must be absent, child fields required. + if hasRootCIDR { + allErrs = append(allErrs, field.Forbidden(specPath.Child("cidr"), + "spec.cidr must not be set on a child pool (computed from parent allocation)")) + } + if hasRootFamily { + allErrs = append(allErrs, field.Forbidden(specPath.Child("ipFamily"), + "spec.ipFamily must not be set on a child pool (inherited from parent)")) + } + if p.Spec.ParentPoolRef.Name == "" { + allErrs = append(allErrs, field.Required(specPath.Child("parentPoolRef", "name"), + "parentPoolRef.name is required for child pools")) + } + if !hasChildLen { + allErrs = append(allErrs, field.Required(specPath.Child("prefixLength"), + "prefixLength is required for child pools")) + } else if p.Spec.PrefixLength < 1 || p.Spec.PrefixLength > 128 { + allErrs = append(allErrs, field.Invalid(specPath.Child("prefixLength"), p.Spec.PrefixLength, + "prefixLength must be in [1, 128]")) + } + default: + // Root pool — child fields must be absent, root fields required. + if hasChildLen { + allErrs = append(allErrs, field.Forbidden(specPath.Child("prefixLength"), + "spec.prefixLength must not be set without spec.parentPoolRef")) + } + if !hasRootCIDR { + allErrs = append(allErrs, field.Required(specPath.Child("cidr"), + "cidr is required for root pools")) + } else if _, _, err := net.ParseCIDR(p.Spec.CIDR); err != nil { + allErrs = append(allErrs, field.Invalid(specPath.Child("cidr"), p.Spec.CIDR, + fmt.Sprintf("invalid CIDR: %v", err))) + } + if !hasRootFamily { + allErrs = append(allErrs, field.Required(specPath.Child("ipFamily"), + "ipFamily is required for root pools")) + } else if p.Spec.IPFamily != ipam.IPv4 && p.Spec.IPFamily != ipam.IPv6 { + allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), p.Spec.IPFamily, + []string{string(ipam.IPv4), string(ipam.IPv6)})) + } + } + + if p.Spec.Allocation.MinPrefixLength > 0 && p.Spec.Allocation.MaxPrefixLength > 0 && + p.Spec.Allocation.MinPrefixLength > p.Spec.Allocation.MaxPrefixLength { + allErrs = append(allErrs, field.Invalid( + specPath.Child("allocation"), p.Spec.Allocation, + "minPrefixLength must be <= maxPrefixLength", + )) + } + + switch p.Spec.Visibility { + case "", "platform", "consumer", "shared": + // ok + default: + allErrs = append(allErrs, field.NotSupported(specPath.Child("visibility"), p.Spec.Visibility, + []string{"", "platform", "consumer", "shared"})) + } + + return allErrs +} + +func localRefEqual(a, b *ipam.LocalRef) bool { + switch { + case a == nil && b == nil: + return true + case a == nil || b == nil: + return false + default: + return a.Name == b.Name + } +} + + +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + p, ok := obj.(*ipam.IPPool) + if !ok { + return nil, nil, fmt.Errorf("given object is not an IPPool") + } + return p.Labels, SelectableFields(p), nil +} + +func SelectableFields(p *ipam.IPPool) fields.Set { + objectMetaFields := generic.ObjectMetaFieldsSet(&p.ObjectMeta, false) + parentName := "" + if p.Spec.ParentPoolRef != nil { + parentName = p.Spec.ParentPoolRef.Name + } + specific := fields.Set{ + "spec.ipFamily": string(p.Spec.IPFamily), + "spec.parentPoolRef.name": parentName, + } + return generic.MergeFieldsSets(objectMetaFields, specific) +} + +func Match(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { + return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetAttrs} +} + +func (ipPoolStatusStrategy) NamespaceScoped() bool { return false } + +func (ipPoolStatusStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { + n := obj.(*ipam.IPPool) + o := old.(*ipam.IPPool) + n.Spec = o.Spec + n.Labels = o.Labels + n.Annotations = o.Annotations +} + +func (ipPoolStatusStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { + return nil +} + +func (ipPoolStatusStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { + return nil +} + +func (ipPoolStatusStrategy) AllowCreateOnUpdate() bool { return false } +func (ipPoolStatusStrategy) AllowUnconditionalUpdate() bool { return true } +func (ipPoolStatusStrategy) Canonicalize(_ runtime.Object) {} + +func (ipPoolStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + return map[fieldpath.APIVersion]*fieldpath.Set{ + "ipam.miloapis.com/v1alpha1": fieldpath.NewSet(fieldpath.MakePathOrDie("spec")), + } +} diff --git a/internal/registry/ipam/ipprefix/storage.go b/internal/registry/ipam/ipprefix/storage.go deleted file mode 100644 index 7e96cde..0000000 --- a/internal/registry/ipam/ipprefix/storage.go +++ /dev/null @@ -1,150 +0,0 @@ -// Package ipprefix provides REST storage for the IPPrefix resource and the -// closely-related IPPrefixClass resource. -package ipprefix - -import ( - "context" - "fmt" - - "github.com/jackc/pgx/v5/pgxpool" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/generic" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "k8s.io/apiserver/pkg/registry/rest" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/pkg/apis/ipam" - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" -) - -// ---------------------------------------------------------------------------- -// IPPrefixClass storage (cluster-scoped, no status subresource). -// ---------------------------------------------------------------------------- - -type IPPrefixClassStorage struct { - *genericregistry.Store -} - -func NewClassStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*IPPrefixClassStorage, error) { - strategy := NewClassStrategy(scheme) - - store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &ipam.IPPrefixClass{} }, - NewListFunc: func() runtime.Object { return &ipam.IPPrefixClassList{} }, - DefaultQualifiedResource: v1alpha1.Resource("ipprefixclasses"), - SingularQualifiedResource: v1alpha1.Resource("ipprefixclass"), - - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - - TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ipprefixclasses")), - } - - if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetClassAttrs}); err != nil { - return nil, err - } - return &IPPrefixClassStorage{store}, nil -} - -// ---------------------------------------------------------------------------- -// IPPrefix storage (cluster-scoped, with status subresource). -// ---------------------------------------------------------------------------- - -type IPPrefixStorage struct { - *genericregistry.Store - // db is used by Delete to count active allocations against this prefix - // before letting the standard delete proceed. - db *pgxpool.Pool -} - -type IPPrefixStatusStorage struct { - store *genericregistry.Store -} - -func (s *IPPrefixStatusStorage) New() runtime.Object { return &ipam.IPPrefix{} } -func (s *IPPrefixStatusStorage) Destroy() {} - -func (s *IPPrefixStatusStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return s.store.Get(ctx, name, options) -} - -func (s *IPPrefixStatusStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) -} - -func (s *IPPrefixStatusStorage) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return s.store.GetResetFields() -} - -func (s *IPPrefixStatusStorage) ConvertToTable(ctx context.Context, obj runtime.Object, opts runtime.Object) (*metav1.Table, error) { - return s.store.ConvertToTable(ctx, obj, opts) -} - -// NewPrefixStorage builds the IPPrefix REST storage. -// -// db is the pgx pool used by Delete to reject prefixes that still have -// active allocations recorded in ipam_prefix_allocations (HTTP 409 -// Conflict). -func NewPrefixStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, db *pgxpool.Pool) (*IPPrefixStorage, *IPPrefixStatusStorage, error) { - strategy := NewPrefixStrategy(scheme) - statusStrategy := NewPrefixStatusStrategy(scheme) - - store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &ipam.IPPrefix{} }, - NewListFunc: func() runtime.Object { return &ipam.IPPrefixList{} }, - DefaultQualifiedResource: v1alpha1.Resource("ipprefixes"), - SingularQualifiedResource: v1alpha1.Resource("ipprefix"), - - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - - TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ipprefixes")), - } - - if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetPrefixAttrs}); err != nil { - return nil, nil, err - } - - statusStore := *store - statusStore.UpdateStrategy = statusStrategy - statusStore.ResetFieldsStrategy = statusStrategy - - return &IPPrefixStorage{Store: store, db: db}, &IPPrefixStatusStorage{store: &statusStore}, nil -} - -// Delete rejects the request when active allocations are tracked against -// this prefix in ipam_prefix_allocations. We check up-front rather than -// cascade-delete so callers see a deterministic 409 they can react to — -// "release all claims first" — instead of finding orphaned claims after -// the fact. -func (r *IPPrefixStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - poolKey := prefixPoolKey(name) - var count int - if err := r.db.QueryRow(ctx, - `SELECT COUNT(*) FROM ipam_prefix_allocations WHERE pool_key = $1`, - poolKey, - ).Scan(&count); err != nil { - return nil, false, fmt.Errorf("count active allocations for %q: %w", name, err) - } - if count > 0 { - return nil, false, apierrors.NewConflict( - schema.GroupResource{Group: v1alpha1.GroupName, Resource: "ipprefixes"}, - name, - fmt.Errorf("cannot delete IPPrefix with %d active allocation(s); release all claims first", count), - ) - } - return r.Store.Delete(ctx, name, deleteValidation, options) -} - -// prefixPoolKey is the storage key for the cluster-scoped IPPrefix pool. It -// matches the key shape used by the AllocatingREST claim handlers when they -// write into ipam_prefix_allocations, so a COUNT keyed on it is a faithful -// "is anything still using this pool?" query. -func prefixPoolKey(name string) string { - return fmt.Sprintf("/ipam.miloapis.com/ipprefixes/%s", name) -} diff --git a/internal/registry/ipam/ipprefix/strategy_class.go b/internal/registry/ipam/ipprefix/strategy_class.go deleted file mode 100644 index a872d3e..0000000 --- a/internal/registry/ipam/ipprefix/strategy_class.go +++ /dev/null @@ -1,65 +0,0 @@ -package ipprefix - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/storage" - "k8s.io/apiserver/pkg/storage/names" - - "go.miloapis.com/ipam/pkg/apis/ipam" -) - -type ipPrefixClassStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -func NewClassStrategy(typer runtime.ObjectTyper) ipPrefixClassStrategy { - return ipPrefixClassStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func (ipPrefixClassStrategy) NamespaceScoped() bool { return false } -func (ipPrefixClassStrategy) PrepareForCreate(_ context.Context, _ runtime.Object) {} -func (ipPrefixClassStrategy) PrepareForUpdate(_ context.Context, _, _ runtime.Object) {} -func (ipPrefixClassStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { - return validateIPPrefixClass(obj.(*ipam.IPPrefixClass)) -} -func (ipPrefixClassStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { - return nil -} -func (ipPrefixClassStrategy) AllowCreateOnUpdate() bool { return false } -func (ipPrefixClassStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipPrefixClassStrategy) Canonicalize(_ runtime.Object) {} -func (ipPrefixClassStrategy) ValidateUpdate(_ context.Context, obj, _ runtime.Object) field.ErrorList { - return validateIPPrefixClass(obj.(*ipam.IPPrefixClass)) -} -func (ipPrefixClassStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func validateIPPrefixClass(c *ipam.IPPrefixClass) field.ErrorList { - var allErrs field.ErrorList - specPath := field.NewPath("spec") - if c.Spec.Visibility != "" && c.Spec.Visibility != "platform" && c.Spec.Visibility != "consumer" && c.Spec.Visibility != "shared" { - allErrs = append(allErrs, field.NotSupported(specPath.Child("visibility"), c.Spec.Visibility, []string{"platform", "consumer", "shared"})) - } - return allErrs -} - -func GetClassAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { - c, ok := obj.(*ipam.IPPrefixClass) - if !ok { - return nil, nil, fmt.Errorf("given object is not an IPPrefixClass") - } - return c.Labels, generic.ObjectMetaFieldsSet(&c.ObjectMeta, false), nil -} - -func MatchIPPrefixClass(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { - return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetClassAttrs} -} diff --git a/internal/registry/ipam/ipprefix/strategy_prefix.go b/internal/registry/ipam/ipprefix/strategy_prefix.go deleted file mode 100644 index fcd2430..0000000 --- a/internal/registry/ipam/ipprefix/strategy_prefix.go +++ /dev/null @@ -1,200 +0,0 @@ -package ipprefix - -import ( - "context" - "fmt" - "net" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/storage" - "k8s.io/apiserver/pkg/storage/names" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/internal/allocation" - "go.miloapis.com/ipam/internal/fieldindex" - "go.miloapis.com/ipam/pkg/apis/ipam" -) - -// FieldIndexes are the SQL expression indexes that back IPPrefix field -// selectors declared in SelectablePrefixFields. Applied idempotently by SyncIndexes. -var FieldIndexes = []fieldindex.FieldIndex{ - { - IndexName: "idx_ipam_ipprefix_ip_family", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) WHERE kind = 'IPPrefix'`, - }, - { - IndexName: "idx_ipam_ipprefix_class_ref_name", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' -> 'classRef' ->> 'name')) WHERE kind = 'IPPrefix'`, - }, -} - -type ipPrefixStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -type ipPrefixStatusStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -func NewPrefixStrategy(typer runtime.ObjectTyper) ipPrefixStrategy { - return ipPrefixStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func NewPrefixStatusStrategy(typer runtime.ObjectTyper) ipPrefixStatusStrategy { - return ipPrefixStatusStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func (ipPrefixStrategy) NamespaceScoped() bool { return false } - -func (ipPrefixStrategy) PrepareForCreate(_ context.Context, obj runtime.Object) { - p := obj.(*ipam.IPPrefix) - // Default the allocation strategy so the field is visible in - // `kubectl get ipprefix -o yaml`. The allocator silently falls back - // to FirstFit when the field is empty, but operators reasoning about - // behaviour should not have to know that — making it explicit also - // surfaces it on the audit log. - if p.Spec.Allocation.Strategy == "" { - p.Spec.Allocation.Strategy = ipam.FirstFit - } - // This apiserver allocates synchronously; there is no controller that - // later transitions Pending → Ready. Compute the canonical CIDR and - // total capacity here so the persisted row is immediately usable as a - // pool. If the CIDR is invalid, fall back to Pending — Validate will - // reject the create on the next step in the strategy chain. - p.Status = ipam.IPPrefixStatus{Phase: ipam.PrefixPending} - if p.Spec.CIDR == "" { - return - } - _, ipnet, err := net.ParseCIDR(p.Spec.CIDR) - if err != nil { - return - } - p.Status.CIDR = ipnet.String() - p.Status.Capacity = ipam.PrefixCapacity{Total: allocation.CountAddresses(*ipnet)} - p.Status.Phase = ipam.PrefixReady - p.Status.Conditions = []metav1.Condition{{ - Type: "Ready", - Status: metav1.ConditionTrue, - Reason: "PrefixReady", - Message: "IPPrefix is ready for allocation", - LastTransitionTime: metav1.Now(), - }} -} - -func (ipPrefixStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPPrefix) - o := old.(*ipam.IPPrefix) - n.Status = o.Status -} - -func (ipPrefixStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { - return validateIPPrefix(obj.(*ipam.IPPrefix)) -} - -func (ipPrefixStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { return nil } -func (ipPrefixStrategy) AllowCreateOnUpdate() bool { return false } -func (ipPrefixStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipPrefixStrategy) Canonicalize(_ runtime.Object) {} - -func (ipPrefixStrategy) ValidateUpdate(_ context.Context, obj, old runtime.Object) field.ErrorList { - n := obj.(*ipam.IPPrefix) - o := old.(*ipam.IPPrefix) - allErrs := validateIPPrefix(n) - if n.Spec.CIDR != o.Spec.CIDR { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cidr"), "spec.cidr is immutable")) - } - if n.Spec.IPFamily != o.Spec.IPFamily { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ipFamily"), "spec.ipFamily is immutable")) - } - if n.Spec.ClassRef != o.Spec.ClassRef { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "classRef"), "spec.classRef is immutable")) - } - return allErrs -} - -func (ipPrefixStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func validateIPPrefix(p *ipam.IPPrefix) field.ErrorList { - var allErrs field.ErrorList - specPath := field.NewPath("spec") - - if p.Spec.CIDR == "" { - allErrs = append(allErrs, field.Required(specPath.Child("cidr"), "cidr is required")) - } else if _, _, err := net.ParseCIDR(p.Spec.CIDR); err != nil { - allErrs = append(allErrs, field.Invalid(specPath.Child("cidr"), p.Spec.CIDR, fmt.Sprintf("invalid CIDR: %v", err))) - } - if p.Spec.IPFamily == "" { - allErrs = append(allErrs, field.Required(specPath.Child("ipFamily"), "ipFamily is required")) - } else if p.Spec.IPFamily != ipam.IPv4 && p.Spec.IPFamily != ipam.IPv6 { - allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), p.Spec.IPFamily, []string{string(ipam.IPv4), string(ipam.IPv6)})) - } - if p.Spec.ClassRef.Name == "" { - allErrs = append(allErrs, field.Required(specPath.Child("classRef", "name"), "classRef.name is required")) - } - if p.Spec.Allocation.MinPrefixLength > 0 && p.Spec.Allocation.MaxPrefixLength > 0 && - p.Spec.Allocation.MinPrefixLength > p.Spec.Allocation.MaxPrefixLength { - allErrs = append(allErrs, field.Invalid( - specPath.Child("allocation"), p.Spec.Allocation, - "minPrefixLength must be <= maxPrefixLength", - )) - } - return allErrs -} - -func GetPrefixAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { - p, ok := obj.(*ipam.IPPrefix) - if !ok { - return nil, nil, fmt.Errorf("given object is not an IPPrefix") - } - return p.Labels, SelectablePrefixFields(p), nil -} - -func SelectablePrefixFields(p *ipam.IPPrefix) fields.Set { - objectMetaFields := generic.ObjectMetaFieldsSet(&p.ObjectMeta, true) - specific := fields.Set{ - "spec.ipFamily": string(p.Spec.IPFamily), - "spec.classRef.name": p.Spec.ClassRef.Name, - } - return generic.MergeFieldsSets(objectMetaFields, specific) -} - -func MatchIPPrefix(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { - return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetPrefixAttrs} -} - -func (ipPrefixStatusStrategy) NamespaceScoped() bool { return false } - -func (ipPrefixStatusStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPPrefix) - o := old.(*ipam.IPPrefix) - n.Spec = o.Spec - n.Labels = o.Labels - n.Annotations = o.Annotations -} - -func (ipPrefixStatusStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { - return nil -} - -func (ipPrefixStatusStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func (ipPrefixStatusStrategy) AllowCreateOnUpdate() bool { return false } -func (ipPrefixStatusStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipPrefixStatusStrategy) Canonicalize(_ runtime.Object) {} - -func (ipPrefixStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return map[fieldpath.APIVersion]*fieldpath.Set{ - "ipam.miloapis.com/v1alpha1": fieldpath.NewSet(fieldpath.MakePathOrDie("spec")), - } -} diff --git a/internal/registry/ipam/ipprefixclaim/storage.go b/internal/registry/ipam/ipprefixclaim/storage.go deleted file mode 100644 index 2b00f06..0000000 --- a/internal/registry/ipam/ipprefixclaim/storage.go +++ /dev/null @@ -1,553 +0,0 @@ -// Package ipprefixclaim provides REST storage for the IPPrefixClaim -// resource. The exported AllocatingREST type wraps the standard storage -// with a synchronous Postgres-backed allocator: when configured, Create -// resolves a free sub-prefix from the parent IPPrefix pool inside a single -// transaction so the caller's response includes the allocated CIDR. -package ipprefixclaim - -import ( - "context" - "errors" - "fmt" - "net" - "strings" - "time" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/generic" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/apiserver/pkg/storage" - "k8s.io/klog/v2" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/internal/access" - "go.miloapis.com/ipam/internal/allocation" - "go.miloapis.com/ipam/internal/allocator" - "go.miloapis.com/ipam/internal/metrics" - "go.miloapis.com/ipam/internal/registry/ipam/registryerrors" - "go.miloapis.com/ipam/internal/tenant" - "go.miloapis.com/ipam/pkg/apis/ipam" - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" -) - -type IPPrefixClaimStorage struct { - *genericregistry.Store -} - -type IPPrefixClaimStatusStorage struct { - store *genericregistry.Store -} - -func (s *IPPrefixClaimStatusStorage) New() runtime.Object { return &ipam.IPPrefixClaim{} } -func (s *IPPrefixClaimStatusStorage) Destroy() {} - -func (s *IPPrefixClaimStatusStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return s.store.Get(ctx, name, options) -} - -func (s *IPPrefixClaimStatusStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) -} - -func (s *IPPrefixClaimStatusStorage) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return s.store.GetResetFields() -} - -func (s *IPPrefixClaimStatusStorage) ConvertToTable(ctx context.Context, obj runtime.Object, opts runtime.Object) (*metav1.Table, error) { - return s.store.ConvertToTable(ctx, obj, opts) -} - -// newInnerStorage builds the underlying genericregistry.Store-backed REST -// storage for IPPrefixClaim. NewAllocatingStorage wraps the result to add -// synchronous Postgres-backed allocation in the request path; nothing -// outside this package calls it directly. -func newInnerStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*IPPrefixClaimStorage, *IPPrefixClaimStatusStorage, error) { - strategy := NewStrategy(scheme) - statusStrategy := NewStatusStrategy(scheme) - - store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &ipam.IPPrefixClaim{} }, - NewListFunc: func() runtime.Object { return &ipam.IPPrefixClaimList{} }, - DefaultQualifiedResource: v1alpha1.Resource("ipprefixclaims"), - SingularQualifiedResource: v1alpha1.Resource("ipprefixclaim"), - - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - - TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("ipprefixclaims")), - } - - if err := store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}); err != nil { - return nil, nil, err - } - - statusStore := *store - statusStore.UpdateStrategy = statusStrategy - statusStore.ResetFieldsStrategy = statusStrategy - - return &IPPrefixClaimStorage{store}, &IPPrefixClaimStatusStorage{store: &statusStore}, nil -} - -// AllocatingREST decorates the standard claim storage with a synchronous -// allocator. On Create it begins a Postgres transaction, asks the allocator -// to reserve a sub-prefix from the parent pool, and returns the claim with -// its status fully populated. On Delete it asks the allocator to release the -// recorded allocation in the same transaction as the claim deletion. -type AllocatingREST struct { - *IPPrefixClaimStorage - allocator allocator.PrefixAllocator - db *pgxpool.Pool - strategy ipPrefixClaimStrategy - poolChecker access.PoolAccessChecker - // codec serialises the in-memory claim into the same wire format the - // storage Get path expects. Internal types lack JSON tags, so json.Marshal - // would silently drop spec/status fields when read back. - codec runtime.Codec -} - -// NewAllocatingStorage builds the IPPrefixClaim REST storage with synchronous -// Postgres-backed allocation. db must be the same pool the allocator commits -// against. codec is used to serialise the synchronously-allocated claim into -// ipam_objects so subsequent GETs return a fully-populated object. -// poolChecker may be nil; when non-nil it authorises cross-project claims -// via SubjectAccessReview before allocation. -func NewAllocatingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, alloc allocator.PrefixAllocator, db *pgxpool.Pool, codec runtime.Codec, poolChecker access.PoolAccessChecker) (*AllocatingREST, *IPPrefixClaimStatusStorage, error) { - claimStore, statusStore, err := newInnerStorage(scheme, optsGetter) - if err != nil { - return nil, nil, err - } - return &AllocatingREST{ - IPPrefixClaimStorage: claimStore, - allocator: alloc, - db: db, - strategy: NewStrategy(scheme), - poolChecker: poolChecker, - codec: codec, - }, statusStore, nil -} - -// Create runs the standard create pipeline (system-metadata fill, strategy -// PrepareForCreate, validation), then drives the allocator inside a -// short-lived transaction. The allocator is expected to persist the claim -// row, the allocation row, and (when ChildPrefixTemplate is set) the child -// IPPrefix object inside that transaction. -func (r *AllocatingREST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - claim, ok := obj.(*ipam.IPPrefixClaim) - if !ok { - return nil, fmt.Errorf("expected *ipam.IPPrefixClaim, got %T", obj) - } - - // Tenant identity is needed up front so the project / org metric labels - // are available to AllocationAttempts and the deferred AllocationDuration - // observation. project / org come from the iam.miloapis.com/parent-* extras - // via tenant.Identity helpers; both are "" for platform-scoped requests - // (and org is "" today for project-scoped requests until Milo forwards - // the owning org alongside the project). - id := tenant.FromContext(ctx) - project := id.Project() - org := id.Org() - - // ipFamily is derived from the claim spec up front so it can label - // AllocationAttempts (counted immediately below) and AllocationFailures - // (recorded throughout the handler) identically with the latency - // histogram. claim.Spec.IPFamily is set on every valid claim; pre-spec - // failures land in the empty-string family, distinguishable from - // family-tagged successes. - ipFamily := string(claim.Spec.IPFamily) - // Counted at the top of the synchronous path so failures (validation, - // auth, allocation, encode, commit) all show up against attempts and - // success ratios survive partial flow-through. - metrics.AllocationAttempts.WithLabelValues("ipprefixclaim", ipFamily, project, org).Inc() - // Track latency for every synchronous attempt under (resource, result, - // ip_family, project, org). `result` defaults to "error" and is - // overwritten by the success branch just before commit. The deferred - // Observe runs after every return so the histogram count tracks - // AllocationAttempts 1:1. - allocStart := time.Now() - result := "error" - defer func() { - metrics.ObserveAllocationDuration("ipprefixclaim", result, ipFamily, project, org, allocStart) - }() - - objectMeta, err := meta.Accessor(claim) - if err != nil { - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("get object metadata: %w", err) - } - rest.FillObjectMetaSystemFields(objectMeta) - - if err := rest.BeforeCreate(r.strategy, ctx, claim); err != nil { - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, err - } - if createValidation != nil { - if err := createValidation(ctx, claim.DeepCopyObject()); err != nil { - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, err - } - } - - if claim.Spec.PrefixRef == nil && claim.Spec.PrefixSelector == nil { - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, apierrors.NewBadRequest("synchronous allocation requires spec.prefixRef or spec.prefixSelector") - } - if claim.Spec.PrefixRef != nil && claim.Spec.PrefixSelector != nil { - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, apierrors.NewBadRequest("spec.prefixRef and spec.prefixSelector are mutually exclusive") - } - - if !id.IsPlatform() { - // Overwrite client-supplied ownerRef — requestheader CA guarantees - // Extra authenticity, so the tenant identity is the source of truth. - claim.Spec.OwnerRef = &ipam.ObjectRef{ - APIGroup: id.APIGroup, - Kind: id.Kind, - Name: id.Name, - } - } - - tx, err := r.db.Begin(ctx) - if err != nil { - metrics.RecordAllocationFailure("ipprefixclaim", "tx_error", ipFamily, project, org) - return nil, fmt.Errorf("begin allocation transaction: %w", err) - } - - // Resolve the target pool. With spec.prefixRef this is a direct named - // lookup; with spec.prefixSelector we list candidate pools, filter by - // the supplied label selector, and pick the first match by storage key - // (see allocator.ResolvePrefixPool for why first-match is the chosen - // strategy). Cross-project routing is only supported through - // spec.prefixRef.projectRef; selectors evaluate within the caller's - // project scope unless they carry an explicit projectRef. - isCrossProject := false - var poolKey, poolName string - if claim.Spec.PrefixRef != nil { - poolName = claim.Spec.PrefixRef.Name - isCrossProject = !id.IsPlatform() && - claim.Spec.PrefixRef.ProjectRef != nil && - claim.Spec.PrefixRef.ProjectRef.Name != id.Name - if isCrossProject { - poolKey = tenant.Identity{Name: claim.Spec.PrefixRef.ProjectRef.Name}.ResourceKey("ipprefixes", poolName) - } else { - poolKey = id.ResourceKey("ipprefixes", poolName) - } - } else { - // PrefixSelector path. The selector's optional ProjectRef lets a - // claim target a specific project's pools; absent that, scope to - // the caller's own project (or platform). - ownerProject := id.Name - if claim.Spec.PrefixSelector.ProjectRef != nil { - ownerProject = claim.Spec.PrefixSelector.ProjectRef.Name - isCrossProject = !id.IsPlatform() && ownerProject != id.Name - } - resolved, rerr := allocator.ResolvePrefixPool(ctx, tx, claim.Spec.PrefixSelector.LabelSelector, ownerProject, string(claim.Spec.IPFamily)) - if rerr != nil { - _ = tx.Rollback(ctx) - if errors.Is(rerr, allocator.ErrPoolNotFound) { - metrics.RecordAllocationFailure("ipprefixclaim", "pool_not_found", ipFamily, project, org) - return nil, apierrors.NewBadRequest("no IPPrefix pool matches spec.prefixSelector") - } - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("resolve prefix pool: %w", rerr) - } - poolKey = resolved - // Storage key has the form "/ipam.miloapis.com/ipprefixes/" or - // "project//ipam.miloapis.com/ipprefixes/"; the last - // segment after the final '/' is the pool name. We need it for - // status.boundPrefixRef and (when ChildPrefixTemplate is set) the - // child's ParentRef. - poolName = poolKey[strings.LastIndex(poolKey, "/")+1:] - } - claimKey := claimObjectKey(claim.Namespace, claim.Name) - - if isCrossProject { - if err := r.authorizeCrossProject(ctx, tx, poolKey); err != nil { - _ = tx.Rollback(ctx) - if errors.Is(err, access.ErrCrossProjectDenied) { - // Selector-driven lookups must not distinguish "no pool - // matched the selector" from "a pool matched but you can't - // use it" — that distinction is a label/existence - // fingerprint into another project (audit finding H1). - // Direct prefixRef lookups can return Forbidden because - // the caller already named the pool by hand. - if claim.Spec.PrefixSelector != nil { - metrics.RecordAllocationFailure("ipprefixclaim", "pool_not_found", ipFamily, project, org) - return nil, apierrors.NewBadRequest("no IPPrefix pool matches spec.prefixSelector") - } - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, apierrors.NewForbidden( - v1alpha1.Resource("ipprefixes"), - poolKey, - fmt.Errorf("cross-project pool not accessible"), - ) - } - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, err - } - } - - cidr, err := r.allocator.AllocatePrefix(ctx, tx, poolKey, claim.Spec.PrefixLength, string(claim.Spec.IPFamily), claimKey, id.Name) - if err != nil { - _ = tx.Rollback(ctx) - reason := allocationFailureReason(err) - metrics.RecordAllocationFailure("ipprefixclaim", reason, ipFamily, project, org) - if reason == "pool_exhausted" { - result = "exhausted" - } - return nil, mapAllocationError(err) - } - - // Populate status synchronously so the persisted row already reflects - // the bound state and the CREATE response carries the allocated CIDR. - claim.Status.Phase = ipam.ClaimBound - claim.Status.AllocatedCIDR = cidr - claim.Status.BoundPrefixRef = &ipam.LocalRef{Name: poolName} - - claimData, err := runtime.Encode(r.codec, claim) - if err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("encode claim: %w", err) - } - rv, err := r.allocator.InsertObject(ctx, tx, claimKey, "IPPrefixClaim", claim.Namespace, claim.Name, claimData) - if err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipprefixclaim", "tx_error", ipFamily, project, org) - return nil, fmt.Errorf("persist claim: %w", err) - } - versioner := storage.APIObjectVersioner{} - if err := versioner.UpdateObject(claim, uint64(rv)); err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("set resource version: %w", err) - } - - if claim.Spec.ChildPrefixTemplate != nil { - child := &ipam.IPPrefix{ - ObjectMeta: claim.Spec.ChildPrefixTemplate.Metadata, - Spec: claim.Spec.ChildPrefixTemplate.Spec, - } - // IPPrefix is cluster-scoped; drop any namespace the template may - // have carried over from older configurations. - child.Namespace = "" - child.Spec.CIDR = cidr - // Inherit ipFamily from the claim when the template did not set it - // — otherwise the child lands with spec.ipFamily="" and downstream - // validation/allocation has no way to recover it. - if child.Spec.IPFamily == "" { - child.Spec.IPFamily = claim.Spec.IPFamily - } - child.Spec.ParentRef = &ipam.ObjectRef{ - APIGroup: v1alpha1.GroupName, - Kind: "IPPrefix", - Name: poolName, - } - // Children skip the standard create path so PrepareForCreate never - // runs on them. Mirror the full Status block PrepareForCreate would - // have set (phase + canonical CIDR + capacity + Ready condition) so - // the prefix-hierarchy e2e suite — which asserts on all four — does - // not have to wait for a follow-up status update that never comes. - if _, ipnet, parseErr := net.ParseCIDR(cidr); parseErr == nil { - child.Status = ipam.IPPrefixStatus{ - Phase: ipam.PrefixReady, - CIDR: ipnet.String(), - Capacity: ipam.PrefixCapacity{Total: allocation.CountAddresses(*ipnet)}, - Conditions: []metav1.Condition{{ - Type: "Ready", - Status: metav1.ConditionTrue, - Reason: "PrefixReady", - Message: "IPPrefix is ready for allocation", - LastTransitionTime: metav1.Now(), - }}, - } - } - childKey := childPrefixObjectKey(child.Namespace, child.Name) - childData, err := runtime.Encode(r.codec, child) - if err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipprefixclaim", "internal", ipFamily, project, org) - return nil, fmt.Errorf("encode child prefix: %w", err) - } - if err := r.allocator.InsertChildPrefix(ctx, tx, childKey, child.Namespace, child.Name, childData); err != nil { - _ = tx.Rollback(ctx) - metrics.RecordAllocationFailure("ipprefixclaim", "tx_error", ipFamily, project, org) - return nil, fmt.Errorf("insert child prefix: %w", err) - } - } - - if err := tx.Commit(ctx); err != nil { - metrics.RecordAllocationFailure("ipprefixclaim", "tx_error", ipFamily, project, org) - return nil, fmt.Errorf("commit allocation transaction: %w", err) - } - - result = "success" - return claim, nil -} - -// allocationFailureReason maps an allocator error onto the canonical reason -// label set used by ipam_allocation_failures_total. The histogram's `result` -// label uses a coarser bucketing — pool exhaustion is its own outcome, every -// other failure rolls up to "error" — so the two metrics intentionally do -// not share a label set. -func allocationFailureReason(err error) string { - switch { - case errors.Is(err, allocator.ErrPoolExhausted): - return "pool_exhausted" - case errors.Is(err, allocator.ErrPoolNotFound): - return "pool_not_found" - default: - return "tx_error" - } -} - -// Delete runs the claim teardown in two transactions so watchers can observe -// the intermediate phase=Releasing state before the object disappears: -// -// TX1: UPDATE the claim row with status.phase=Releasing + MODIFIED changelog -// TX2: Release the allocation + DeleteObject + DELETED changelog -// -// TX2 is retried up to deleteMaxAttempts times with a short backoff because a -// transient failure between the two transactions would leave the claim -// stranded in Releasing. After the retries are exhausted the claim stays in -// Releasing and is visible to operators — the allocation may have been -// released by an aborted attempt, but no allocation is leaked because Release -// is idempotent on the claim_key. -func (r *AllocatingREST) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - existing, err := r.Get(ctx, name, &metav1.GetOptions{}) - if err != nil { - return nil, false, err - } - claim, ok := existing.(*ipam.IPPrefixClaim) - if !ok { - return nil, false, fmt.Errorf("expected *ipam.IPPrefixClaim from Get, got %T", existing) - } - if deleteValidation != nil { - if err := deleteValidation(ctx, claim.DeepCopyObject()); err != nil { - return nil, false, err - } - } - - claimKey := claimObjectKey(claim.Namespace, claim.Name) - - // TX1 — publish phase=Releasing. Deep-copy first so the in-memory claim - // returned to the caller carries the Releasing phase without mutating the - // object the storage layer cached. - releasing := claim.DeepCopy() - releasing.Status.Phase = ipam.ClaimReleasing - releasingData, err := runtime.Encode(r.codec, releasing) - if err != nil { - return nil, false, fmt.Errorf("encode releasing claim: %w", err) - } - tx1, err := r.db.Begin(ctx) - if err != nil { - return nil, false, fmt.Errorf("begin releasing transaction: %w", err) - } - rv, err := r.allocator.UpdateObject(ctx, tx1, claimKey, releasingData) - if err != nil { - _ = tx1.Rollback(ctx) - return nil, false, fmt.Errorf("publish releasing phase: %w", err) - } - versioner := storage.APIObjectVersioner{} - if err := versioner.UpdateObject(releasing, uint64(rv)); err != nil { - _ = tx1.Rollback(ctx) - return nil, false, fmt.Errorf("set releasing resource version: %w", err) - } - if err := tx1.Commit(ctx); err != nil { - return nil, false, fmt.Errorf("commit releasing transaction: %w", err) - } - klog.V(2).InfoS("claim entering Releasing phase", "claim", name) - - // TX2 — release the allocation and delete the object row. Retry on - // transient failures so a brief PG hiccup does not leave the claim - // stranded in Releasing forever; the user-facing Delete contract is - // "Releasing is observable, then the object disappears". - var lastErr error - for attempt := 1; attempt <= deleteMaxAttempts; attempt++ { - lastErr = r.releaseAndDelete(ctx, claimKey) - if lastErr == nil { - break - } - klog.ErrorS(lastErr, "release-and-delete attempt failed", "claim", name, "attempt", attempt) - if attempt < deleteMaxAttempts { - time.Sleep(deleteRetryBackoff) - } - } - if lastErr != nil { - klog.ErrorS(lastErr, "claim stuck in Releasing after retries — manual intervention may be required", "claim", name, "attempts", deleteMaxAttempts) - return nil, false, fmt.Errorf("release allocation after %d attempts: %w", deleteMaxAttempts, lastErr) - } - - klog.V(2).InfoS("claim released and deleted", "claim", name) - metrics.RecordRelease("ipprefixclaim") - return releasing, true, nil -} - -// releaseAndDelete is a single attempt of TX2: release the allocation row(s) -// for claimKey and delete the object row, all inside one transaction. -func (r *AllocatingREST) releaseAndDelete(ctx context.Context, claimKey string) error { - tx, err := r.db.Begin(ctx) - if err != nil { - return fmt.Errorf("begin release transaction: %w", err) - } - if err := r.allocator.Release(ctx, tx, claimKey); err != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("release allocation: %w", err) - } - if _, err := r.allocator.DeleteObject(ctx, tx, claimKey); err != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("delete claim row: %w", err) - } - if err := tx.Commit(ctx); err != nil { - return fmt.Errorf("commit release transaction: %w", err) - } - return nil -} - -// deleteMaxAttempts and deleteRetryBackoff govern the TX2 retry loop. Three -// attempts at 100ms covers the common transient failure (brief connection -// loss) without holding the request open for more than a few hundred -// milliseconds; persistent failures surface as a 500 with the claim still -// observable in Releasing. -const ( - deleteMaxAttempts = 3 - deleteRetryBackoff = 100 * time.Millisecond -) - -func claimObjectKey(namespace, name string) string { - return fmt.Sprintf("/ipam.miloapis.com/ipprefixclaims/%s/%s", namespace, name) -} - -// childPrefixObjectKey is the storage key for a child IPPrefix materialised -// from a claim's ChildPrefixTemplate. IPPrefix is cluster-scoped, so -// the namespace argument from the template is ignored at the key layer. -func childPrefixObjectKey(_, name string) string { - return fmt.Sprintf("/ipam.miloapis.com/ipprefixes/%s", name) -} - -// authorizeCrossProject delegates to the shared cross-project gate in -// internal/access. Kept as a thin method so the call site reads naturally; -// the same gate is used by ipaddressclaim's Create handler so the policy -// (fail-closed when no checker, visibility=shared check, SAR, single -// sentinel for all denial paths) lives in exactly one place. -func (r *AllocatingREST) authorizeCrossProject(ctx context.Context, tx pgx.Tx, poolKey string) error { - return access.AuthorizeCrossProjectPrefix(ctx, tx, poolKey, r.poolChecker) -} - -func mapAllocationError(err error) error { - switch { - case errors.Is(err, allocator.ErrPoolExhausted): - return registryerrors.NewInsufficientStorage("prefix pool exhausted") - case errors.Is(err, allocator.ErrPoolNotFound): - return apierrors.NewBadRequest("prefix pool not found") - default: - return apierrors.NewInternalError(err) - } -} - diff --git a/internal/registry/ipam/ipprefixclaim/strategy.go b/internal/registry/ipam/ipprefixclaim/strategy.go deleted file mode 100644 index e6919f1..0000000 --- a/internal/registry/ipam/ipprefixclaim/strategy.go +++ /dev/null @@ -1,185 +0,0 @@ -package ipprefixclaim - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/storage" - "k8s.io/apiserver/pkg/storage/names" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" - - "go.miloapis.com/ipam/internal/fieldindex" - "go.miloapis.com/ipam/pkg/apis/ipam" -) - -// FieldIndexes are the SQL expression indexes that back IPPrefixClaim field -// selectors declared in SelectableFields. Applied idempotently by SyncIndexes. -var FieldIndexes = []fieldindex.FieldIndex{ - { - IndexName: "idx_ipam_ipprefixclaim_ip_family", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) WHERE kind = 'IPPrefixClaim'`, - }, - { - IndexName: "idx_ipam_ipprefixclaim_prefix_ref_name", - Expression: `((ipam_data_to_jsonb(data) -> 'spec' -> 'prefixRef' ->> 'name')) WHERE kind = 'IPPrefixClaim'`, - }, -} - -type ipPrefixClaimStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -type ipPrefixClaimStatusStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -func NewStrategy(typer runtime.ObjectTyper) ipPrefixClaimStrategy { - return ipPrefixClaimStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func NewStatusStrategy(typer runtime.ObjectTyper) ipPrefixClaimStatusStrategy { - return ipPrefixClaimStatusStrategy{ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator} -} - -func (ipPrefixClaimStrategy) NamespaceScoped() bool { return true } - -func (ipPrefixClaimStrategy) PrepareForCreate(_ context.Context, obj runtime.Object) { - c := obj.(*ipam.IPPrefixClaim) - c.Status = ipam.IPPrefixClaimStatus{Phase: ipam.ClaimPending} -} - -func (ipPrefixClaimStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPPrefixClaim) - o := old.(*ipam.IPPrefixClaim) - n.Status = o.Status -} - -func (ipPrefixClaimStrategy) Validate(_ context.Context, obj runtime.Object) field.ErrorList { - return validateIPPrefixClaim(obj.(*ipam.IPPrefixClaim)) -} - -func (ipPrefixClaimStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { - return nil -} - -func (ipPrefixClaimStrategy) AllowCreateOnUpdate() bool { return false } -func (ipPrefixClaimStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipPrefixClaimStrategy) Canonicalize(_ runtime.Object) {} - -func (ipPrefixClaimStrategy) ValidateUpdate(_ context.Context, obj, old runtime.Object) field.ErrorList { - n := obj.(*ipam.IPPrefixClaim) - o := old.(*ipam.IPPrefixClaim) - allErrs := validateIPPrefixClaim(n) - - if n.Spec.IPFamily != o.Spec.IPFamily { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ipFamily"), "ipFamily is immutable")) - } - if n.Spec.PrefixLength != o.Spec.PrefixLength { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "prefixLength"), "prefixLength is immutable")) - } - if !equality.Semantic.DeepEqual(n.Spec.PrefixRef, o.Spec.PrefixRef) { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "prefixRef"), "prefixRef is immutable")) - } - if !equality.Semantic.DeepEqual(n.Spec.PrefixSelector, o.Spec.PrefixSelector) { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "prefixSelector"), "prefixSelector is immutable")) - } - return allErrs -} - -func (ipPrefixClaimStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func validateIPPrefixClaim(c *ipam.IPPrefixClaim) field.ErrorList { - var allErrs field.ErrorList - specPath := field.NewPath("spec") - - if c.Spec.IPFamily == "" { - allErrs = append(allErrs, field.Required(specPath.Child("ipFamily"), "ipFamily is required")) - } else if c.Spec.IPFamily != ipam.IPv4 && c.Spec.IPFamily != ipam.IPv6 { - allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), c.Spec.IPFamily, []string{string(ipam.IPv4), string(ipam.IPv6)})) - } - if c.Spec.PrefixLength <= 0 { - allErrs = append(allErrs, field.Invalid(specPath.Child("prefixLength"), c.Spec.PrefixLength, "prefixLength must be greater than 0")) - } - maxLen := 32 - if c.Spec.IPFamily == ipam.IPv6 { - maxLen = 128 - } - if c.Spec.PrefixLength > maxLen { - allErrs = append(allErrs, field.Invalid(specPath.Child("prefixLength"), c.Spec.PrefixLength, fmt.Sprintf("prefixLength must not exceed %d for %s", maxLen, c.Spec.IPFamily))) - } - if c.Spec.PrefixRef == nil && c.Spec.PrefixSelector == nil { - allErrs = append(allErrs, field.Required(specPath, "exactly one of prefixRef or prefixSelector must be specified")) - } - if c.Spec.PrefixRef != nil && c.Spec.PrefixSelector != nil { - allErrs = append(allErrs, field.Forbidden(specPath, "prefixRef and prefixSelector are mutually exclusive")) - } - return allErrs -} - -func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { - c, ok := obj.(*ipam.IPPrefixClaim) - if !ok { - return nil, nil, fmt.Errorf("given object is not an IPPrefixClaim") - } - return c.Labels, SelectableFields(c), nil -} - -func SelectableFields(c *ipam.IPPrefixClaim) fields.Set { - objectMetaFields := generic.ObjectMetaFieldsSet(&c.ObjectMeta, true) - // spec.prefixRef.name lets clients filter watches/lists by the - // targeted pool — useful for operator dashboards and "what claims - // reference this pool" queries. Empty when the claim used a - // prefixSelector instead, which is the right behavior (no fixed - // pool to filter by). - prefixRefName := "" - if c.Spec.PrefixRef != nil { - prefixRefName = c.Spec.PrefixRef.Name - } - specific := fields.Set{ - "spec.ipFamily": string(c.Spec.IPFamily), - "spec.prefixRef.name": prefixRefName, - } - return generic.MergeFieldsSets(objectMetaFields, specific) -} - -func MatchIPPrefixClaim(label labels.Selector, fld fields.Selector) storage.SelectionPredicate { - return storage.SelectionPredicate{Label: label, Field: fld, GetAttrs: GetAttrs} -} - -func (ipPrefixClaimStatusStrategy) NamespaceScoped() bool { return true } - -func (ipPrefixClaimStatusStrategy) PrepareForUpdate(_ context.Context, obj, old runtime.Object) { - n := obj.(*ipam.IPPrefixClaim) - o := old.(*ipam.IPPrefixClaim) - n.Spec = o.Spec - n.Labels = o.Labels - n.Annotations = o.Annotations -} - -func (ipPrefixClaimStatusStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { - return nil -} - -func (ipPrefixClaimStatusStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { - return nil -} - -func (ipPrefixClaimStatusStrategy) AllowCreateOnUpdate() bool { return false } -func (ipPrefixClaimStatusStrategy) AllowUnconditionalUpdate() bool { return true } -func (ipPrefixClaimStatusStrategy) Canonicalize(_ runtime.Object) {} - -func (ipPrefixClaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { - return map[fieldpath.APIVersion]*fieldpath.Set{ - "ipam.miloapis.com/v1alpha1": fieldpath.NewSet(fieldpath.MakePathOrDie("spec")), - } -} diff --git a/internal/watch/postgres.go b/internal/watch/postgres.go index 8d20390..0eb0e3a 100644 --- a/internal/watch/postgres.go +++ b/internal/watch/postgres.go @@ -553,7 +553,7 @@ func (w *postgresWatch) poll(ctx context.Context) { if rv > uint64(w.lastRV) { w.lastRV = int64(rv) } - w.sendBookmarkAt(uint64(w.lastRV)) + w.sendBookmarkBlocking(uint64(w.lastRV)) case <-w.kick: // LISTEN/NOTIFY push: a Postgres notification told us // there's new data. Wait briefly so any additional kicks @@ -696,14 +696,16 @@ func (w *postgresWatch) sendInitialEventList(ctx context.Context) error { } listArgs := []any{w.key, keyPrefix + "%"} - listQuery := `SELECT key, resource_version, data + var listQueryBuilder strings.Builder + listQueryBuilder.WriteString(`SELECT key, resource_version, data FROM ipam_objects - WHERE (key = $1 OR key LIKE $2)` + WHERE (key = $1 OR key LIKE $2)`) for _, excl := range w.excludedKeyPrefixes { listArgs = append(listArgs, excl+"%") - listQuery += fmt.Sprintf(" AND key NOT LIKE $%d", len(listArgs)) + fmt.Fprintf(&listQueryBuilder, " AND key NOT LIKE $%d", len(listArgs)) } - listQuery += " ORDER BY resource_version ASC" + listQueryBuilder.WriteString(" ORDER BY resource_version ASC") + listQuery := listQueryBuilder.String() rows, err := tx.QueryContext(ctx, listQuery, listArgs...) if err != nil { @@ -890,11 +892,14 @@ func (w *postgresWatch) pollChanges(ctx context.Context) (int, error) { WHERE commit_xid < $1 AND (commit_xid > $2 OR (commit_xid = $2 AND id > $3)) AND (key = $4 OR key LIKE $5)` + var queryBuilder strings.Builder + queryBuilder.WriteString(query) for _, excl := range w.excludedKeyPrefixes { args = append(args, excl+"%") - query += fmt.Sprintf(" AND key NOT LIKE $%d", len(args)) + fmt.Fprintf(&queryBuilder, " AND key NOT LIKE $%d", len(args)) } - query += fmt.Sprintf(" ORDER BY commit_xid ASC, id ASC LIMIT %d", pollBatchSize) + fmt.Fprintf(&queryBuilder, " ORDER BY commit_xid ASC, id ASC LIMIT %d", pollBatchSize) + query = queryBuilder.String() rows, err := w.db.QueryContext(ctx, query, args...) if err != nil { @@ -1069,8 +1074,9 @@ func (w *postgresWatch) sendBookmark() { } // sendBookmarkAt emits a bookmark event with the supplied resource version. -// Used both by the periodic bookmark ticker and by RequestWatchProgress to -// signal "the storage is at least at this RV". +// Used by the periodic 30-second bookmark ticker where dropping an occasional +// bookmark is harmless — if the channel is full the ticker will fire again +// shortly and deliver the next one. func (w *postgresWatch) sendBookmarkAt(rv uint64) { if w.newFunc == nil { return @@ -1089,6 +1095,41 @@ func (w *postgresWatch) sendBookmarkAt(rv uint64) { } case <-w.done: default: - // Channel full — caller will retry + // Channel full — the periodic ticker will deliver the next bookmark soon. + } +} + +// sendBookmarkBlocking emits a bookmark event with the supplied resource +// version, blocking until the event is delivered or the watch is stopped. +// +// This MUST be used on the RequestWatchProgress path (the progress channel +// handler) rather than sendBookmarkAt. The cacher's +// waitUntilWatchCacheFreshAndForceAllEvents blocks for up to 3 seconds +// waiting for a bookmark at the requested RV; if the bookmark is dropped +// because the result channel is momentarily full, the cacher never unblocks +// and the WatchList request (kubectl v1.35+ `--for=condition=Ready`) returns +// TooLargeResourceVersionError, causing kubectl to retry indefinitely. +// +// Blocking here is safe: the ConditionalProgressRequester fires at most every +// 100 ms, the result channel is 100-deep, and the cacher drains it faster than +// any realistic write burst, so the actual wait is sub-millisecond in practice. +func (w *postgresWatch) sendBookmarkBlocking(rv uint64) { + if w.newFunc == nil { + return + } + obj := w.newFunc() + versioner := storage.APIObjectVersioner{} + if err := versioner.UpdateObject(obj, rv); err != nil { + klog.ErrorS(err, "Failed to set resource version on bookmark object") + return + } + event := watch.Event{Type: watch.Bookmark, Object: obj} + select { + case w.result <- event: + if int64(rv) > w.lastRV { + w.lastRV = int64(rv) + } + case <-w.done: + // Watch stopped — nothing to do. } } diff --git a/migrations/001_initial_schema.sql b/migrations/001_initial_schema.sql index 4c541c1..65dd6c6 100644 --- a/migrations/001_initial_schema.sql +++ b/migrations/001_initial_schema.sql @@ -41,20 +41,46 @@ CREATE INDEX IF NOT EXISTS idx_ipam_objects_key_prefix ON ipam_objects (key text -- checks used in label-selector pushdown. CREATE INDEX IF NOT EXISTS idx_ipam_objects_labels ON ipam_objects USING gin(labels jsonb_path_ops); -CREATE TABLE IF NOT EXISTS ipam_prefix_allocations ( +-- Kind-scoped expression indexes for IPPool. +CREATE INDEX IF NOT EXISTS idx_ipam_ippool_ip_family + ON ipam_objects ((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) + WHERE kind = 'IPPool'; + +CREATE INDEX IF NOT EXISTS idx_ipam_ippool_parent_pool_ref_name + ON ipam_objects ((ipam_data_to_jsonb(data) -> 'spec' -> 'parentPoolRef' ->> 'name')) + WHERE kind = 'IPPool'; + +-- Kind-scoped expression indexes for IPAllocation. +CREATE INDEX IF NOT EXISTS idx_ipam_ipallocation_ip_family + ON ipam_objects ((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) + WHERE kind = 'IPAllocation'; + +CREATE INDEX IF NOT EXISTS idx_ipam_ipallocation_pool_ref_name + ON ipam_objects ((ipam_data_to_jsonb(data) -> 'spec' -> 'poolRef' ->> 'name')) + WHERE kind = 'IPAllocation'; + +-- Kind-scoped expression indexes for IPClaim. +CREATE INDEX IF NOT EXISTS idx_ipam_ipclaim_ip_family + ON ipam_objects ((ipam_data_to_jsonb(data) -> 'spec' ->> 'ipFamily')) + WHERE kind = 'IPClaim'; + +CREATE INDEX IF NOT EXISTS idx_ipam_ipclaim_pool_ref_name + ON ipam_objects ((ipam_data_to_jsonb(data) -> 'spec' -> 'poolRef' ->> 'name')) + WHERE kind = 'IPClaim'; + +CREATE TABLE IF NOT EXISTS ipam_cidr_allocations ( id BIGSERIAL PRIMARY KEY, pool_key TEXT NOT NULL REFERENCES ipam_objects (key) ON DELETE RESTRICT, allocated_cidr CIDR NOT NULL, claim_key TEXT NOT NULL UNIQUE, ip_family TEXT NOT NULL CHECK (ip_family IN ('IPv4', 'IPv6')), - is_child_pool BOOLEAN NOT NULL DEFAULT FALSE, reclaim_policy TEXT NOT NULL DEFAULT 'Delete', owner_project TEXT NOT NULL DEFAULT '', allocated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_ipam_prefix_alloc_pool ON ipam_prefix_allocations (pool_key); -CREATE INDEX IF NOT EXISTS idx_ipam_prefix_alloc_project ON ipam_prefix_allocations (owner_project); +CREATE INDEX IF NOT EXISTS idx_ipam_cidr_alloc_pool ON ipam_cidr_allocations (pool_key); +CREATE INDEX IF NOT EXISTS idx_ipam_cidr_alloc_project ON ipam_cidr_allocations (owner_project); CREATE TABLE IF NOT EXISTS ipam_asn_allocations ( id BIGSERIAL PRIMARY KEY, @@ -107,7 +133,7 @@ DROP TRIGGER IF EXISTS ipam_changelog_notify ON ipam_changelog; DROP FUNCTION IF EXISTS ipam_notify_changelog(); DROP TABLE IF EXISTS ipam_changelog; DROP TABLE IF EXISTS ipam_asn_allocations; -DROP TABLE IF EXISTS ipam_prefix_allocations; +DROP TABLE IF EXISTS ipam_cidr_allocations; DROP TABLE IF EXISTS ipam_objects; DROP FUNCTION IF EXISTS ipam_data_to_jsonb(bytea); DROP SEQUENCE IF EXISTS ipam_resource_version_seq; diff --git a/pkg/apis/ipam/protobuf.go b/pkg/apis/ipam/protobuf.go index 3890379..7afabd6 100644 --- a/pkg/apis/ipam/protobuf.go +++ b/pkg/apis/ipam/protobuf.go @@ -5,38 +5,23 @@ package ipam import "encoding/json" -// --- IPPrefixClass --- +// --- IPPool --- -func (in *IPPrefixClass) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClass) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPPrefixClassList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClassList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPPool) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPPool) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPPoolList) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPPoolList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -// --- IPPrefix --- +// --- IPAllocation --- -func (in *IPPrefix) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefix) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPPrefixList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPAllocation) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPAllocation) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPAllocationList) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPAllocationList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -// --- IPPrefixClaim --- - -func (in *IPPrefixClaim) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClaim) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPPrefixClaimList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClaimList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } - -// --- IPAddress --- - -func (in *IPAddress) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddress) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPAddressList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddressList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } - -// --- IPAddressClaim --- - -func (in *IPAddressClaim) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddressClaim) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPAddressClaimList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddressClaimList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +// --- IPClaim --- +func (in *IPClaim) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPClaim) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPClaimList) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPClaimList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } diff --git a/pkg/apis/ipam/register.go b/pkg/apis/ipam/register.go index 9bf97ef..de676d7 100644 --- a/pkg/apis/ipam/register.go +++ b/pkg/apis/ipam/register.go @@ -31,11 +31,9 @@ func Resource(resource string) schema.GroupResource { func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &IPPrefixClass{}, &IPPrefixClassList{}, - &IPPrefix{}, &IPPrefixList{}, - &IPPrefixClaim{}, &IPPrefixClaimList{}, - &IPAddress{}, &IPAddressList{}, - &IPAddressClaim{}, &IPAddressClaimList{}, + &IPPool{}, &IPPoolList{}, + &IPAllocation{}, &IPAllocationList{}, + &IPClaim{}, &IPClaimList{}, ) return nil } diff --git a/pkg/apis/ipam/types.go b/pkg/apis/ipam/types.go index d3839fa..e2d9d72 100644 --- a/pkg/apis/ipam/types.go +++ b/pkg/apis/ipam/types.go @@ -38,14 +38,24 @@ const ( ClaimError ClaimPhase = "Error" ) -// PrefixPhase is the high-level lifecycle phase of an IP prefix. -type PrefixPhase string +// AllocationPhase is the high-level lifecycle phase of an IPAllocation. +type AllocationPhase string const ( - PrefixPending PrefixPhase = "Pending" - PrefixReady PrefixPhase = "Ready" - PrefixExhausted PrefixPhase = "Exhausted" - PrefixError PrefixPhase = "Error" + AllocationPending AllocationPhase = "Pending" + AllocationReady AllocationPhase = "Ready" + AllocationExhausted AllocationPhase = "Exhausted" + AllocationError AllocationPhase = "Error" +) + +// PoolPhase is the high-level lifecycle phase of an IPPool. +type PoolPhase string + +const ( + PoolPending PoolPhase = "Pending" + PoolReady PoolPhase = "Ready" + PoolExhausted PoolPhase = "Exhausted" + PoolError PoolPhase = "Error" ) // LocalRef references another IPAM object in the same namespace by name. @@ -61,9 +71,9 @@ type NamespacedRef struct { ProjectRef *LocalRef } -// PrefixSelector picks a parent IPPrefix by labels, optionally scoped to a +// PoolSelector picks a parent IPPool by labels, optionally scoped to a // specific project for cross-project shared pools. -type PrefixSelector struct { +type PoolSelector struct { *metav1.LabelSelector ProjectRef *LocalRef } @@ -77,204 +87,129 @@ type ObjectRef struct { Name string } -// AllocationSpec configures sub-allocation behaviour for a prefix. +// AllocationSpec configures sub-allocation behaviour for a pool. type AllocationSpec struct { MinPrefixLength int MaxPrefixLength int Strategy Strategy } -// PrefixCapacity reports utilization for an IPPrefix. -type PrefixCapacity struct { +// PoolCapacity reports utilization for an IPPool. +type PoolCapacity struct { Total int64 Allocated int64 Available int64 } -// IPPrefixTemplate is the metadata + spec used to materialise an IPPrefix -// child created atomically with an IPPrefixClaim. -type IPPrefixTemplate struct { - Metadata metav1.ObjectMeta - Spec IPPrefixSpec -} - // ---------------------------------------------------------------------------- -// IPPrefixClass — cluster-scoped class of prefix pools. +// IPPool — cluster-scoped allocatable address space. // ---------------------------------------------------------------------------- // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +genclient // +genclient:nonNamespaced -// IPPrefixClass declares operational properties shared by a class of -// IPPrefix pools. -type IPPrefixClass struct { +type IPPool struct { metav1.TypeMeta metav1.ObjectMeta - Spec IPPrefixClassSpec + Spec IPPoolSpec + Status IPPoolStatus } -type IPPrefixClassSpec struct { - Visibility string - DefaultAllocation AllocationSpec +type IPPoolSpec struct { + CIDR string + IPFamily IPFamily + ParentPoolRef *LocalRef + PrefixLength int + Allocation AllocationSpec + Visibility string } -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -type IPPrefixClassList struct { - metav1.TypeMeta - metav1.ListMeta - Items []IPPrefixClass +type IPPoolStatus struct { + Phase PoolPhase + AllocatedCIDR string + Capacity PoolCapacity + Conditions []metav1.Condition } -// ---------------------------------------------------------------------------- -// IPPrefix — the prefix pool itself. -// ---------------------------------------------------------------------------- - // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +genclient -type IPPrefix struct { - metav1.TypeMeta - metav1.ObjectMeta - - Spec IPPrefixSpec - Status IPPrefixStatus -} - -type IPPrefixSpec struct { - CIDR string - IPFamily IPFamily - ClassRef LocalRef - Allocation AllocationSpec - ParentRef *ObjectRef -} - -type IPPrefixStatus struct { - Phase PrefixPhase - CIDR string - Capacity PrefixCapacity - Conditions []metav1.Condition -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -type IPPrefixList struct { +type IPPoolList struct { metav1.TypeMeta metav1.ListMeta - Items []IPPrefix + Items []IPPool } // ---------------------------------------------------------------------------- -// IPPrefixClaim +// IPAllocation — namespaced, system-created allocation record. // ---------------------------------------------------------------------------- // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +genclient -type IPPrefixClaim struct { +type IPAllocation struct { metav1.TypeMeta metav1.ObjectMeta - Spec IPPrefixClaimSpec - Status IPPrefixClaimStatus + Spec IPAllocationSpec + Status IPAllocationStatus } -type IPPrefixClaimSpec struct { - IPFamily IPFamily - PrefixLength int - PrefixSelector *PrefixSelector - PrefixRef *NamespacedRef - ChildPrefixTemplate *IPPrefixTemplate - ReclaimPolicy ReclaimPolicy - OwnerRef *ObjectRef +type IPAllocationSpec struct { + IPFamily IPFamily + PoolRef LocalRef } -type IPPrefixClaimStatus struct { - Phase ClaimPhase - AllocatedCIDR string - BoundPrefixRef *LocalRef - Conditions []metav1.Condition +type IPAllocationStatus struct { + Phase AllocationPhase + AllocatedCIDR string + Conditions []metav1.Condition } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPPrefixClaimList struct { +type IPAllocationList struct { metav1.TypeMeta metav1.ListMeta - Items []IPPrefixClaim + Items []IPAllocation } // ---------------------------------------------------------------------------- -// IPAddress +// IPClaim // ---------------------------------------------------------------------------- // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +genclient -type IPAddress struct { +type IPClaim struct { metav1.TypeMeta metav1.ObjectMeta - Spec IPAddressSpec - Status IPAddressStatus + Spec IPClaimSpec + Status IPClaimStatus } -type IPAddressSpec struct { - Address string - IPFamily IPFamily - PrefixRef LocalRef - ClaimRef *LocalRef +type IPClaimSpec struct { + IPFamily IPFamily + PrefixLength int + PoolSelector *PoolSelector + PoolRef *NamespacedRef + ReclaimPolicy ReclaimPolicy + OwnerRef *ObjectRef } -type IPAddressStatus struct { - Conditions []metav1.Condition +type IPClaimStatus struct { + Phase ClaimPhase + AllocatedCIDR string + BoundAllocationRef *LocalRef + Conditions []metav1.Condition } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPAddressList struct { +type IPClaimList struct { metav1.TypeMeta metav1.ListMeta - Items []IPAddress -} - -// ---------------------------------------------------------------------------- -// IPAddressClaim -// ---------------------------------------------------------------------------- - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +genclient - -type IPAddressClaim struct { - metav1.TypeMeta - metav1.ObjectMeta - - Spec IPAddressClaimSpec - Status IPAddressClaimStatus + Items []IPClaim } - -type IPAddressClaimSpec struct { - IPFamily IPFamily - PrefixSelector *PrefixSelector - PrefixRef *NamespacedRef - ReclaimPolicy ReclaimPolicy - OwnerRef *ObjectRef -} - -type IPAddressClaimStatus struct { - Phase ClaimPhase - AllocatedIP string - BoundAddressRef *LocalRef - Conditions []metav1.Condition -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -type IPAddressClaimList struct { - metav1.TypeMeta - metav1.ListMeta - Items []IPAddressClaim -} - diff --git a/pkg/apis/ipam/v1alpha1/conversion.go b/pkg/apis/ipam/v1alpha1/conversion.go index 821cd1a..65915b7 100644 --- a/pkg/apis/ipam/v1alpha1/conversion.go +++ b/pkg/apis/ipam/v1alpha1/conversion.go @@ -13,98 +13,62 @@ import ( // named type, so conversion is a series of mechanical field copies. func RegisterConversions(s *runtime.Scheme) error { pairs := []struct { - internal, external interface{} + internal, external any toInternal conversion.ConversionFunc toExternal conversion.ConversionFunc }{ { - (*ipam.IPPrefixClass)(nil), (*IPPrefixClass)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPPrefixClass_To_ipam(a.(*IPPrefixClass), b.(*ipam.IPPrefixClass)) + (*ipam.IPPool)(nil), (*IPPool)(nil), + func(a, b any, sc conversion.Scope) error { + return convert_v1alpha1_IPPool_To_ipam(a.(*IPPool), b.(*ipam.IPPool)) }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPPrefixClass_To_v1alpha1(a.(*ipam.IPPrefixClass), b.(*IPPrefixClass)) + func(a, b any, sc conversion.Scope) error { + return convert_ipam_IPPool_To_v1alpha1(a.(*ipam.IPPool), b.(*IPPool)) }, }, { - (*ipam.IPPrefixClassList)(nil), (*IPPrefixClassList)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPPrefixClassList_To_ipam(a.(*IPPrefixClassList), b.(*ipam.IPPrefixClassList)) + (*ipam.IPPoolList)(nil), (*IPPoolList)(nil), + func(a, b any, sc conversion.Scope) error { + return convert_v1alpha1_IPPoolList_To_ipam(a.(*IPPoolList), b.(*ipam.IPPoolList)) }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPPrefixClassList_To_v1alpha1(a.(*ipam.IPPrefixClassList), b.(*IPPrefixClassList)) + func(a, b any, sc conversion.Scope) error { + return convert_ipam_IPPoolList_To_v1alpha1(a.(*ipam.IPPoolList), b.(*IPPoolList)) }, }, { - (*ipam.IPPrefix)(nil), (*IPPrefix)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPPrefix_To_ipam(a.(*IPPrefix), b.(*ipam.IPPrefix)) + (*ipam.IPAllocation)(nil), (*IPAllocation)(nil), + func(a, b any, sc conversion.Scope) error { + return convert_v1alpha1_IPAllocation_To_ipam(a.(*IPAllocation), b.(*ipam.IPAllocation)) }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPPrefix_To_v1alpha1(a.(*ipam.IPPrefix), b.(*IPPrefix)) + func(a, b any, sc conversion.Scope) error { + return convert_ipam_IPAllocation_To_v1alpha1(a.(*ipam.IPAllocation), b.(*IPAllocation)) }, }, { - (*ipam.IPPrefixList)(nil), (*IPPrefixList)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPPrefixList_To_ipam(a.(*IPPrefixList), b.(*ipam.IPPrefixList)) + (*ipam.IPAllocationList)(nil), (*IPAllocationList)(nil), + func(a, b any, sc conversion.Scope) error { + return convert_v1alpha1_IPAllocationList_To_ipam(a.(*IPAllocationList), b.(*ipam.IPAllocationList)) }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPPrefixList_To_v1alpha1(a.(*ipam.IPPrefixList), b.(*IPPrefixList)) + func(a, b any, sc conversion.Scope) error { + return convert_ipam_IPAllocationList_To_v1alpha1(a.(*ipam.IPAllocationList), b.(*IPAllocationList)) }, }, { - (*ipam.IPPrefixClaim)(nil), (*IPPrefixClaim)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPPrefixClaim_To_ipam(a.(*IPPrefixClaim), b.(*ipam.IPPrefixClaim)) + (*ipam.IPClaim)(nil), (*IPClaim)(nil), + func(a, b any, sc conversion.Scope) error { + return convert_v1alpha1_IPClaim_To_ipam(a.(*IPClaim), b.(*ipam.IPClaim)) }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPPrefixClaim_To_v1alpha1(a.(*ipam.IPPrefixClaim), b.(*IPPrefixClaim)) + func(a, b any, sc conversion.Scope) error { + return convert_ipam_IPClaim_To_v1alpha1(a.(*ipam.IPClaim), b.(*IPClaim)) }, }, { - (*ipam.IPPrefixClaimList)(nil), (*IPPrefixClaimList)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPPrefixClaimList_To_ipam(a.(*IPPrefixClaimList), b.(*ipam.IPPrefixClaimList)) + (*ipam.IPClaimList)(nil), (*IPClaimList)(nil), + func(a, b any, sc conversion.Scope) error { + return convert_v1alpha1_IPClaimList_To_ipam(a.(*IPClaimList), b.(*ipam.IPClaimList)) }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPPrefixClaimList_To_v1alpha1(a.(*ipam.IPPrefixClaimList), b.(*IPPrefixClaimList)) - }, - }, - { - (*ipam.IPAddress)(nil), (*IPAddress)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPAddress_To_ipam(a.(*IPAddress), b.(*ipam.IPAddress)) - }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPAddress_To_v1alpha1(a.(*ipam.IPAddress), b.(*IPAddress)) - }, - }, - { - (*ipam.IPAddressList)(nil), (*IPAddressList)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPAddressList_To_ipam(a.(*IPAddressList), b.(*ipam.IPAddressList)) - }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPAddressList_To_v1alpha1(a.(*ipam.IPAddressList), b.(*IPAddressList)) - }, - }, - { - (*ipam.IPAddressClaim)(nil), (*IPAddressClaim)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPAddressClaim_To_ipam(a.(*IPAddressClaim), b.(*ipam.IPAddressClaim)) - }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPAddressClaim_To_v1alpha1(a.(*ipam.IPAddressClaim), b.(*IPAddressClaim)) - }, - }, - { - (*ipam.IPAddressClaimList)(nil), (*IPAddressClaimList)(nil), - func(a, b interface{}, sc conversion.Scope) error { - return convert_v1alpha1_IPAddressClaimList_To_ipam(a.(*IPAddressClaimList), b.(*ipam.IPAddressClaimList)) - }, - func(a, b interface{}, sc conversion.Scope) error { - return convert_ipam_IPAddressClaimList_To_v1alpha1(a.(*ipam.IPAddressClaimList), b.(*IPAddressClaimList)) + func(a, b any, sc conversion.Scope) error { + return convert_ipam_IPClaimList_To_v1alpha1(a.(*ipam.IPClaimList), b.(*IPClaimList)) }, }, } diff --git a/pkg/apis/ipam/v1alpha1/conversion_impl.go b/pkg/apis/ipam/v1alpha1/conversion_impl.go index ab1d84a..f3cc323 100644 --- a/pkg/apis/ipam/v1alpha1/conversion_impl.go +++ b/pkg/apis/ipam/v1alpha1/conversion_impl.go @@ -42,20 +42,20 @@ func toV1NamespacedRef(in *ipam.NamespacedRef) *NamespacedRef { } } -func toIpamPrefixSelector(in *PrefixSelector) *ipam.PrefixSelector { +func toIpamPoolSelector(in *PoolSelector) *ipam.PoolSelector { if in == nil { return nil } - return &ipam.PrefixSelector{ + return &ipam.PoolSelector{ LabelSelector: in.LabelSelector.DeepCopy(), ProjectRef: toIpamLocalRef(in.ProjectRef), } } -func toV1PrefixSelector(in *ipam.PrefixSelector) *PrefixSelector { +func toV1PoolSelector(in *ipam.PoolSelector) *PoolSelector { if in == nil { return nil } - return &PrefixSelector{ + return &PoolSelector{ LabelSelector: in.LabelSelector.DeepCopy(), ProjectRef: toV1LocalRef(in.ProjectRef), } @@ -108,104 +108,69 @@ func toIpamConditions(in []metav1.Condition) []metav1.Condition { return out } -func toIpamIPPrefixSpec(in *IPPrefixSpec) ipam.IPPrefixSpec { - return ipam.IPPrefixSpec{ - CIDR: in.CIDR, - IPFamily: ipam.IPFamily(in.IPFamily), - ClassRef: ipam.LocalRef{Name: in.ClassRef.Name}, - Allocation: toIpamAllocation(in.Allocation), - ParentRef: toIpamObjectRef(in.ParentRef), - } -} -func toV1IPPrefixSpec(in *ipam.IPPrefixSpec) IPPrefixSpec { - return IPPrefixSpec{ - CIDR: in.CIDR, - IPFamily: IPFamily(in.IPFamily), - ClassRef: LocalRef{Name: in.ClassRef.Name}, - Allocation: toV1Allocation(in.Allocation), - ParentRef: toV1ObjectRef(in.ParentRef), - } -} - -func toIpamIPPrefixStatus(in *IPPrefixStatus) ipam.IPPrefixStatus { - return ipam.IPPrefixStatus{ - Phase: ipam.PrefixPhase(in.Phase), - CIDR: in.CIDR, - Capacity: ipam.PrefixCapacity(in.Capacity), - Conditions: toIpamConditions(in.Conditions), - } -} -func toV1IPPrefixStatus(in *ipam.IPPrefixStatus) IPPrefixStatus { - return IPPrefixStatus{ - Phase: PrefixPhase(in.Phase), - CIDR: in.CIDR, - Capacity: PrefixCapacity(in.Capacity), - Conditions: toIpamConditions(in.Conditions), - } -} - -func toIpamPrefixTemplate(in *IPPrefixTemplate) *ipam.IPPrefixTemplate { - if in == nil { - return nil - } - return &ipam.IPPrefixTemplate{ - Metadata: *in.Metadata.DeepCopy(), - Spec: toIpamIPPrefixSpec(&in.Spec), - } -} -func toV1PrefixTemplate(in *ipam.IPPrefixTemplate) *IPPrefixTemplate { - if in == nil { - return nil - } - return &IPPrefixTemplate{ - Metadata: *in.Metadata.DeepCopy(), - Spec: toV1IPPrefixSpec(&in.Spec), - } -} - // ---------------------------------------------------------------------------- -// IPPrefixClass +// IPPool // ---------------------------------------------------------------------------- -func convert_v1alpha1_IPPrefixClass_To_ipam(in *IPPrefixClass, out *ipam.IPPrefixClass) error { +func convert_v1alpha1_IPPool_To_ipam(in *IPPool, out *ipam.IPPool) error { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = ipam.IPPrefixClassSpec{ - Visibility: in.Spec.Visibility, - DefaultAllocation: toIpamAllocation(in.Spec.DefaultAllocation), + out.Spec = ipam.IPPoolSpec{ + CIDR: in.Spec.CIDR, + IPFamily: ipam.IPFamily(in.Spec.IPFamily), + ParentPoolRef: toIpamLocalRef(in.Spec.ParentPoolRef), + PrefixLength: in.Spec.PrefixLength, + Allocation: toIpamAllocation(in.Spec.Allocation), + Visibility: in.Spec.Visibility, + } + out.Status = ipam.IPPoolStatus{ + Phase: ipam.PoolPhase(in.Status.Phase), + AllocatedCIDR: in.Status.AllocatedCIDR, + Capacity: ipam.PoolCapacity(in.Status.Capacity), + Conditions: toIpamConditions(in.Status.Conditions), } return nil } -func convert_ipam_IPPrefixClass_To_v1alpha1(in *ipam.IPPrefixClass, out *IPPrefixClass) error { +func convert_ipam_IPPool_To_v1alpha1(in *ipam.IPPool, out *IPPool) error { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = IPPrefixClassSpec{ - Visibility: in.Spec.Visibility, - DefaultAllocation: toV1Allocation(in.Spec.DefaultAllocation), + out.Spec = IPPoolSpec{ + CIDR: in.Spec.CIDR, + IPFamily: IPFamily(in.Spec.IPFamily), + ParentPoolRef: toV1LocalRef(in.Spec.ParentPoolRef), + PrefixLength: in.Spec.PrefixLength, + Allocation: toV1Allocation(in.Spec.Allocation), + Visibility: in.Spec.Visibility, + } + out.Status = IPPoolStatus{ + Phase: PoolPhase(in.Status.Phase), + AllocatedCIDR: in.Status.AllocatedCIDR, + Capacity: PoolCapacity(in.Status.Capacity), + Conditions: toIpamConditions(in.Status.Conditions), } return nil } -func convert_v1alpha1_IPPrefixClassList_To_ipam(in *IPPrefixClassList, out *ipam.IPPrefixClassList) error { +func convert_v1alpha1_IPPoolList_To_ipam(in *IPPoolList, out *ipam.IPPoolList) error { out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { - out.Items = make([]ipam.IPPrefixClass, len(in.Items)) + out.Items = make([]ipam.IPPool, len(in.Items)) for i := range in.Items { - if err := convert_v1alpha1_IPPrefixClass_To_ipam(&in.Items[i], &out.Items[i]); err != nil { + if err := convert_v1alpha1_IPPool_To_ipam(&in.Items[i], &out.Items[i]); err != nil { return err } } } return nil } -func convert_ipam_IPPrefixClassList_To_v1alpha1(in *ipam.IPPrefixClassList, out *IPPrefixClassList) error { +func convert_ipam_IPPoolList_To_v1alpha1(in *ipam.IPPoolList, out *IPPoolList) error { out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { - out.Items = make([]IPPrefixClass, len(in.Items)) + out.Items = make([]IPPool, len(in.Items)) for i := range in.Items { - if err := convert_ipam_IPPrefixClass_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { + if err := convert_ipam_IPPool_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { return err } } @@ -214,172 +179,58 @@ func convert_ipam_IPPrefixClassList_To_v1alpha1(in *ipam.IPPrefixClassList, out } // ---------------------------------------------------------------------------- -// IPPrefix +// IPAllocation // ---------------------------------------------------------------------------- -func convert_v1alpha1_IPPrefix_To_ipam(in *IPPrefix, out *ipam.IPPrefix) error { +func convert_v1alpha1_IPAllocation_To_ipam(in *IPAllocation, out *ipam.IPAllocation) error { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = toIpamIPPrefixSpec(&in.Spec) - out.Status = toIpamIPPrefixStatus(&in.Status) - return nil -} -func convert_ipam_IPPrefix_To_v1alpha1(in *ipam.IPPrefix, out *IPPrefix) error { - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = toV1IPPrefixSpec(&in.Spec) - out.Status = toV1IPPrefixStatus(&in.Status) - return nil -} - -func convert_v1alpha1_IPPrefixList_To_ipam(in *IPPrefixList, out *ipam.IPPrefixList) error { - out.TypeMeta = in.TypeMeta - out.ListMeta = in.ListMeta - if in.Items != nil { - out.Items = make([]ipam.IPPrefix, len(in.Items)) - for i := range in.Items { - if err := convert_v1alpha1_IPPrefix_To_ipam(&in.Items[i], &out.Items[i]); err != nil { - return err - } - } + out.Spec = ipam.IPAllocationSpec{ + IPFamily: ipam.IPFamily(in.Spec.IPFamily), + PoolRef: ipam.LocalRef{Name: in.Spec.PoolRef.Name}, } - return nil -} -func convert_ipam_IPPrefixList_To_v1alpha1(in *ipam.IPPrefixList, out *IPPrefixList) error { - out.TypeMeta = in.TypeMeta - out.ListMeta = in.ListMeta - if in.Items != nil { - out.Items = make([]IPPrefix, len(in.Items)) - for i := range in.Items { - if err := convert_ipam_IPPrefix_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { - return err - } - } + out.Status = ipam.IPAllocationStatus{ + Phase: ipam.AllocationPhase(in.Status.Phase), + AllocatedCIDR: in.Status.AllocatedCIDR, + Conditions: toIpamConditions(in.Status.Conditions), } return nil } - -// ---------------------------------------------------------------------------- -// IPPrefixClaim -// ---------------------------------------------------------------------------- - -func convert_v1alpha1_IPPrefixClaim_To_ipam(in *IPPrefixClaim, out *ipam.IPPrefixClaim) error { - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = ipam.IPPrefixClaimSpec{ - IPFamily: ipam.IPFamily(in.Spec.IPFamily), - PrefixLength: in.Spec.PrefixLength, - PrefixSelector: toIpamPrefixSelector(in.Spec.PrefixSelector), - PrefixRef: toIpamNamespacedRef(in.Spec.PrefixRef), - ChildPrefixTemplate: toIpamPrefixTemplate(in.Spec.ChildPrefixTemplate), - ReclaimPolicy: ipam.ReclaimPolicy(in.Spec.ReclaimPolicy), - OwnerRef: toIpamObjectRef(in.Spec.OwnerRef), - } - out.Status = ipam.IPPrefixClaimStatus{ - Phase: ipam.ClaimPhase(in.Status.Phase), - AllocatedCIDR: in.Status.AllocatedCIDR, - BoundPrefixRef: toIpamLocalRef(in.Status.BoundPrefixRef), - Conditions: toIpamConditions(in.Status.Conditions), - } - return nil -} -func convert_ipam_IPPrefixClaim_To_v1alpha1(in *ipam.IPPrefixClaim, out *IPPrefixClaim) error { - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = IPPrefixClaimSpec{ - IPFamily: IPFamily(in.Spec.IPFamily), - PrefixLength: in.Spec.PrefixLength, - PrefixSelector: toV1PrefixSelector(in.Spec.PrefixSelector), - PrefixRef: toV1NamespacedRef(in.Spec.PrefixRef), - ChildPrefixTemplate: toV1PrefixTemplate(in.Spec.ChildPrefixTemplate), - ReclaimPolicy: ReclaimPolicy(in.Spec.ReclaimPolicy), - OwnerRef: toV1ObjectRef(in.Spec.OwnerRef), - } - out.Status = IPPrefixClaimStatus{ - Phase: ClaimPhase(in.Status.Phase), - AllocatedCIDR: in.Status.AllocatedCIDR, - BoundPrefixRef: toV1LocalRef(in.Status.BoundPrefixRef), - Conditions: toIpamConditions(in.Status.Conditions), - } - return nil -} - -func convert_v1alpha1_IPPrefixClaimList_To_ipam(in *IPPrefixClaimList, out *ipam.IPPrefixClaimList) error { - out.TypeMeta = in.TypeMeta - out.ListMeta = in.ListMeta - if in.Items != nil { - out.Items = make([]ipam.IPPrefixClaim, len(in.Items)) - for i := range in.Items { - if err := convert_v1alpha1_IPPrefixClaim_To_ipam(&in.Items[i], &out.Items[i]); err != nil { - return err - } - } - } - return nil -} -func convert_ipam_IPPrefixClaimList_To_v1alpha1(in *ipam.IPPrefixClaimList, out *IPPrefixClaimList) error { - out.TypeMeta = in.TypeMeta - out.ListMeta = in.ListMeta - if in.Items != nil { - out.Items = make([]IPPrefixClaim, len(in.Items)) - for i := range in.Items { - if err := convert_ipam_IPPrefixClaim_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { - return err - } - } - } - return nil -} - -// ---------------------------------------------------------------------------- -// IPAddress -// ---------------------------------------------------------------------------- - -func convert_v1alpha1_IPAddress_To_ipam(in *IPAddress, out *ipam.IPAddress) error { +func convert_ipam_IPAllocation_To_v1alpha1(in *ipam.IPAllocation, out *IPAllocation) error { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = ipam.IPAddressSpec{ - Address: in.Spec.Address, - IPFamily: ipam.IPFamily(in.Spec.IPFamily), - PrefixRef: ipam.LocalRef{Name: in.Spec.PrefixRef.Name}, - ClaimRef: toIpamLocalRef(in.Spec.ClaimRef), + out.Spec = IPAllocationSpec{ + IPFamily: IPFamily(in.Spec.IPFamily), + PoolRef: LocalRef{Name: in.Spec.PoolRef.Name}, } - out.Status = ipam.IPAddressStatus{Conditions: toIpamConditions(in.Status.Conditions)} - return nil -} -func convert_ipam_IPAddress_To_v1alpha1(in *ipam.IPAddress, out *IPAddress) error { - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = IPAddressSpec{ - Address: in.Spec.Address, - IPFamily: IPFamily(in.Spec.IPFamily), - PrefixRef: LocalRef{Name: in.Spec.PrefixRef.Name}, - ClaimRef: toV1LocalRef(in.Spec.ClaimRef), + out.Status = IPAllocationStatus{ + Phase: AllocationPhase(in.Status.Phase), + AllocatedCIDR: in.Status.AllocatedCIDR, + Conditions: toIpamConditions(in.Status.Conditions), } - out.Status = IPAddressStatus{Conditions: toIpamConditions(in.Status.Conditions)} return nil } -func convert_v1alpha1_IPAddressList_To_ipam(in *IPAddressList, out *ipam.IPAddressList) error { +func convert_v1alpha1_IPAllocationList_To_ipam(in *IPAllocationList, out *ipam.IPAllocationList) error { out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { - out.Items = make([]ipam.IPAddress, len(in.Items)) + out.Items = make([]ipam.IPAllocation, len(in.Items)) for i := range in.Items { - if err := convert_v1alpha1_IPAddress_To_ipam(&in.Items[i], &out.Items[i]); err != nil { + if err := convert_v1alpha1_IPAllocation_To_ipam(&in.Items[i], &out.Items[i]); err != nil { return err } } } return nil } -func convert_ipam_IPAddressList_To_v1alpha1(in *ipam.IPAddressList, out *IPAddressList) error { +func convert_ipam_IPAllocationList_To_v1alpha1(in *ipam.IPAllocationList, out *IPAllocationList) error { out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { - out.Items = make([]IPAddress, len(in.Items)) + out.Items = make([]IPAllocation, len(in.Items)) for i := range in.Items { - if err := convert_ipam_IPAddress_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { + if err := convert_ipam_IPAllocation_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { return err } } @@ -388,71 +239,71 @@ func convert_ipam_IPAddressList_To_v1alpha1(in *ipam.IPAddressList, out *IPAddre } // ---------------------------------------------------------------------------- -// IPAddressClaim +// IPClaim // ---------------------------------------------------------------------------- -func convert_v1alpha1_IPAddressClaim_To_ipam(in *IPAddressClaim, out *ipam.IPAddressClaim) error { +func convert_v1alpha1_IPClaim_To_ipam(in *IPClaim, out *ipam.IPClaim) error { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = ipam.IPAddressClaimSpec{ - IPFamily: ipam.IPFamily(in.Spec.IPFamily), - PrefixSelector: toIpamPrefixSelector(in.Spec.PrefixSelector), - PrefixRef: toIpamNamespacedRef(in.Spec.PrefixRef), - ReclaimPolicy: ipam.ReclaimPolicy(in.Spec.ReclaimPolicy), - OwnerRef: toIpamObjectRef(in.Spec.OwnerRef), - } - out.Status = ipam.IPAddressClaimStatus{ - Phase: ipam.ClaimPhase(in.Status.Phase), - AllocatedIP: in.Status.AllocatedIP, - BoundAddressRef: toIpamLocalRef(in.Status.BoundAddressRef), - Conditions: toIpamConditions(in.Status.Conditions), + out.Spec = ipam.IPClaimSpec{ + IPFamily: ipam.IPFamily(in.Spec.IPFamily), + PrefixLength: in.Spec.PrefixLength, + PoolSelector: toIpamPoolSelector(in.Spec.PoolSelector), + PoolRef: toIpamNamespacedRef(in.Spec.PoolRef), + ReclaimPolicy: ipam.ReclaimPolicy(in.Spec.ReclaimPolicy), + OwnerRef: toIpamObjectRef(in.Spec.OwnerRef), + } + out.Status = ipam.IPClaimStatus{ + Phase: ipam.ClaimPhase(in.Status.Phase), + AllocatedCIDR: in.Status.AllocatedCIDR, + BoundAllocationRef: toIpamLocalRef(in.Status.BoundAllocationRef), + Conditions: toIpamConditions(in.Status.Conditions), } return nil } -func convert_ipam_IPAddressClaim_To_v1alpha1(in *ipam.IPAddressClaim, out *IPAddressClaim) error { +func convert_ipam_IPClaim_To_v1alpha1(in *ipam.IPClaim, out *IPClaim) error { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = IPAddressClaimSpec{ - IPFamily: IPFamily(in.Spec.IPFamily), - PrefixSelector: toV1PrefixSelector(in.Spec.PrefixSelector), - PrefixRef: toV1NamespacedRef(in.Spec.PrefixRef), - ReclaimPolicy: ReclaimPolicy(in.Spec.ReclaimPolicy), - OwnerRef: toV1ObjectRef(in.Spec.OwnerRef), - } - out.Status = IPAddressClaimStatus{ - Phase: ClaimPhase(in.Status.Phase), - AllocatedIP: in.Status.AllocatedIP, - BoundAddressRef: toV1LocalRef(in.Status.BoundAddressRef), - Conditions: toIpamConditions(in.Status.Conditions), + out.Spec = IPClaimSpec{ + IPFamily: IPFamily(in.Spec.IPFamily), + PrefixLength: in.Spec.PrefixLength, + PoolSelector: toV1PoolSelector(in.Spec.PoolSelector), + PoolRef: toV1NamespacedRef(in.Spec.PoolRef), + ReclaimPolicy: ReclaimPolicy(in.Spec.ReclaimPolicy), + OwnerRef: toV1ObjectRef(in.Spec.OwnerRef), + } + out.Status = IPClaimStatus{ + Phase: ClaimPhase(in.Status.Phase), + AllocatedCIDR: in.Status.AllocatedCIDR, + BoundAllocationRef: toV1LocalRef(in.Status.BoundAllocationRef), + Conditions: toIpamConditions(in.Status.Conditions), } return nil } -func convert_v1alpha1_IPAddressClaimList_To_ipam(in *IPAddressClaimList, out *ipam.IPAddressClaimList) error { +func convert_v1alpha1_IPClaimList_To_ipam(in *IPClaimList, out *ipam.IPClaimList) error { out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { - out.Items = make([]ipam.IPAddressClaim, len(in.Items)) + out.Items = make([]ipam.IPClaim, len(in.Items)) for i := range in.Items { - if err := convert_v1alpha1_IPAddressClaim_To_ipam(&in.Items[i], &out.Items[i]); err != nil { + if err := convert_v1alpha1_IPClaim_To_ipam(&in.Items[i], &out.Items[i]); err != nil { return err } } } return nil } -func convert_ipam_IPAddressClaimList_To_v1alpha1(in *ipam.IPAddressClaimList, out *IPAddressClaimList) error { +func convert_ipam_IPClaimList_To_v1alpha1(in *ipam.IPClaimList, out *IPClaimList) error { out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { - out.Items = make([]IPAddressClaim, len(in.Items)) + out.Items = make([]IPClaim, len(in.Items)) for i := range in.Items { - if err := convert_ipam_IPAddressClaim_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { + if err := convert_ipam_IPClaim_To_v1alpha1(&in.Items[i], &out.Items[i]); err != nil { return err } } } return nil } - - diff --git a/pkg/apis/ipam/v1alpha1/protobuf.go b/pkg/apis/ipam/v1alpha1/protobuf.go index cefe784..19d4d23 100644 --- a/pkg/apis/ipam/v1alpha1/protobuf.go +++ b/pkg/apis/ipam/v1alpha1/protobuf.go @@ -12,38 +12,23 @@ package v1alpha1 import "encoding/json" -// --- IPPrefixClass --- +// --- IPPool --- -func (in *IPPrefixClass) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClass) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPPrefixClassList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClassList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPPool) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPPool) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPPoolList) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPPoolList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -// --- IPPrefix --- +// --- IPAllocation --- -func (in *IPPrefix) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefix) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPPrefixList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPAllocation) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPAllocation) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPAllocationList) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPAllocationList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -// --- IPPrefixClaim --- - -func (in *IPPrefixClaim) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClaim) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPPrefixClaimList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPPrefixClaimList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } - -// --- IPAddress --- - -func (in *IPAddress) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddress) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPAddressList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddressList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } - -// --- IPAddressClaim --- - -func (in *IPAddressClaim) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddressClaim) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } -func (in *IPAddressClaimList) Marshal() ([]byte, error) { return json.Marshal(in) } -func (in *IPAddressClaimList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +// --- IPClaim --- +func (in *IPClaim) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPClaim) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } +func (in *IPClaimList) Marshal() ([]byte, error) { return json.Marshal(in) } +func (in *IPClaimList) Unmarshal(data []byte) error { return json.Unmarshal(data, in) } diff --git a/pkg/apis/ipam/v1alpha1/register.go b/pkg/apis/ipam/v1alpha1/register.go index 74d736e..3edad05 100644 --- a/pkg/apis/ipam/v1alpha1/register.go +++ b/pkg/apis/ipam/v1alpha1/register.go @@ -24,11 +24,9 @@ func Resource(resource string) schema.GroupResource { func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &IPPrefixClass{}, &IPPrefixClassList{}, - &IPPrefix{}, &IPPrefixList{}, - &IPPrefixClaim{}, &IPPrefixClaimList{}, - &IPAddress{}, &IPAddressList{}, - &IPAddressClaim{}, &IPAddressClaimList{}, + &IPPool{}, &IPPoolList{}, + &IPAllocation{}, &IPAllocationList{}, + &IPClaim{}, &IPClaimList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/ipam/v1alpha1/types.go b/pkg/apis/ipam/v1alpha1/types.go index 5b5ba8c..842b699 100644 --- a/pkg/apis/ipam/v1alpha1/types.go +++ b/pkg/apis/ipam/v1alpha1/types.go @@ -42,15 +42,26 @@ const ( ClaimError ClaimPhase = "Error" ) -// PrefixPhase is the high-level lifecycle phase of an IP prefix. +// AllocationPhase is the high-level lifecycle phase of an IPAllocation. // +kubebuilder:validation:Enum=Pending;Ready;Exhausted;Error -type PrefixPhase string +type AllocationPhase string const ( - PrefixPending PrefixPhase = "Pending" - PrefixReady PrefixPhase = "Ready" - PrefixExhausted PrefixPhase = "Exhausted" - PrefixError PrefixPhase = "Error" + AllocationPending AllocationPhase = "Pending" + AllocationReady AllocationPhase = "Ready" + AllocationExhausted AllocationPhase = "Exhausted" + AllocationError AllocationPhase = "Error" +) + +// PoolPhase is the high-level lifecycle phase of an IPPool. +// +kubebuilder:validation:Enum=Pending;Ready;Exhausted;Error +type PoolPhase string + +const ( + PoolPending PoolPhase = "Pending" + PoolReady PoolPhase = "Ready" + PoolExhausted PoolPhase = "Exhausted" + PoolError PoolPhase = "Error" ) // LocalRef references another IPAM object in the same namespace by name. @@ -67,16 +78,16 @@ type NamespacedRef struct { ProjectRef *LocalRef `json:"projectRef,omitempty"` } -// PrefixSelector picks a parent IPPrefix by labels, optionally scoped to a +// PoolSelector picks a parent IPPool by labels, optionally scoped to a // specific project for cross-project shared pools. -type PrefixSelector struct { +type PoolSelector struct { // +optional *metav1.LabelSelector `json:",inline"` // +optional ProjectRef *LocalRef `json:"projectRef,omitempty"` } -// Pool visibility constants for IPPrefixClass.spec.visibility. +// Pool visibility constants for IPPool.spec.visibility. const ( VisibilityPlatform string = "platform" VisibilityConsumer string = "consumer" @@ -91,184 +102,70 @@ type ObjectRef struct { Name string `json:"name"` } -// AllocationSpec configures sub-allocation behaviour for a prefix. +// AllocationSpec configures sub-allocation behaviour for a pool. type AllocationSpec struct { MinPrefixLength int `json:"minPrefixLength,omitempty"` MaxPrefixLength int `json:"maxPrefixLength,omitempty"` Strategy Strategy `json:"strategy,omitempty"` } -// PrefixCapacity reports utilization for an IPPrefix. -type PrefixCapacity struct { +// PoolCapacity reports utilization for an IPPool. +type PoolCapacity struct { Total int64 `json:"total"` Allocated int64 `json:"allocated"` Available int64 `json:"available"` } -// IPPrefixTemplate is the metadata + spec used to materialise an IPPrefix -// child created atomically with an IPPrefixClaim. -type IPPrefixTemplate struct { - Metadata metav1.ObjectMeta `json:"metadata,omitempty"` - Spec IPPrefixSpec `json:"spec"` -} - -// ---------------------------------------------------------------------------- -// IPPrefixClass — cluster-scoped class of prefix pools. -// ---------------------------------------------------------------------------- - -// +kubebuilder:object:root=true -// +kubebuilder:resource:scope=Cluster,shortName=ippc -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Visibility",type=string,JSONPath=`.spec.visibility` -// +kubebuilder:printcolumn:name="ReqVerify",type=boolean,JSONPath=`.spec.requiresVerification` -// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// +genclient -// +genclient:nonNamespaced - -// IPPrefixClass declares operational properties shared by a class of -// IPPrefix pools. -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPPrefixClass struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec IPPrefixClassSpec `json:"spec,omitempty"` -} - -type IPPrefixClassSpec struct { - // Visibility controls cross-project access semantics for IPPrefix - // pools that reference this class. "platform" pools are platform-only - // (callers see them only when running with platform scope); - // "consumer" pools are visible to a single project; "shared" pools - // are eligible for cross-project allocation via prefixSelector.projectRef - // gated by a SubjectAccessReview. - // +optional - // +kubebuilder:validation:Enum=platform;consumer;shared - Visibility string `json:"visibility,omitempty"` - // +optional - DefaultAllocation AllocationSpec `json:"defaultAllocation,omitempty"` -} - -// +kubebuilder:object:root=true - -// IPPrefixClassList is a list of IPPrefixClass. -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPPrefixClassList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []IPPrefixClass `json:"items"` -} - // ---------------------------------------------------------------------------- -// IPPrefix +// IPPool — cluster-scoped allocatable address space. // ---------------------------------------------------------------------------- // +kubebuilder:object:root=true -// +kubebuilder:resource:scope=Cluster,shortName=ipp +// +kubebuilder:resource:scope=Cluster,shortName=ippool // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="CIDR",type=string,JSONPath=`.spec.cidr` -// +kubebuilder:printcolumn:name="Family",type=string,JSONPath=`.spec.ipFamily` -// +kubebuilder:printcolumn:name="Class",type=string,JSONPath=`.spec.classRef.name` +// +kubebuilder:printcolumn:name="CIDR",type=string,JSONPath=`.status.allocatedCIDR` // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Family",type=string,JSONPath=`.spec.ipFamily` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +genclient // +genclient:nonNamespaced -// IPPrefix is a CIDR pool from which sub-prefixes or addresses can be -// allocated. +// IPPool is an allocatable address space. Root pools declare a CIDR +// directly; child pools carve a sub-prefix from a parent pool. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPPrefix struct { +type IPPool struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec IPPrefixSpec `json:"spec,omitempty"` - Status IPPrefixStatus `json:"status,omitempty"` + Spec IPPoolSpec `json:"spec,omitempty"` + Status IPPoolStatus `json:"status,omitempty"` } -type IPPrefixSpec struct { - // CIDR is the parent prefix in canonical form, e.g. "10.0.0.0/8" - // (IPv4) or "2001:db8::/32" (IPv6). Validation parses with - // net.ParseCIDR and rejects malformed values. - CIDR string `json:"cidr"` - IPFamily IPFamily `json:"ipFamily"` - ClassRef LocalRef `json:"classRef"` - // +optional - Allocation AllocationSpec `json:"allocation,omitempty"` - // +optional - ParentRef *ObjectRef `json:"parentRef,omitempty"` -} - -type IPPrefixStatus struct { - // +optional - Phase PrefixPhase `json:"phase,omitempty"` +type IPPoolSpec struct { // +optional CIDR string `json:"cidr,omitempty"` // +optional - Capacity PrefixCapacity `json:"capacity,omitempty"` + IPFamily IPFamily `json:"ipFamily,omitempty"` + // +optional + ParentPoolRef *LocalRef `json:"parentPoolRef,omitempty"` // +optional - // +listType=map - // +listMapKey=type - Conditions []metav1.Condition `json:"conditions,omitempty"` -} - -// +kubebuilder:object:root=true -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPPrefixList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []IPPrefix `json:"items"` -} - -// ---------------------------------------------------------------------------- -// IPPrefixClaim -// ---------------------------------------------------------------------------- - -// +kubebuilder:object:root=true -// +kubebuilder:resource:shortName=ippc -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` -// +kubebuilder:printcolumn:name="CIDR",type=string,JSONPath=`.status.allocatedCIDR` -// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.status.boundPrefixRef.name` -// +kubebuilder:printcolumn:name="Family",type=string,JSONPath=`.spec.ipFamily` -// +kubebuilder:printcolumn:name="Length",type=integer,JSONPath=`.spec.prefixLength` -// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPPrefixClaim struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec IPPrefixClaimSpec `json:"spec,omitempty"` - Status IPPrefixClaimStatus `json:"status,omitempty"` -} - -type IPPrefixClaimSpec struct { - IPFamily IPFamily `json:"ipFamily"` - // PrefixLength is the requested sub-prefix size in bits. Must be a - // valid mask length for the chosen ipFamily (0-32 for IPv4, 0-128 - // for IPv6). // +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=128 - PrefixLength int `json:"prefixLength"` - // +optional - PrefixSelector *PrefixSelector `json:"prefixSelector,omitempty"` - // +optional - PrefixRef *NamespacedRef `json:"prefixRef,omitempty"` + PrefixLength int `json:"prefixLength,omitempty"` // +optional - ChildPrefixTemplate *IPPrefixTemplate `json:"childPrefixTemplate,omitempty"` - // +optional - ReclaimPolicy ReclaimPolicy `json:"reclaimPolicy,omitempty"` + Allocation AllocationSpec `json:"allocation,omitempty"` // +optional - OwnerRef *ObjectRef `json:"ownerRef,omitempty"` + // +kubebuilder:validation:Enum=platform;consumer;shared + Visibility string `json:"visibility,omitempty"` } -type IPPrefixClaimStatus struct { +type IPPoolStatus struct { // +optional - Phase ClaimPhase `json:"phase,omitempty"` + Phase PoolPhase `json:"phase,omitempty"` // +optional AllocatedCIDR string `json:"allocatedCIDR,omitempty"` // +optional - BoundPrefixRef *LocalRef `json:"boundPrefixRef,omitempty"` + Capacity PoolCapacity `json:"capacity,omitempty"` // +optional // +listType=map // +listMapKey=type @@ -277,42 +174,45 @@ type IPPrefixClaimStatus struct { // +kubebuilder:object:root=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPPrefixClaimList struct { +type IPPoolList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []IPPrefixClaim `json:"items"` + Items []IPPool `json:"items"` } // ---------------------------------------------------------------------------- -// IPAddress +// IPAllocation — namespace-scoped, system-created allocation record. // ---------------------------------------------------------------------------- // +kubebuilder:object:root=true -// +kubebuilder:resource:shortName=ipa +// +kubebuilder:resource:shortName=ipalloc // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Address",type=string,JSONPath=`.spec.address` -// +kubebuilder:printcolumn:name="Family",type=string,JSONPath=`.spec.ipFamily` -// +kubebuilder:printcolumn:name="Prefix",type=string,JSONPath=`.spec.prefixRef.name` +// +kubebuilder:printcolumn:name="CIDR",type=string,JSONPath=`.status.allocatedCIDR` +// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.spec.poolRef.name` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +genclient + +// IPAllocation records a CIDR carved out of an IPPool by an IPClaim. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPAddress struct { +type IPAllocation struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec IPAddressSpec `json:"spec,omitempty"` - Status IPAddressStatus `json:"status,omitempty"` + Spec IPAllocationSpec `json:"spec,omitempty"` + Status IPAllocationStatus `json:"status,omitempty"` } -type IPAddressSpec struct { - Address string `json:"address"` - IPFamily IPFamily `json:"ipFamily"` - PrefixRef LocalRef `json:"prefixRef"` - // +optional - ClaimRef *LocalRef `json:"claimRef,omitempty"` +type IPAllocationSpec struct { + IPFamily IPFamily `json:"ipFamily"` + PoolRef LocalRef `json:"poolRef"` } -type IPAddressStatus struct { +type IPAllocationStatus struct { + // +optional + Phase AllocationPhase `json:"phase,omitempty"` + // +optional + AllocatedCIDR string `json:"allocatedCIDR,omitempty"` // +optional // +listType=map // +listMapKey=type @@ -321,53 +221,60 @@ type IPAddressStatus struct { // +kubebuilder:object:root=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPAddressList struct { +type IPAllocationList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []IPAddress `json:"items"` + Items []IPAllocation `json:"items"` } // ---------------------------------------------------------------------------- -// IPAddressClaim +// IPClaim // ---------------------------------------------------------------------------- // +kubebuilder:object:root=true -// +kubebuilder:resource:shortName=ipac +// +kubebuilder:resource:shortName=ipclaim // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` -// +kubebuilder:printcolumn:name="IP",type=string,JSONPath=`.status.allocatedIP` -// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.spec.prefixRef.name` +// +kubebuilder:printcolumn:name="CIDR",type=string,JSONPath=`.status.allocatedCIDR` +// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.spec.poolRef.name` // +kubebuilder:printcolumn:name="Family",type=string,JSONPath=`.spec.ipFamily` +// +kubebuilder:printcolumn:name="Length",type=integer,JSONPath=`.spec.prefixLength` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPAddressClaim struct { +type IPClaim struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec IPAddressClaimSpec `json:"spec,omitempty"` - Status IPAddressClaimStatus `json:"status,omitempty"` + Spec IPClaimSpec `json:"spec,omitempty"` + Status IPClaimStatus `json:"status,omitempty"` } -type IPAddressClaimSpec struct { +type IPClaimSpec struct { IPFamily IPFamily `json:"ipFamily"` + // PrefixLength is the requested sub-prefix size in bits. Must be a + // valid mask length for the chosen ipFamily (0-32 for IPv4, 0-128 + // for IPv6). + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=128 + PrefixLength int `json:"prefixLength"` // +optional - PrefixSelector *PrefixSelector `json:"prefixSelector,omitempty"` + PoolSelector *PoolSelector `json:"poolSelector,omitempty"` // +optional - PrefixRef *NamespacedRef `json:"prefixRef,omitempty"` + PoolRef *NamespacedRef `json:"poolRef,omitempty"` // +optional ReclaimPolicy ReclaimPolicy `json:"reclaimPolicy,omitempty"` // +optional OwnerRef *ObjectRef `json:"ownerRef,omitempty"` } -type IPAddressClaimStatus struct { +type IPClaimStatus struct { // +optional Phase ClaimPhase `json:"phase,omitempty"` // +optional - AllocatedIP string `json:"allocatedIP,omitempty"` + AllocatedCIDR string `json:"allocatedCIDR,omitempty"` // +optional - BoundAddressRef *LocalRef `json:"boundAddressRef,omitempty"` + BoundAllocationRef *LocalRef `json:"boundAllocationRef,omitempty"` // +optional // +listType=map // +listMapKey=type @@ -376,9 +283,8 @@ type IPAddressClaimStatus struct { // +kubebuilder:object:root=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IPAddressClaimList struct { +type IPClaimList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []IPAddressClaim `json:"items"` + Items []IPClaim `json:"items"` } - diff --git a/pkg/apis/ipam/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/ipam/v1alpha1/zz_generated.deepcopy.go index 2b7fbbc..e2b9898 100644 --- a/pkg/apis/ipam/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/ipam/v1alpha1/zz_generated.deepcopy.go @@ -27,88 +27,27 @@ func (in *AllocationSpec) DeepCopy() *AllocationSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddress) DeepCopyInto(out *IPAddress) { +func (in *IPAllocation) DeepCopyInto(out *IPAllocation) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddress. -func (in *IPAddress) DeepCopy() *IPAddress { - if in == nil { - return nil - } - out := new(IPAddress) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddress) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaim) DeepCopyInto(out *IPAddressClaim) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaim. -func (in *IPAddressClaim) DeepCopy() *IPAddressClaim { - if in == nil { - return nil - } - out := new(IPAddressClaim) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddressClaim) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaimList) DeepCopyInto(out *IPAddressClaimList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]IPAddressClaim, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimList. -func (in *IPAddressClaimList) DeepCopy() *IPAddressClaimList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocation. +func (in *IPAllocation) DeepCopy() *IPAllocation { if in == nil { return nil } - out := new(IPAddressClaimList) + out := new(IPAllocation) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddressClaimList) DeepCopyObject() runtime.Object { +func (in *IPAllocation) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -116,72 +55,13 @@ func (in *IPAddressClaimList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaimSpec) DeepCopyInto(out *IPAddressClaimSpec) { - *out = *in - if in.PrefixSelector != nil { - in, out := &in.PrefixSelector, &out.PrefixSelector - *out = new(PrefixSelector) - (*in).DeepCopyInto(*out) - } - if in.PrefixRef != nil { - in, out := &in.PrefixRef, &out.PrefixRef - *out = new(NamespacedRef) - (*in).DeepCopyInto(*out) - } - if in.OwnerRef != nil { - in, out := &in.OwnerRef, &out.OwnerRef - *out = new(ObjectRef) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimSpec. -func (in *IPAddressClaimSpec) DeepCopy() *IPAddressClaimSpec { - if in == nil { - return nil - } - out := new(IPAddressClaimSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaimStatus) DeepCopyInto(out *IPAddressClaimStatus) { - *out = *in - if in.BoundAddressRef != nil { - in, out := &in.BoundAddressRef, &out.BoundAddressRef - *out = new(LocalRef) - **out = **in - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimStatus. -func (in *IPAddressClaimStatus) DeepCopy() *IPAddressClaimStatus { - if in == nil { - return nil - } - out := new(IPAddressClaimStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressList) DeepCopyInto(out *IPAddressList) { +func (in *IPAllocationList) DeepCopyInto(out *IPAllocationList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]IPAddress, len(*in)) + *out = make([]IPAllocation, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -189,18 +69,18 @@ func (in *IPAddressList) DeepCopyInto(out *IPAddressList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressList. -func (in *IPAddressList) DeepCopy() *IPAddressList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocationList. +func (in *IPAllocationList) DeepCopy() *IPAllocationList { if in == nil { return nil } - out := new(IPAddressList) + out := new(IPAllocationList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddressList) DeepCopyObject() runtime.Object { +func (in *IPAllocationList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -208,29 +88,24 @@ func (in *IPAddressList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressSpec) DeepCopyInto(out *IPAddressSpec) { +func (in *IPAllocationSpec) DeepCopyInto(out *IPAllocationSpec) { *out = *in - out.PrefixRef = in.PrefixRef - if in.ClaimRef != nil { - in, out := &in.ClaimRef, &out.ClaimRef - *out = new(LocalRef) - **out = **in - } + out.PoolRef = in.PoolRef return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressSpec. -func (in *IPAddressSpec) DeepCopy() *IPAddressSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocationSpec. +func (in *IPAllocationSpec) DeepCopy() *IPAllocationSpec { if in == nil { return nil } - out := new(IPAddressSpec) + out := new(IPAllocationSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressStatus) DeepCopyInto(out *IPAddressStatus) { +func (in *IPAllocationStatus) DeepCopyInto(out *IPAllocationStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -242,46 +117,18 @@ func (in *IPAddressStatus) DeepCopyInto(out *IPAddressStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressStatus. -func (in *IPAddressStatus) DeepCopy() *IPAddressStatus { - if in == nil { - return nil - } - out := new(IPAddressStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefix) DeepCopyInto(out *IPPrefix) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefix. -func (in *IPPrefix) DeepCopy() *IPPrefix { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocationStatus. +func (in *IPAllocationStatus) DeepCopy() *IPAllocationStatus { if in == nil { return nil } - out := new(IPPrefix) + out := new(IPAllocationStatus) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefix) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaim) DeepCopyInto(out *IPPrefixClaim) { +func (in *IPClaim) DeepCopyInto(out *IPClaim) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -290,18 +137,18 @@ func (in *IPPrefixClaim) DeepCopyInto(out *IPPrefixClaim) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaim. -func (in *IPPrefixClaim) DeepCopy() *IPPrefixClaim { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaim. +func (in *IPClaim) DeepCopy() *IPClaim { if in == nil { return nil } - out := new(IPPrefixClaim) + out := new(IPClaim) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClaim) DeepCopyObject() runtime.Object { +func (in *IPClaim) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -309,13 +156,13 @@ func (in *IPPrefixClaim) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaimList) DeepCopyInto(out *IPPrefixClaimList) { +func (in *IPClaimList) DeepCopyInto(out *IPClaimList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]IPPrefixClaim, len(*in)) + *out = make([]IPClaim, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -323,18 +170,18 @@ func (in *IPPrefixClaimList) DeepCopyInto(out *IPPrefixClaimList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaimList. -func (in *IPPrefixClaimList) DeepCopy() *IPPrefixClaimList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimList. +func (in *IPClaimList) DeepCopy() *IPClaimList { if in == nil { return nil } - out := new(IPPrefixClaimList) + out := new(IPClaimList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClaimList) DeepCopyObject() runtime.Object { +func (in *IPClaimList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -342,23 +189,18 @@ func (in *IPPrefixClaimList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaimSpec) DeepCopyInto(out *IPPrefixClaimSpec) { +func (in *IPClaimSpec) DeepCopyInto(out *IPClaimSpec) { *out = *in - if in.PrefixSelector != nil { - in, out := &in.PrefixSelector, &out.PrefixSelector - *out = new(PrefixSelector) + if in.PoolSelector != nil { + in, out := &in.PoolSelector, &out.PoolSelector + *out = new(PoolSelector) (*in).DeepCopyInto(*out) } - if in.PrefixRef != nil { - in, out := &in.PrefixRef, &out.PrefixRef + if in.PoolRef != nil { + in, out := &in.PoolRef, &out.PoolRef *out = new(NamespacedRef) (*in).DeepCopyInto(*out) } - if in.ChildPrefixTemplate != nil { - in, out := &in.ChildPrefixTemplate, &out.ChildPrefixTemplate - *out = new(IPPrefixTemplate) - (*in).DeepCopyInto(*out) - } if in.OwnerRef != nil { in, out := &in.OwnerRef, &out.OwnerRef *out = new(ObjectRef) @@ -367,21 +209,21 @@ func (in *IPPrefixClaimSpec) DeepCopyInto(out *IPPrefixClaimSpec) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaimSpec. -func (in *IPPrefixClaimSpec) DeepCopy() *IPPrefixClaimSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimSpec. +func (in *IPClaimSpec) DeepCopy() *IPClaimSpec { if in == nil { return nil } - out := new(IPPrefixClaimSpec) + out := new(IPClaimSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaimStatus) DeepCopyInto(out *IPPrefixClaimStatus) { +func (in *IPClaimStatus) DeepCopyInto(out *IPClaimStatus) { *out = *in - if in.BoundPrefixRef != nil { - in, out := &in.BoundPrefixRef, &out.BoundPrefixRef + if in.BoundAllocationRef != nil { + in, out := &in.BoundAllocationRef, &out.BoundAllocationRef *out = new(LocalRef) **out = **in } @@ -395,70 +237,38 @@ func (in *IPPrefixClaimStatus) DeepCopyInto(out *IPPrefixClaimStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaimStatus. -func (in *IPPrefixClaimStatus) DeepCopy() *IPPrefixClaimStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimStatus. +func (in *IPClaimStatus) DeepCopy() *IPClaimStatus { if in == nil { return nil } - out := new(IPPrefixClaimStatus) + out := new(IPClaimStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClass) DeepCopyInto(out *IPPrefixClass) { +func (in *IPPool) DeepCopyInto(out *IPPool) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClass. -func (in *IPPrefixClass) DeepCopy() *IPPrefixClass { - if in == nil { - return nil - } - out := new(IPPrefixClass) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClass) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClassList) DeepCopyInto(out *IPPrefixClassList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]IPPrefixClass, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClassList. -func (in *IPPrefixClassList) DeepCopy() *IPPrefixClassList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPool. +func (in *IPPool) DeepCopy() *IPPool { if in == nil { return nil } - out := new(IPPrefixClassList) + out := new(IPPool) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClassList) DeepCopyObject() runtime.Object { +func (in *IPPool) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -466,30 +276,13 @@ func (in *IPPrefixClassList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClassSpec) DeepCopyInto(out *IPPrefixClassSpec) { - *out = *in - out.DefaultAllocation = in.DefaultAllocation - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClassSpec. -func (in *IPPrefixClassSpec) DeepCopy() *IPPrefixClassSpec { - if in == nil { - return nil - } - out := new(IPPrefixClassSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixList) DeepCopyInto(out *IPPrefixList) { +func (in *IPPoolList) DeepCopyInto(out *IPPoolList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]IPPrefix, len(*in)) + *out = make([]IPPool, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -497,18 +290,18 @@ func (in *IPPrefixList) DeepCopyInto(out *IPPrefixList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixList. -func (in *IPPrefixList) DeepCopy() *IPPrefixList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolList. +func (in *IPPoolList) DeepCopy() *IPPoolList { if in == nil { return nil } - out := new(IPPrefixList) + out := new(IPPoolList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixList) DeepCopyObject() runtime.Object { +func (in *IPPoolList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -516,30 +309,29 @@ func (in *IPPrefixList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixSpec) DeepCopyInto(out *IPPrefixSpec) { +func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) { *out = *in - out.ClassRef = in.ClassRef - out.Allocation = in.Allocation - if in.ParentRef != nil { - in, out := &in.ParentRef, &out.ParentRef - *out = new(ObjectRef) + if in.ParentPoolRef != nil { + in, out := &in.ParentPoolRef, &out.ParentPoolRef + *out = new(LocalRef) **out = **in } + out.Allocation = in.Allocation return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixSpec. -func (in *IPPrefixSpec) DeepCopy() *IPPrefixSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolSpec. +func (in *IPPoolSpec) DeepCopy() *IPPoolSpec { if in == nil { return nil } - out := new(IPPrefixSpec) + out := new(IPPoolSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixStatus) DeepCopyInto(out *IPPrefixStatus) { +func (in *IPPoolStatus) DeepCopyInto(out *IPPoolStatus) { *out = *in out.Capacity = in.Capacity if in.Conditions != nil { @@ -552,30 +344,12 @@ func (in *IPPrefixStatus) DeepCopyInto(out *IPPrefixStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixStatus. -func (in *IPPrefixStatus) DeepCopy() *IPPrefixStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolStatus. +func (in *IPPoolStatus) DeepCopy() *IPPoolStatus { if in == nil { return nil } - out := new(IPPrefixStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixTemplate) DeepCopyInto(out *IPPrefixTemplate) { - *out = *in - in.Metadata.DeepCopyInto(&out.Metadata) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixTemplate. -func (in *IPPrefixTemplate) DeepCopy() *IPPrefixTemplate { - if in == nil { - return nil - } - out := new(IPPrefixTemplate) + out := new(IPPoolStatus) in.DeepCopyInto(out) return out } @@ -634,23 +408,23 @@ func (in *ObjectRef) DeepCopy() *ObjectRef { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PrefixCapacity) DeepCopyInto(out *PrefixCapacity) { +func (in *PoolCapacity) DeepCopyInto(out *PoolCapacity) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixCapacity. -func (in *PrefixCapacity) DeepCopy() *PrefixCapacity { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PoolCapacity. +func (in *PoolCapacity) DeepCopy() *PoolCapacity { if in == nil { return nil } - out := new(PrefixCapacity) + out := new(PoolCapacity) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PrefixSelector) DeepCopyInto(out *PrefixSelector) { +func (in *PoolSelector) DeepCopyInto(out *PoolSelector) { *out = *in if in.LabelSelector != nil { in, out := &in.LabelSelector, &out.LabelSelector @@ -665,13 +439,12 @@ func (in *PrefixSelector) DeepCopyInto(out *PrefixSelector) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixSelector. -func (in *PrefixSelector) DeepCopy() *PrefixSelector { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PoolSelector. +func (in *PoolSelector) DeepCopy() *PoolSelector { if in == nil { return nil } - out := new(PrefixSelector) + out := new(PoolSelector) in.DeepCopyInto(out) return out } - diff --git a/pkg/apis/ipam/zz_generated.deepcopy.go b/pkg/apis/ipam/zz_generated.deepcopy.go index 5cc362e..0ac7cf7 100644 --- a/pkg/apis/ipam/zz_generated.deepcopy.go +++ b/pkg/apis/ipam/zz_generated.deepcopy.go @@ -27,88 +27,27 @@ func (in *AllocationSpec) DeepCopy() *AllocationSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddress) DeepCopyInto(out *IPAddress) { +func (in *IPAllocation) DeepCopyInto(out *IPAllocation) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddress. -func (in *IPAddress) DeepCopy() *IPAddress { - if in == nil { - return nil - } - out := new(IPAddress) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddress) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaim) DeepCopyInto(out *IPAddressClaim) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaim. -func (in *IPAddressClaim) DeepCopy() *IPAddressClaim { - if in == nil { - return nil - } - out := new(IPAddressClaim) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddressClaim) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaimList) DeepCopyInto(out *IPAddressClaimList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]IPAddressClaim, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimList. -func (in *IPAddressClaimList) DeepCopy() *IPAddressClaimList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocation. +func (in *IPAllocation) DeepCopy() *IPAllocation { if in == nil { return nil } - out := new(IPAddressClaimList) + out := new(IPAllocation) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddressClaimList) DeepCopyObject() runtime.Object { +func (in *IPAllocation) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -116,72 +55,13 @@ func (in *IPAddressClaimList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaimSpec) DeepCopyInto(out *IPAddressClaimSpec) { - *out = *in - if in.PrefixSelector != nil { - in, out := &in.PrefixSelector, &out.PrefixSelector - *out = new(PrefixSelector) - (*in).DeepCopyInto(*out) - } - if in.PrefixRef != nil { - in, out := &in.PrefixRef, &out.PrefixRef - *out = new(NamespacedRef) - (*in).DeepCopyInto(*out) - } - if in.OwnerRef != nil { - in, out := &in.OwnerRef, &out.OwnerRef - *out = new(ObjectRef) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimSpec. -func (in *IPAddressClaimSpec) DeepCopy() *IPAddressClaimSpec { - if in == nil { - return nil - } - out := new(IPAddressClaimSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressClaimStatus) DeepCopyInto(out *IPAddressClaimStatus) { - *out = *in - if in.BoundAddressRef != nil { - in, out := &in.BoundAddressRef, &out.BoundAddressRef - *out = new(LocalRef) - **out = **in - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimStatus. -func (in *IPAddressClaimStatus) DeepCopy() *IPAddressClaimStatus { - if in == nil { - return nil - } - out := new(IPAddressClaimStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressList) DeepCopyInto(out *IPAddressList) { +func (in *IPAllocationList) DeepCopyInto(out *IPAllocationList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]IPAddress, len(*in)) + *out = make([]IPAllocation, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -189,18 +69,18 @@ func (in *IPAddressList) DeepCopyInto(out *IPAddressList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressList. -func (in *IPAddressList) DeepCopy() *IPAddressList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocationList. +func (in *IPAllocationList) DeepCopy() *IPAllocationList { if in == nil { return nil } - out := new(IPAddressList) + out := new(IPAllocationList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPAddressList) DeepCopyObject() runtime.Object { +func (in *IPAllocationList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -208,29 +88,24 @@ func (in *IPAddressList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressSpec) DeepCopyInto(out *IPAddressSpec) { +func (in *IPAllocationSpec) DeepCopyInto(out *IPAllocationSpec) { *out = *in - out.PrefixRef = in.PrefixRef - if in.ClaimRef != nil { - in, out := &in.ClaimRef, &out.ClaimRef - *out = new(LocalRef) - **out = **in - } + out.PoolRef = in.PoolRef return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressSpec. -func (in *IPAddressSpec) DeepCopy() *IPAddressSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocationSpec. +func (in *IPAllocationSpec) DeepCopy() *IPAllocationSpec { if in == nil { return nil } - out := new(IPAddressSpec) + out := new(IPAllocationSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPAddressStatus) DeepCopyInto(out *IPAddressStatus) { +func (in *IPAllocationStatus) DeepCopyInto(out *IPAllocationStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -242,46 +117,18 @@ func (in *IPAddressStatus) DeepCopyInto(out *IPAddressStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressStatus. -func (in *IPAddressStatus) DeepCopy() *IPAddressStatus { - if in == nil { - return nil - } - out := new(IPAddressStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefix) DeepCopyInto(out *IPPrefix) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefix. -func (in *IPPrefix) DeepCopy() *IPPrefix { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAllocationStatus. +func (in *IPAllocationStatus) DeepCopy() *IPAllocationStatus { if in == nil { return nil } - out := new(IPPrefix) + out := new(IPAllocationStatus) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefix) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaim) DeepCopyInto(out *IPPrefixClaim) { +func (in *IPClaim) DeepCopyInto(out *IPClaim) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -290,18 +137,18 @@ func (in *IPPrefixClaim) DeepCopyInto(out *IPPrefixClaim) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaim. -func (in *IPPrefixClaim) DeepCopy() *IPPrefixClaim { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaim. +func (in *IPClaim) DeepCopy() *IPClaim { if in == nil { return nil } - out := new(IPPrefixClaim) + out := new(IPClaim) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClaim) DeepCopyObject() runtime.Object { +func (in *IPClaim) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -309,13 +156,13 @@ func (in *IPPrefixClaim) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaimList) DeepCopyInto(out *IPPrefixClaimList) { +func (in *IPClaimList) DeepCopyInto(out *IPClaimList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]IPPrefixClaim, len(*in)) + *out = make([]IPClaim, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -323,18 +170,18 @@ func (in *IPPrefixClaimList) DeepCopyInto(out *IPPrefixClaimList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaimList. -func (in *IPPrefixClaimList) DeepCopy() *IPPrefixClaimList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimList. +func (in *IPClaimList) DeepCopy() *IPClaimList { if in == nil { return nil } - out := new(IPPrefixClaimList) + out := new(IPClaimList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClaimList) DeepCopyObject() runtime.Object { +func (in *IPClaimList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -342,23 +189,18 @@ func (in *IPPrefixClaimList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaimSpec) DeepCopyInto(out *IPPrefixClaimSpec) { +func (in *IPClaimSpec) DeepCopyInto(out *IPClaimSpec) { *out = *in - if in.PrefixSelector != nil { - in, out := &in.PrefixSelector, &out.PrefixSelector - *out = new(PrefixSelector) + if in.PoolSelector != nil { + in, out := &in.PoolSelector, &out.PoolSelector + *out = new(PoolSelector) (*in).DeepCopyInto(*out) } - if in.PrefixRef != nil { - in, out := &in.PrefixRef, &out.PrefixRef + if in.PoolRef != nil { + in, out := &in.PoolRef, &out.PoolRef *out = new(NamespacedRef) (*in).DeepCopyInto(*out) } - if in.ChildPrefixTemplate != nil { - in, out := &in.ChildPrefixTemplate, &out.ChildPrefixTemplate - *out = new(IPPrefixTemplate) - (*in).DeepCopyInto(*out) - } if in.OwnerRef != nil { in, out := &in.OwnerRef, &out.OwnerRef *out = new(ObjectRef) @@ -367,21 +209,21 @@ func (in *IPPrefixClaimSpec) DeepCopyInto(out *IPPrefixClaimSpec) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaimSpec. -func (in *IPPrefixClaimSpec) DeepCopy() *IPPrefixClaimSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimSpec. +func (in *IPClaimSpec) DeepCopy() *IPClaimSpec { if in == nil { return nil } - out := new(IPPrefixClaimSpec) + out := new(IPClaimSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClaimStatus) DeepCopyInto(out *IPPrefixClaimStatus) { +func (in *IPClaimStatus) DeepCopyInto(out *IPClaimStatus) { *out = *in - if in.BoundPrefixRef != nil { - in, out := &in.BoundPrefixRef, &out.BoundPrefixRef + if in.BoundAllocationRef != nil { + in, out := &in.BoundAllocationRef, &out.BoundAllocationRef *out = new(LocalRef) **out = **in } @@ -395,70 +237,38 @@ func (in *IPPrefixClaimStatus) DeepCopyInto(out *IPPrefixClaimStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClaimStatus. -func (in *IPPrefixClaimStatus) DeepCopy() *IPPrefixClaimStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimStatus. +func (in *IPClaimStatus) DeepCopy() *IPClaimStatus { if in == nil { return nil } - out := new(IPPrefixClaimStatus) + out := new(IPClaimStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClass) DeepCopyInto(out *IPPrefixClass) { +func (in *IPPool) DeepCopyInto(out *IPPool) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClass. -func (in *IPPrefixClass) DeepCopy() *IPPrefixClass { - if in == nil { - return nil - } - out := new(IPPrefixClass) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClass) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClassList) DeepCopyInto(out *IPPrefixClassList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]IPPrefixClass, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClassList. -func (in *IPPrefixClassList) DeepCopy() *IPPrefixClassList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPool. +func (in *IPPool) DeepCopy() *IPPool { if in == nil { return nil } - out := new(IPPrefixClassList) + out := new(IPPool) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixClassList) DeepCopyObject() runtime.Object { +func (in *IPPool) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -466,30 +276,13 @@ func (in *IPPrefixClassList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixClassSpec) DeepCopyInto(out *IPPrefixClassSpec) { - *out = *in - out.DefaultAllocation = in.DefaultAllocation - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixClassSpec. -func (in *IPPrefixClassSpec) DeepCopy() *IPPrefixClassSpec { - if in == nil { - return nil - } - out := new(IPPrefixClassSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixList) DeepCopyInto(out *IPPrefixList) { +func (in *IPPoolList) DeepCopyInto(out *IPPoolList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]IPPrefix, len(*in)) + *out = make([]IPPool, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -497,18 +290,18 @@ func (in *IPPrefixList) DeepCopyInto(out *IPPrefixList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixList. -func (in *IPPrefixList) DeepCopy() *IPPrefixList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolList. +func (in *IPPoolList) DeepCopy() *IPPoolList { if in == nil { return nil } - out := new(IPPrefixList) + out := new(IPPoolList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IPPrefixList) DeepCopyObject() runtime.Object { +func (in *IPPoolList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -516,30 +309,29 @@ func (in *IPPrefixList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixSpec) DeepCopyInto(out *IPPrefixSpec) { +func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) { *out = *in - out.ClassRef = in.ClassRef - out.Allocation = in.Allocation - if in.ParentRef != nil { - in, out := &in.ParentRef, &out.ParentRef - *out = new(ObjectRef) + if in.ParentPoolRef != nil { + in, out := &in.ParentPoolRef, &out.ParentPoolRef + *out = new(LocalRef) **out = **in } + out.Allocation = in.Allocation return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixSpec. -func (in *IPPrefixSpec) DeepCopy() *IPPrefixSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolSpec. +func (in *IPPoolSpec) DeepCopy() *IPPoolSpec { if in == nil { return nil } - out := new(IPPrefixSpec) + out := new(IPPoolSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixStatus) DeepCopyInto(out *IPPrefixStatus) { +func (in *IPPoolStatus) DeepCopyInto(out *IPPoolStatus) { *out = *in out.Capacity = in.Capacity if in.Conditions != nil { @@ -552,30 +344,12 @@ func (in *IPPrefixStatus) DeepCopyInto(out *IPPrefixStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixStatus. -func (in *IPPrefixStatus) DeepCopy() *IPPrefixStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolStatus. +func (in *IPPoolStatus) DeepCopy() *IPPoolStatus { if in == nil { return nil } - out := new(IPPrefixStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPrefixTemplate) DeepCopyInto(out *IPPrefixTemplate) { - *out = *in - in.Metadata.DeepCopyInto(&out.Metadata) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPrefixTemplate. -func (in *IPPrefixTemplate) DeepCopy() *IPPrefixTemplate { - if in == nil { - return nil - } - out := new(IPPrefixTemplate) + out := new(IPPoolStatus) in.DeepCopyInto(out) return out } @@ -634,23 +408,23 @@ func (in *ObjectRef) DeepCopy() *ObjectRef { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PrefixCapacity) DeepCopyInto(out *PrefixCapacity) { +func (in *PoolCapacity) DeepCopyInto(out *PoolCapacity) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixCapacity. -func (in *PrefixCapacity) DeepCopy() *PrefixCapacity { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PoolCapacity. +func (in *PoolCapacity) DeepCopy() *PoolCapacity { if in == nil { return nil } - out := new(PrefixCapacity) + out := new(PoolCapacity) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PrefixSelector) DeepCopyInto(out *PrefixSelector) { +func (in *PoolSelector) DeepCopyInto(out *PoolSelector) { *out = *in if in.LabelSelector != nil { in, out := &in.LabelSelector, &out.LabelSelector @@ -665,13 +439,12 @@ func (in *PrefixSelector) DeepCopyInto(out *PrefixSelector) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixSelector. -func (in *PrefixSelector) DeepCopy() *PrefixSelector { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PoolSelector. +func (in *PoolSelector) DeepCopy() *PoolSelector { if in == nil { return nil } - out := new(PrefixSelector) + out := new(PoolSelector) in.DeepCopyInto(out) return out } - diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipaddress.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipaddress.go deleted file mode 100644 index c647b66..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipaddress.go +++ /dev/null @@ -1,34 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeIPAddresses implements IPAddressInterface -type fakeIPAddresses struct { - *gentype.FakeClientWithList[*v1alpha1.IPAddress, *v1alpha1.IPAddressList] - Fake *FakeIpamV1alpha1 -} - -func newFakeIPAddresses(fake *FakeIpamV1alpha1, namespace string) ipamv1alpha1.IPAddressInterface { - return &fakeIPAddresses{ - gentype.NewFakeClientWithList[*v1alpha1.IPAddress, *v1alpha1.IPAddressList]( - fake.Fake, - namespace, - v1alpha1.SchemeGroupVersion.WithResource("ipaddresses"), - v1alpha1.SchemeGroupVersion.WithKind("IPAddress"), - func() *v1alpha1.IPAddress { return &v1alpha1.IPAddress{} }, - func() *v1alpha1.IPAddressList { return &v1alpha1.IPAddressList{} }, - func(dst, src *v1alpha1.IPAddressList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.IPAddressList) []*v1alpha1.IPAddress { return gentype.ToPointerSlice(list.Items) }, - func(list *v1alpha1.IPAddressList, items []*v1alpha1.IPAddress) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipaddressclaim.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipaddressclaim.go deleted file mode 100644 index 73e13f5..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipaddressclaim.go +++ /dev/null @@ -1,36 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeIPAddressClaims implements IPAddressClaimInterface -type fakeIPAddressClaims struct { - *gentype.FakeClientWithList[*v1alpha1.IPAddressClaim, *v1alpha1.IPAddressClaimList] - Fake *FakeIpamV1alpha1 -} - -func newFakeIPAddressClaims(fake *FakeIpamV1alpha1, namespace string) ipamv1alpha1.IPAddressClaimInterface { - return &fakeIPAddressClaims{ - gentype.NewFakeClientWithList[*v1alpha1.IPAddressClaim, *v1alpha1.IPAddressClaimList]( - fake.Fake, - namespace, - v1alpha1.SchemeGroupVersion.WithResource("ipaddressclaims"), - v1alpha1.SchemeGroupVersion.WithKind("IPAddressClaim"), - func() *v1alpha1.IPAddressClaim { return &v1alpha1.IPAddressClaim{} }, - func() *v1alpha1.IPAddressClaimList { return &v1alpha1.IPAddressClaimList{} }, - func(dst, src *v1alpha1.IPAddressClaimList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.IPAddressClaimList) []*v1alpha1.IPAddressClaim { - return gentype.ToPointerSlice(list.Items) - }, - func(list *v1alpha1.IPAddressClaimList, items []*v1alpha1.IPAddressClaim) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipallocation.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipallocation.go new file mode 100644 index 0000000..7d25d8d --- /dev/null +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipallocation.go @@ -0,0 +1,36 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeIPAllocations implements IPAllocationInterface +type fakeIPAllocations struct { + *gentype.FakeClientWithList[*v1alpha1.IPAllocation, *v1alpha1.IPAllocationList] + Fake *FakeIpamV1alpha1 +} + +func newFakeIPAllocations(fake *FakeIpamV1alpha1, namespace string) ipamv1alpha1.IPAllocationInterface { + return &fakeIPAllocations{ + gentype.NewFakeClientWithList[*v1alpha1.IPAllocation, *v1alpha1.IPAllocationList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("ipallocations"), + v1alpha1.SchemeGroupVersion.WithKind("IPAllocation"), + func() *v1alpha1.IPAllocation { return &v1alpha1.IPAllocation{} }, + func() *v1alpha1.IPAllocationList { return &v1alpha1.IPAllocationList{} }, + func(dst, src *v1alpha1.IPAllocationList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.IPAllocationList) []*v1alpha1.IPAllocation { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.IPAllocationList, items []*v1alpha1.IPAllocation) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipam_client.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipam_client.go index 2e4ae04..05659ad 100644 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipam_client.go +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipam_client.go @@ -12,24 +12,16 @@ type FakeIpamV1alpha1 struct { *testing.Fake } -func (c *FakeIpamV1alpha1) IPAddresses(namespace string) v1alpha1.IPAddressInterface { - return newFakeIPAddresses(c, namespace) +func (c *FakeIpamV1alpha1) IPAllocations(namespace string) v1alpha1.IPAllocationInterface { + return newFakeIPAllocations(c, namespace) } -func (c *FakeIpamV1alpha1) IPAddressClaims(namespace string) v1alpha1.IPAddressClaimInterface { - return newFakeIPAddressClaims(c, namespace) +func (c *FakeIpamV1alpha1) IPClaims(namespace string) v1alpha1.IPClaimInterface { + return newFakeIPClaims(c, namespace) } -func (c *FakeIpamV1alpha1) IPPrefixes() v1alpha1.IPPrefixInterface { - return newFakeIPPrefixes(c) -} - -func (c *FakeIpamV1alpha1) IPPrefixClaims(namespace string) v1alpha1.IPPrefixClaimInterface { - return newFakeIPPrefixClaims(c, namespace) -} - -func (c *FakeIpamV1alpha1) IPPrefixClasses() v1alpha1.IPPrefixClassInterface { - return newFakeIPPrefixClasses(c) +func (c *FakeIpamV1alpha1) IPPools() v1alpha1.IPPoolInterface { + return newFakeIPPools(c) } // RESTClient returns a RESTClient that is used to communicate diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipclaim.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipclaim.go new file mode 100644 index 0000000..c2d6889 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipclaim.go @@ -0,0 +1,34 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeIPClaims implements IPClaimInterface +type fakeIPClaims struct { + *gentype.FakeClientWithList[*v1alpha1.IPClaim, *v1alpha1.IPClaimList] + Fake *FakeIpamV1alpha1 +} + +func newFakeIPClaims(fake *FakeIpamV1alpha1, namespace string) ipamv1alpha1.IPClaimInterface { + return &fakeIPClaims{ + gentype.NewFakeClientWithList[*v1alpha1.IPClaim, *v1alpha1.IPClaimList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("ipclaims"), + v1alpha1.SchemeGroupVersion.WithKind("IPClaim"), + func() *v1alpha1.IPClaim { return &v1alpha1.IPClaim{} }, + func() *v1alpha1.IPClaimList { return &v1alpha1.IPClaimList{} }, + func(dst, src *v1alpha1.IPClaimList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.IPClaimList) []*v1alpha1.IPClaim { return gentype.ToPointerSlice(list.Items) }, + func(list *v1alpha1.IPClaimList, items []*v1alpha1.IPClaim) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ippool.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ippool.go new file mode 100644 index 0000000..3e32ddb --- /dev/null +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ippool.go @@ -0,0 +1,34 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeIPPools implements IPPoolInterface +type fakeIPPools struct { + *gentype.FakeClientWithList[*v1alpha1.IPPool, *v1alpha1.IPPoolList] + Fake *FakeIpamV1alpha1 +} + +func newFakeIPPools(fake *FakeIpamV1alpha1) ipamv1alpha1.IPPoolInterface { + return &fakeIPPools{ + gentype.NewFakeClientWithList[*v1alpha1.IPPool, *v1alpha1.IPPoolList]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("ippools"), + v1alpha1.SchemeGroupVersion.WithKind("IPPool"), + func() *v1alpha1.IPPool { return &v1alpha1.IPPool{} }, + func() *v1alpha1.IPPoolList { return &v1alpha1.IPPoolList{} }, + func(dst, src *v1alpha1.IPPoolList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.IPPoolList) []*v1alpha1.IPPool { return gentype.ToPointerSlice(list.Items) }, + func(list *v1alpha1.IPPoolList, items []*v1alpha1.IPPool) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefix.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefix.go deleted file mode 100644 index 87f1595..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefix.go +++ /dev/null @@ -1,34 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeIPPrefixes implements IPPrefixInterface -type fakeIPPrefixes struct { - *gentype.FakeClientWithList[*v1alpha1.IPPrefix, *v1alpha1.IPPrefixList] - Fake *FakeIpamV1alpha1 -} - -func newFakeIPPrefixes(fake *FakeIpamV1alpha1) ipamv1alpha1.IPPrefixInterface { - return &fakeIPPrefixes{ - gentype.NewFakeClientWithList[*v1alpha1.IPPrefix, *v1alpha1.IPPrefixList]( - fake.Fake, - "", - v1alpha1.SchemeGroupVersion.WithResource("ipprefixes"), - v1alpha1.SchemeGroupVersion.WithKind("IPPrefix"), - func() *v1alpha1.IPPrefix { return &v1alpha1.IPPrefix{} }, - func() *v1alpha1.IPPrefixList { return &v1alpha1.IPPrefixList{} }, - func(dst, src *v1alpha1.IPPrefixList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.IPPrefixList) []*v1alpha1.IPPrefix { return gentype.ToPointerSlice(list.Items) }, - func(list *v1alpha1.IPPrefixList, items []*v1alpha1.IPPrefix) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefixclaim.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefixclaim.go deleted file mode 100644 index b83d10d..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefixclaim.go +++ /dev/null @@ -1,36 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeIPPrefixClaims implements IPPrefixClaimInterface -type fakeIPPrefixClaims struct { - *gentype.FakeClientWithList[*v1alpha1.IPPrefixClaim, *v1alpha1.IPPrefixClaimList] - Fake *FakeIpamV1alpha1 -} - -func newFakeIPPrefixClaims(fake *FakeIpamV1alpha1, namespace string) ipamv1alpha1.IPPrefixClaimInterface { - return &fakeIPPrefixClaims{ - gentype.NewFakeClientWithList[*v1alpha1.IPPrefixClaim, *v1alpha1.IPPrefixClaimList]( - fake.Fake, - namespace, - v1alpha1.SchemeGroupVersion.WithResource("ipprefixclaims"), - v1alpha1.SchemeGroupVersion.WithKind("IPPrefixClaim"), - func() *v1alpha1.IPPrefixClaim { return &v1alpha1.IPPrefixClaim{} }, - func() *v1alpha1.IPPrefixClaimList { return &v1alpha1.IPPrefixClaimList{} }, - func(dst, src *v1alpha1.IPPrefixClaimList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.IPPrefixClaimList) []*v1alpha1.IPPrefixClaim { - return gentype.ToPointerSlice(list.Items) - }, - func(list *v1alpha1.IPPrefixClaimList, items []*v1alpha1.IPPrefixClaim) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefixclass.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefixclass.go deleted file mode 100644 index 7007031..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/fake/fake_ipprefixclass.go +++ /dev/null @@ -1,36 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/clientset/versioned/typed/ipam/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeIPPrefixClasses implements IPPrefixClassInterface -type fakeIPPrefixClasses struct { - *gentype.FakeClientWithList[*v1alpha1.IPPrefixClass, *v1alpha1.IPPrefixClassList] - Fake *FakeIpamV1alpha1 -} - -func newFakeIPPrefixClasses(fake *FakeIpamV1alpha1) ipamv1alpha1.IPPrefixClassInterface { - return &fakeIPPrefixClasses{ - gentype.NewFakeClientWithList[*v1alpha1.IPPrefixClass, *v1alpha1.IPPrefixClassList]( - fake.Fake, - "", - v1alpha1.SchemeGroupVersion.WithResource("ipprefixclasses"), - v1alpha1.SchemeGroupVersion.WithKind("IPPrefixClass"), - func() *v1alpha1.IPPrefixClass { return &v1alpha1.IPPrefixClass{} }, - func() *v1alpha1.IPPrefixClassList { return &v1alpha1.IPPrefixClassList{} }, - func(dst, src *v1alpha1.IPPrefixClassList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.IPPrefixClassList) []*v1alpha1.IPPrefixClass { - return gentype.ToPointerSlice(list.Items) - }, - func(list *v1alpha1.IPPrefixClassList, items []*v1alpha1.IPPrefixClass) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/generated_expansion.go index 29c66d6..f7d7cff 100644 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/generated_expansion.go @@ -2,12 +2,8 @@ package v1alpha1 -type IPAddressExpansion interface{} +type IPAllocationExpansion interface{} -type IPAddressClaimExpansion interface{} +type IPClaimExpansion interface{} -type IPPrefixExpansion interface{} - -type IPPrefixClaimExpansion interface{} - -type IPPrefixClassExpansion interface{} +type IPPoolExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipaddress.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipaddress.go deleted file mode 100644 index fef71a9..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipaddress.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// IPAddressesGetter has a method to return a IPAddressInterface. -// A group's client should implement this interface. -type IPAddressesGetter interface { - IPAddresses(namespace string) IPAddressInterface -} - -// IPAddressInterface has methods to work with IPAddress resources. -type IPAddressInterface interface { - Create(ctx context.Context, iPAddress *ipamv1alpha1.IPAddress, opts v1.CreateOptions) (*ipamv1alpha1.IPAddress, error) - Update(ctx context.Context, iPAddress *ipamv1alpha1.IPAddress, opts v1.UpdateOptions) (*ipamv1alpha1.IPAddress, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, iPAddress *ipamv1alpha1.IPAddress, opts v1.UpdateOptions) (*ipamv1alpha1.IPAddress, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPAddress, error) - List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPAddressList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPAddress, err error) - IPAddressExpansion -} - -// iPAddresses implements IPAddressInterface -type iPAddresses struct { - *gentype.ClientWithList[*ipamv1alpha1.IPAddress, *ipamv1alpha1.IPAddressList] -} - -// newIPAddresses returns a IPAddresses -func newIPAddresses(c *IpamV1alpha1Client, namespace string) *iPAddresses { - return &iPAddresses{ - gentype.NewClientWithList[*ipamv1alpha1.IPAddress, *ipamv1alpha1.IPAddressList]( - "ipaddresses", - c.RESTClient(), - scheme.ParameterCodec, - namespace, - func() *ipamv1alpha1.IPAddress { return &ipamv1alpha1.IPAddress{} }, - func() *ipamv1alpha1.IPAddressList { return &ipamv1alpha1.IPAddressList{} }, - ), - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipaddressclaim.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipaddressclaim.go deleted file mode 100644 index 9baf7af..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipaddressclaim.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// IPAddressClaimsGetter has a method to return a IPAddressClaimInterface. -// A group's client should implement this interface. -type IPAddressClaimsGetter interface { - IPAddressClaims(namespace string) IPAddressClaimInterface -} - -// IPAddressClaimInterface has methods to work with IPAddressClaim resources. -type IPAddressClaimInterface interface { - Create(ctx context.Context, iPAddressClaim *ipamv1alpha1.IPAddressClaim, opts v1.CreateOptions) (*ipamv1alpha1.IPAddressClaim, error) - Update(ctx context.Context, iPAddressClaim *ipamv1alpha1.IPAddressClaim, opts v1.UpdateOptions) (*ipamv1alpha1.IPAddressClaim, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, iPAddressClaim *ipamv1alpha1.IPAddressClaim, opts v1.UpdateOptions) (*ipamv1alpha1.IPAddressClaim, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPAddressClaim, error) - List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPAddressClaimList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPAddressClaim, err error) - IPAddressClaimExpansion -} - -// iPAddressClaims implements IPAddressClaimInterface -type iPAddressClaims struct { - *gentype.ClientWithList[*ipamv1alpha1.IPAddressClaim, *ipamv1alpha1.IPAddressClaimList] -} - -// newIPAddressClaims returns a IPAddressClaims -func newIPAddressClaims(c *IpamV1alpha1Client, namespace string) *iPAddressClaims { - return &iPAddressClaims{ - gentype.NewClientWithList[*ipamv1alpha1.IPAddressClaim, *ipamv1alpha1.IPAddressClaimList]( - "ipaddressclaims", - c.RESTClient(), - scheme.ParameterCodec, - namespace, - func() *ipamv1alpha1.IPAddressClaim { return &ipamv1alpha1.IPAddressClaim{} }, - func() *ipamv1alpha1.IPAddressClaimList { return &ipamv1alpha1.IPAddressClaimList{} }, - ), - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipallocation.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipallocation.go new file mode 100644 index 0000000..28e3f6a --- /dev/null +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipallocation.go @@ -0,0 +1,54 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// IPAllocationsGetter has a method to return a IPAllocationInterface. +// A group's client should implement this interface. +type IPAllocationsGetter interface { + IPAllocations(namespace string) IPAllocationInterface +} + +// IPAllocationInterface has methods to work with IPAllocation resources. +type IPAllocationInterface interface { + Create(ctx context.Context, iPAllocation *ipamv1alpha1.IPAllocation, opts v1.CreateOptions) (*ipamv1alpha1.IPAllocation, error) + Update(ctx context.Context, iPAllocation *ipamv1alpha1.IPAllocation, opts v1.UpdateOptions) (*ipamv1alpha1.IPAllocation, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, iPAllocation *ipamv1alpha1.IPAllocation, opts v1.UpdateOptions) (*ipamv1alpha1.IPAllocation, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPAllocation, error) + List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPAllocationList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPAllocation, err error) + IPAllocationExpansion +} + +// iPAllocations implements IPAllocationInterface +type iPAllocations struct { + *gentype.ClientWithList[*ipamv1alpha1.IPAllocation, *ipamv1alpha1.IPAllocationList] +} + +// newIPAllocations returns a IPAllocations +func newIPAllocations(c *IpamV1alpha1Client, namespace string) *iPAllocations { + return &iPAllocations{ + gentype.NewClientWithList[*ipamv1alpha1.IPAllocation, *ipamv1alpha1.IPAllocationList]( + "ipallocations", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *ipamv1alpha1.IPAllocation { return &ipamv1alpha1.IPAllocation{} }, + func() *ipamv1alpha1.IPAllocationList { return &ipamv1alpha1.IPAllocationList{} }, + ), + } +} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipam_client.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipam_client.go index f2ff1eb..936570e 100644 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipam_client.go +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipam_client.go @@ -12,11 +12,9 @@ import ( type IpamV1alpha1Interface interface { RESTClient() rest.Interface - IPAddressesGetter - IPAddressClaimsGetter - IPPrefixesGetter - IPPrefixClaimsGetter - IPPrefixClassesGetter + IPAllocationsGetter + IPClaimsGetter + IPPoolsGetter } // IpamV1alpha1Client is used to interact with features provided by the ipam.miloapis.com group. @@ -24,24 +22,16 @@ type IpamV1alpha1Client struct { restClient rest.Interface } -func (c *IpamV1alpha1Client) IPAddresses(namespace string) IPAddressInterface { - return newIPAddresses(c, namespace) +func (c *IpamV1alpha1Client) IPAllocations(namespace string) IPAllocationInterface { + return newIPAllocations(c, namespace) } -func (c *IpamV1alpha1Client) IPAddressClaims(namespace string) IPAddressClaimInterface { - return newIPAddressClaims(c, namespace) +func (c *IpamV1alpha1Client) IPClaims(namespace string) IPClaimInterface { + return newIPClaims(c, namespace) } -func (c *IpamV1alpha1Client) IPPrefixes() IPPrefixInterface { - return newIPPrefixes(c) -} - -func (c *IpamV1alpha1Client) IPPrefixClaims(namespace string) IPPrefixClaimInterface { - return newIPPrefixClaims(c, namespace) -} - -func (c *IpamV1alpha1Client) IPPrefixClasses() IPPrefixClassInterface { - return newIPPrefixClasses(c) +func (c *IpamV1alpha1Client) IPPools() IPPoolInterface { + return newIPPools(c) } // NewForConfig creates a new IpamV1alpha1Client for the given config. diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipclaim.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipclaim.go new file mode 100644 index 0000000..d6d101b --- /dev/null +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipclaim.go @@ -0,0 +1,54 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// IPClaimsGetter has a method to return a IPClaimInterface. +// A group's client should implement this interface. +type IPClaimsGetter interface { + IPClaims(namespace string) IPClaimInterface +} + +// IPClaimInterface has methods to work with IPClaim resources. +type IPClaimInterface interface { + Create(ctx context.Context, iPClaim *ipamv1alpha1.IPClaim, opts v1.CreateOptions) (*ipamv1alpha1.IPClaim, error) + Update(ctx context.Context, iPClaim *ipamv1alpha1.IPClaim, opts v1.UpdateOptions) (*ipamv1alpha1.IPClaim, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, iPClaim *ipamv1alpha1.IPClaim, opts v1.UpdateOptions) (*ipamv1alpha1.IPClaim, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPClaim, error) + List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPClaimList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPClaim, err error) + IPClaimExpansion +} + +// iPClaims implements IPClaimInterface +type iPClaims struct { + *gentype.ClientWithList[*ipamv1alpha1.IPClaim, *ipamv1alpha1.IPClaimList] +} + +// newIPClaims returns a IPClaims +func newIPClaims(c *IpamV1alpha1Client, namespace string) *iPClaims { + return &iPClaims{ + gentype.NewClientWithList[*ipamv1alpha1.IPClaim, *ipamv1alpha1.IPClaimList]( + "ipclaims", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *ipamv1alpha1.IPClaim { return &ipamv1alpha1.IPClaim{} }, + func() *ipamv1alpha1.IPClaimList { return &ipamv1alpha1.IPClaimList{} }, + ), + } +} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ippool.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ippool.go new file mode 100644 index 0000000..d696fff --- /dev/null +++ b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ippool.go @@ -0,0 +1,54 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// IPPoolsGetter has a method to return a IPPoolInterface. +// A group's client should implement this interface. +type IPPoolsGetter interface { + IPPools() IPPoolInterface +} + +// IPPoolInterface has methods to work with IPPool resources. +type IPPoolInterface interface { + Create(ctx context.Context, iPPool *ipamv1alpha1.IPPool, opts v1.CreateOptions) (*ipamv1alpha1.IPPool, error) + Update(ctx context.Context, iPPool *ipamv1alpha1.IPPool, opts v1.UpdateOptions) (*ipamv1alpha1.IPPool, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, iPPool *ipamv1alpha1.IPPool, opts v1.UpdateOptions) (*ipamv1alpha1.IPPool, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPPool, error) + List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPPoolList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPPool, err error) + IPPoolExpansion +} + +// iPPools implements IPPoolInterface +type iPPools struct { + *gentype.ClientWithList[*ipamv1alpha1.IPPool, *ipamv1alpha1.IPPoolList] +} + +// newIPPools returns a IPPools +func newIPPools(c *IpamV1alpha1Client) *iPPools { + return &iPPools{ + gentype.NewClientWithList[*ipamv1alpha1.IPPool, *ipamv1alpha1.IPPoolList]( + "ippools", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *ipamv1alpha1.IPPool { return &ipamv1alpha1.IPPool{} }, + func() *ipamv1alpha1.IPPoolList { return &ipamv1alpha1.IPPoolList{} }, + ), + } +} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefix.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefix.go deleted file mode 100644 index c91f1ab..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefix.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// IPPrefixesGetter has a method to return a IPPrefixInterface. -// A group's client should implement this interface. -type IPPrefixesGetter interface { - IPPrefixes() IPPrefixInterface -} - -// IPPrefixInterface has methods to work with IPPrefix resources. -type IPPrefixInterface interface { - Create(ctx context.Context, iPPrefix *ipamv1alpha1.IPPrefix, opts v1.CreateOptions) (*ipamv1alpha1.IPPrefix, error) - Update(ctx context.Context, iPPrefix *ipamv1alpha1.IPPrefix, opts v1.UpdateOptions) (*ipamv1alpha1.IPPrefix, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, iPPrefix *ipamv1alpha1.IPPrefix, opts v1.UpdateOptions) (*ipamv1alpha1.IPPrefix, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPPrefix, error) - List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPPrefixList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPPrefix, err error) - IPPrefixExpansion -} - -// iPPrefixes implements IPPrefixInterface -type iPPrefixes struct { - *gentype.ClientWithList[*ipamv1alpha1.IPPrefix, *ipamv1alpha1.IPPrefixList] -} - -// newIPPrefixes returns a IPPrefixes -func newIPPrefixes(c *IpamV1alpha1Client) *iPPrefixes { - return &iPPrefixes{ - gentype.NewClientWithList[*ipamv1alpha1.IPPrefix, *ipamv1alpha1.IPPrefixList]( - "ipprefixes", - c.RESTClient(), - scheme.ParameterCodec, - "", - func() *ipamv1alpha1.IPPrefix { return &ipamv1alpha1.IPPrefix{} }, - func() *ipamv1alpha1.IPPrefixList { return &ipamv1alpha1.IPPrefixList{} }, - ), - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefixclaim.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefixclaim.go deleted file mode 100644 index d8887da..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefixclaim.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// IPPrefixClaimsGetter has a method to return a IPPrefixClaimInterface. -// A group's client should implement this interface. -type IPPrefixClaimsGetter interface { - IPPrefixClaims(namespace string) IPPrefixClaimInterface -} - -// IPPrefixClaimInterface has methods to work with IPPrefixClaim resources. -type IPPrefixClaimInterface interface { - Create(ctx context.Context, iPPrefixClaim *ipamv1alpha1.IPPrefixClaim, opts v1.CreateOptions) (*ipamv1alpha1.IPPrefixClaim, error) - Update(ctx context.Context, iPPrefixClaim *ipamv1alpha1.IPPrefixClaim, opts v1.UpdateOptions) (*ipamv1alpha1.IPPrefixClaim, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, iPPrefixClaim *ipamv1alpha1.IPPrefixClaim, opts v1.UpdateOptions) (*ipamv1alpha1.IPPrefixClaim, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPPrefixClaim, error) - List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPPrefixClaimList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPPrefixClaim, err error) - IPPrefixClaimExpansion -} - -// iPPrefixClaims implements IPPrefixClaimInterface -type iPPrefixClaims struct { - *gentype.ClientWithList[*ipamv1alpha1.IPPrefixClaim, *ipamv1alpha1.IPPrefixClaimList] -} - -// newIPPrefixClaims returns a IPPrefixClaims -func newIPPrefixClaims(c *IpamV1alpha1Client, namespace string) *iPPrefixClaims { - return &iPPrefixClaims{ - gentype.NewClientWithList[*ipamv1alpha1.IPPrefixClaim, *ipamv1alpha1.IPPrefixClaimList]( - "ipprefixclaims", - c.RESTClient(), - scheme.ParameterCodec, - namespace, - func() *ipamv1alpha1.IPPrefixClaim { return &ipamv1alpha1.IPPrefixClaim{} }, - func() *ipamv1alpha1.IPPrefixClaimList { return &ipamv1alpha1.IPPrefixClaimList{} }, - ), - } -} diff --git a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefixclass.go b/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefixclass.go deleted file mode 100644 index b469000..0000000 --- a/pkg/client/clientset/versioned/typed/ipam/v1alpha1/ipprefixclass.go +++ /dev/null @@ -1,52 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - scheme "go.miloapis.com/ipam/pkg/client/clientset/versioned/scheme" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// IPPrefixClassesGetter has a method to return a IPPrefixClassInterface. -// A group's client should implement this interface. -type IPPrefixClassesGetter interface { - IPPrefixClasses() IPPrefixClassInterface -} - -// IPPrefixClassInterface has methods to work with IPPrefixClass resources. -type IPPrefixClassInterface interface { - Create(ctx context.Context, iPPrefixClass *ipamv1alpha1.IPPrefixClass, opts v1.CreateOptions) (*ipamv1alpha1.IPPrefixClass, error) - Update(ctx context.Context, iPPrefixClass *ipamv1alpha1.IPPrefixClass, opts v1.UpdateOptions) (*ipamv1alpha1.IPPrefixClass, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*ipamv1alpha1.IPPrefixClass, error) - List(ctx context.Context, opts v1.ListOptions) (*ipamv1alpha1.IPPrefixClassList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ipamv1alpha1.IPPrefixClass, err error) - IPPrefixClassExpansion -} - -// iPPrefixClasses implements IPPrefixClassInterface -type iPPrefixClasses struct { - *gentype.ClientWithList[*ipamv1alpha1.IPPrefixClass, *ipamv1alpha1.IPPrefixClassList] -} - -// newIPPrefixClasses returns a IPPrefixClasses -func newIPPrefixClasses(c *IpamV1alpha1Client) *iPPrefixClasses { - return &iPPrefixClasses{ - gentype.NewClientWithList[*ipamv1alpha1.IPPrefixClass, *ipamv1alpha1.IPPrefixClassList]( - "ipprefixclasses", - c.RESTClient(), - scheme.ParameterCodec, - "", - func() *ipamv1alpha1.IPPrefixClass { return &ipamv1alpha1.IPPrefixClass{} }, - func() *ipamv1alpha1.IPPrefixClassList { return &ipamv1alpha1.IPPrefixClassList{} }, - ), - } -} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 37321b1..aaa50d9 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -37,16 +37,12 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=ipam.miloapis.com, Version=v1alpha1 - case v1alpha1.SchemeGroupVersion.WithResource("ipaddresses"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPAddresses().Informer()}, nil - case v1alpha1.SchemeGroupVersion.WithResource("ipaddressclaims"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPAddressClaims().Informer()}, nil - case v1alpha1.SchemeGroupVersion.WithResource("ipprefixes"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPPrefixes().Informer()}, nil - case v1alpha1.SchemeGroupVersion.WithResource("ipprefixclaims"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPPrefixClaims().Informer()}, nil - case v1alpha1.SchemeGroupVersion.WithResource("ipprefixclasses"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPPrefixClasses().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("ipallocations"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPAllocations().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("ipclaims"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPClaims().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("ippools"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Ipam().V1alpha1().IPPools().Informer()}, nil } diff --git a/pkg/client/informers/externalversions/ipam/v1alpha1/interface.go b/pkg/client/informers/externalversions/ipam/v1alpha1/interface.go index 1818bc0..fa5e2e5 100644 --- a/pkg/client/informers/externalversions/ipam/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/ipam/v1alpha1/interface.go @@ -8,16 +8,12 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { - // IPAddresses returns a IPAddressInformer. - IPAddresses() IPAddressInformer - // IPAddressClaims returns a IPAddressClaimInformer. - IPAddressClaims() IPAddressClaimInformer - // IPPrefixes returns a IPPrefixInformer. - IPPrefixes() IPPrefixInformer - // IPPrefixClaims returns a IPPrefixClaimInformer. - IPPrefixClaims() IPPrefixClaimInformer - // IPPrefixClasses returns a IPPrefixClassInformer. - IPPrefixClasses() IPPrefixClassInformer + // IPAllocations returns a IPAllocationInformer. + IPAllocations() IPAllocationInformer + // IPClaims returns a IPClaimInformer. + IPClaims() IPClaimInformer + // IPPools returns a IPPoolInformer. + IPPools() IPPoolInformer } type version struct { @@ -31,27 +27,17 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } -// IPAddresses returns a IPAddressInformer. -func (v *version) IPAddresses() IPAddressInformer { - return &iPAddressInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +// IPAllocations returns a IPAllocationInformer. +func (v *version) IPAllocations() IPAllocationInformer { + return &iPAllocationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } -// IPAddressClaims returns a IPAddressClaimInformer. -func (v *version) IPAddressClaims() IPAddressClaimInformer { - return &iPAddressClaimInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +// IPClaims returns a IPClaimInformer. +func (v *version) IPClaims() IPClaimInformer { + return &iPClaimInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } -// IPPrefixes returns a IPPrefixInformer. -func (v *version) IPPrefixes() IPPrefixInformer { - return &iPPrefixInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} -} - -// IPPrefixClaims returns a IPPrefixClaimInformer. -func (v *version) IPPrefixClaims() IPPrefixClaimInformer { - return &iPPrefixClaimInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} -} - -// IPPrefixClasses returns a IPPrefixClassInformer. -func (v *version) IPPrefixClasses() IPPrefixClassInformer { - return &iPPrefixClassInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +// IPPools returns a IPPoolInformer. +func (v *version) IPPools() IPPoolInformer { + return &iPPoolInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } diff --git a/pkg/client/informers/externalversions/ipam/v1alpha1/ipaddressclaim.go b/pkg/client/informers/externalversions/ipam/v1alpha1/ipaddressclaim.go deleted file mode 100644 index d4a1db3..0000000 --- a/pkg/client/informers/externalversions/ipam/v1alpha1/ipaddressclaim.go +++ /dev/null @@ -1,86 +0,0 @@ -// Code generated by informer-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - time "time" - - apisipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - versioned "go.miloapis.com/ipam/pkg/client/clientset/versioned" - internalinterfaces "go.miloapis.com/ipam/pkg/client/informers/externalversions/internalinterfaces" - ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/listers/ipam/v1alpha1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" - watch "k8s.io/apimachinery/pkg/watch" - cache "k8s.io/client-go/tools/cache" -) - -// IPAddressClaimInformer provides access to a shared informer and lister for -// IPAddressClaims. -type IPAddressClaimInformer interface { - Informer() cache.SharedIndexInformer - Lister() ipamv1alpha1.IPAddressClaimLister -} - -type iPAddressClaimInformer struct { - factory internalinterfaces.SharedInformerFactory - tweakListOptions internalinterfaces.TweakListOptionsFunc - namespace string -} - -// NewIPAddressClaimInformer constructs a new informer for IPAddressClaim type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewIPAddressClaimInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredIPAddressClaimInformer(client, namespace, resyncPeriod, indexers, nil) -} - -// NewFilteredIPAddressClaimInformer constructs a new informer for IPAddressClaim type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewFilteredIPAddressClaimInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ - ListFunc: func(options v1.ListOptions) (runtime.Object, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPAddressClaims(namespace).List(context.Background(), options) - }, - WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPAddressClaims(namespace).Watch(context.Background(), options) - }, - ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPAddressClaims(namespace).List(ctx, options) - }, - WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPAddressClaims(namespace).Watch(ctx, options) - }, - }, client), - &apisipamv1alpha1.IPAddressClaim{}, - resyncPeriod, - indexers, - ) -} - -func (f *iPAddressClaimInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredIPAddressClaimInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) -} - -func (f *iPAddressClaimInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apisipamv1alpha1.IPAddressClaim{}, f.defaultInformer) -} - -func (f *iPAddressClaimInformer) Lister() ipamv1alpha1.IPAddressClaimLister { - return ipamv1alpha1.NewIPAddressClaimLister(f.Informer().GetIndexer()) -} diff --git a/pkg/client/informers/externalversions/ipam/v1alpha1/ipprefixclaim.go b/pkg/client/informers/externalversions/ipam/v1alpha1/ipallocation.go similarity index 52% rename from pkg/client/informers/externalversions/ipam/v1alpha1/ipprefixclaim.go rename to pkg/client/informers/externalversions/ipam/v1alpha1/ipallocation.go index f0839b4..0086380 100644 --- a/pkg/client/informers/externalversions/ipam/v1alpha1/ipprefixclaim.go +++ b/pkg/client/informers/externalversions/ipam/v1alpha1/ipallocation.go @@ -16,71 +16,71 @@ import ( cache "k8s.io/client-go/tools/cache" ) -// IPPrefixClaimInformer provides access to a shared informer and lister for -// IPPrefixClaims. -type IPPrefixClaimInformer interface { +// IPAllocationInformer provides access to a shared informer and lister for +// IPAllocations. +type IPAllocationInformer interface { Informer() cache.SharedIndexInformer - Lister() ipamv1alpha1.IPPrefixClaimLister + Lister() ipamv1alpha1.IPAllocationLister } -type iPPrefixClaimInformer struct { +type iPAllocationInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } -// NewIPPrefixClaimInformer constructs a new informer for IPPrefixClaim type. +// NewIPAllocationInformer constructs a new informer for IPAllocation type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewIPPrefixClaimInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredIPPrefixClaimInformer(client, namespace, resyncPeriod, indexers, nil) +func NewIPAllocationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredIPAllocationInformer(client, namespace, resyncPeriod, indexers, nil) } -// NewFilteredIPPrefixClaimInformer constructs a new informer for IPPrefixClaim type. +// NewFilteredIPAllocationInformer constructs a new informer for IPAllocation type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredIPPrefixClaimInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredIPAllocationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixClaims(namespace).List(context.Background(), options) + return client.IpamV1alpha1().IPAllocations(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixClaims(namespace).Watch(context.Background(), options) + return client.IpamV1alpha1().IPAllocations(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixClaims(namespace).List(ctx, options) + return client.IpamV1alpha1().IPAllocations(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixClaims(namespace).Watch(ctx, options) + return client.IpamV1alpha1().IPAllocations(namespace).Watch(ctx, options) }, }, client), - &apisipamv1alpha1.IPPrefixClaim{}, + &apisipamv1alpha1.IPAllocation{}, resyncPeriod, indexers, ) } -func (f *iPPrefixClaimInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredIPPrefixClaimInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +func (f *iPAllocationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredIPAllocationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } -func (f *iPPrefixClaimInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apisipamv1alpha1.IPPrefixClaim{}, f.defaultInformer) +func (f *iPAllocationInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisipamv1alpha1.IPAllocation{}, f.defaultInformer) } -func (f *iPPrefixClaimInformer) Lister() ipamv1alpha1.IPPrefixClaimLister { - return ipamv1alpha1.NewIPPrefixClaimLister(f.Informer().GetIndexer()) +func (f *iPAllocationInformer) Lister() ipamv1alpha1.IPAllocationLister { + return ipamv1alpha1.NewIPAllocationLister(f.Informer().GetIndexer()) } diff --git a/pkg/client/informers/externalversions/ipam/v1alpha1/ipaddress.go b/pkg/client/informers/externalversions/ipam/v1alpha1/ipclaim.go similarity index 53% rename from pkg/client/informers/externalversions/ipam/v1alpha1/ipaddress.go rename to pkg/client/informers/externalversions/ipam/v1alpha1/ipclaim.go index 545445b..47faa90 100644 --- a/pkg/client/informers/externalversions/ipam/v1alpha1/ipaddress.go +++ b/pkg/client/informers/externalversions/ipam/v1alpha1/ipclaim.go @@ -16,71 +16,71 @@ import ( cache "k8s.io/client-go/tools/cache" ) -// IPAddressInformer provides access to a shared informer and lister for -// IPAddresses. -type IPAddressInformer interface { +// IPClaimInformer provides access to a shared informer and lister for +// IPClaims. +type IPClaimInformer interface { Informer() cache.SharedIndexInformer - Lister() ipamv1alpha1.IPAddressLister + Lister() ipamv1alpha1.IPClaimLister } -type iPAddressInformer struct { +type iPClaimInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } -// NewIPAddressInformer constructs a new informer for IPAddress type. +// NewIPClaimInformer constructs a new informer for IPClaim type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewIPAddressInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredIPAddressInformer(client, namespace, resyncPeriod, indexers, nil) +func NewIPClaimInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredIPClaimInformer(client, namespace, resyncPeriod, indexers, nil) } -// NewFilteredIPAddressInformer constructs a new informer for IPAddress type. +// NewFilteredIPClaimInformer constructs a new informer for IPClaim type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredIPAddressInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredIPClaimInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPAddresses(namespace).List(context.Background(), options) + return client.IpamV1alpha1().IPClaims(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPAddresses(namespace).Watch(context.Background(), options) + return client.IpamV1alpha1().IPClaims(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPAddresses(namespace).List(ctx, options) + return client.IpamV1alpha1().IPClaims(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPAddresses(namespace).Watch(ctx, options) + return client.IpamV1alpha1().IPClaims(namespace).Watch(ctx, options) }, }, client), - &apisipamv1alpha1.IPAddress{}, + &apisipamv1alpha1.IPClaim{}, resyncPeriod, indexers, ) } -func (f *iPAddressInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredIPAddressInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +func (f *iPClaimInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredIPClaimInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } -func (f *iPAddressInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apisipamv1alpha1.IPAddress{}, f.defaultInformer) +func (f *iPClaimInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisipamv1alpha1.IPClaim{}, f.defaultInformer) } -func (f *iPAddressInformer) Lister() ipamv1alpha1.IPAddressLister { - return ipamv1alpha1.NewIPAddressLister(f.Informer().GetIndexer()) +func (f *iPClaimInformer) Lister() ipamv1alpha1.IPClaimLister { + return ipamv1alpha1.NewIPClaimLister(f.Informer().GetIndexer()) } diff --git a/pkg/client/informers/externalversions/ipam/v1alpha1/ipprefix.go b/pkg/client/informers/externalversions/ipam/v1alpha1/ippool.go similarity index 54% rename from pkg/client/informers/externalversions/ipam/v1alpha1/ipprefix.go rename to pkg/client/informers/externalversions/ipam/v1alpha1/ippool.go index 2b40ac3..6063d72 100644 --- a/pkg/client/informers/externalversions/ipam/v1alpha1/ipprefix.go +++ b/pkg/client/informers/externalversions/ipam/v1alpha1/ippool.go @@ -16,70 +16,70 @@ import ( cache "k8s.io/client-go/tools/cache" ) -// IPPrefixInformer provides access to a shared informer and lister for -// IPPrefixes. -type IPPrefixInformer interface { +// IPPoolInformer provides access to a shared informer and lister for +// IPPools. +type IPPoolInformer interface { Informer() cache.SharedIndexInformer - Lister() ipamv1alpha1.IPPrefixLister + Lister() ipamv1alpha1.IPPoolLister } -type iPPrefixInformer struct { +type iPPoolInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc } -// NewIPPrefixInformer constructs a new informer for IPPrefix type. +// NewIPPoolInformer constructs a new informer for IPPool type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewIPPrefixInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredIPPrefixInformer(client, resyncPeriod, indexers, nil) +func NewIPPoolInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredIPPoolInformer(client, resyncPeriod, indexers, nil) } -// NewFilteredIPPrefixInformer constructs a new informer for IPPrefix type. +// NewFilteredIPPoolInformer constructs a new informer for IPPool type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredIPPrefixInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredIPPoolInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixes().List(context.Background(), options) + return client.IpamV1alpha1().IPPools().List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixes().Watch(context.Background(), options) + return client.IpamV1alpha1().IPPools().Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixes().List(ctx, options) + return client.IpamV1alpha1().IPPools().List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.IpamV1alpha1().IPPrefixes().Watch(ctx, options) + return client.IpamV1alpha1().IPPools().Watch(ctx, options) }, }, client), - &apisipamv1alpha1.IPPrefix{}, + &apisipamv1alpha1.IPPool{}, resyncPeriod, indexers, ) } -func (f *iPPrefixInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredIPPrefixInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +func (f *iPPoolInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredIPPoolInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } -func (f *iPPrefixInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apisipamv1alpha1.IPPrefix{}, f.defaultInformer) +func (f *iPPoolInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisipamv1alpha1.IPPool{}, f.defaultInformer) } -func (f *iPPrefixInformer) Lister() ipamv1alpha1.IPPrefixLister { - return ipamv1alpha1.NewIPPrefixLister(f.Informer().GetIndexer()) +func (f *iPPoolInformer) Lister() ipamv1alpha1.IPPoolLister { + return ipamv1alpha1.NewIPPoolLister(f.Informer().GetIndexer()) } diff --git a/pkg/client/informers/externalversions/ipam/v1alpha1/ipprefixclass.go b/pkg/client/informers/externalversions/ipam/v1alpha1/ipprefixclass.go deleted file mode 100644 index 44ad617..0000000 --- a/pkg/client/informers/externalversions/ipam/v1alpha1/ipprefixclass.go +++ /dev/null @@ -1,85 +0,0 @@ -// Code generated by informer-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - time "time" - - apisipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - versioned "go.miloapis.com/ipam/pkg/client/clientset/versioned" - internalinterfaces "go.miloapis.com/ipam/pkg/client/informers/externalversions/internalinterfaces" - ipamv1alpha1 "go.miloapis.com/ipam/pkg/client/listers/ipam/v1alpha1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" - watch "k8s.io/apimachinery/pkg/watch" - cache "k8s.io/client-go/tools/cache" -) - -// IPPrefixClassInformer provides access to a shared informer and lister for -// IPPrefixClasses. -type IPPrefixClassInformer interface { - Informer() cache.SharedIndexInformer - Lister() ipamv1alpha1.IPPrefixClassLister -} - -type iPPrefixClassInformer struct { - factory internalinterfaces.SharedInformerFactory - tweakListOptions internalinterfaces.TweakListOptionsFunc -} - -// NewIPPrefixClassInformer constructs a new informer for IPPrefixClass type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewIPPrefixClassInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredIPPrefixClassInformer(client, resyncPeriod, indexers, nil) -} - -// NewFilteredIPPrefixClassInformer constructs a new informer for IPPrefixClass type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewFilteredIPPrefixClassInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ - ListFunc: func(options v1.ListOptions) (runtime.Object, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPPrefixClasses().List(context.Background(), options) - }, - WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPPrefixClasses().Watch(context.Background(), options) - }, - ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPPrefixClasses().List(ctx, options) - }, - WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.IpamV1alpha1().IPPrefixClasses().Watch(ctx, options) - }, - }, client), - &apisipamv1alpha1.IPPrefixClass{}, - resyncPeriod, - indexers, - ) -} - -func (f *iPPrefixClassInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredIPPrefixClassInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) -} - -func (f *iPPrefixClassInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apisipamv1alpha1.IPPrefixClass{}, f.defaultInformer) -} - -func (f *iPPrefixClassInformer) Lister() ipamv1alpha1.IPPrefixClassLister { - return ipamv1alpha1.NewIPPrefixClassLister(f.Informer().GetIndexer()) -} diff --git a/pkg/client/listers/ipam/v1alpha1/expansion_generated.go b/pkg/client/listers/ipam/v1alpha1/expansion_generated.go index 980bc0c..bb1c070 100644 --- a/pkg/client/listers/ipam/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/ipam/v1alpha1/expansion_generated.go @@ -2,34 +2,22 @@ package v1alpha1 -// IPAddressListerExpansion allows custom methods to be added to -// IPAddressLister. -type IPAddressListerExpansion interface{} +// IPAllocationListerExpansion allows custom methods to be added to +// IPAllocationLister. +type IPAllocationListerExpansion interface{} -// IPAddressNamespaceListerExpansion allows custom methods to be added to -// IPAddressNamespaceLister. -type IPAddressNamespaceListerExpansion interface{} +// IPAllocationNamespaceListerExpansion allows custom methods to be added to +// IPAllocationNamespaceLister. +type IPAllocationNamespaceListerExpansion interface{} -// IPAddressClaimListerExpansion allows custom methods to be added to -// IPAddressClaimLister. -type IPAddressClaimListerExpansion interface{} +// IPClaimListerExpansion allows custom methods to be added to +// IPClaimLister. +type IPClaimListerExpansion interface{} -// IPAddressClaimNamespaceListerExpansion allows custom methods to be added to -// IPAddressClaimNamespaceLister. -type IPAddressClaimNamespaceListerExpansion interface{} +// IPClaimNamespaceListerExpansion allows custom methods to be added to +// IPClaimNamespaceLister. +type IPClaimNamespaceListerExpansion interface{} -// IPPrefixListerExpansion allows custom methods to be added to -// IPPrefixLister. -type IPPrefixListerExpansion interface{} - -// IPPrefixClaimListerExpansion allows custom methods to be added to -// IPPrefixClaimLister. -type IPPrefixClaimListerExpansion interface{} - -// IPPrefixClaimNamespaceListerExpansion allows custom methods to be added to -// IPPrefixClaimNamespaceLister. -type IPPrefixClaimNamespaceListerExpansion interface{} - -// IPPrefixClassListerExpansion allows custom methods to be added to -// IPPrefixClassLister. -type IPPrefixClassListerExpansion interface{} +// IPPoolListerExpansion allows custom methods to be added to +// IPPoolLister. +type IPPoolListerExpansion interface{} diff --git a/pkg/client/listers/ipam/v1alpha1/ipaddress.go b/pkg/client/listers/ipam/v1alpha1/ipaddress.go deleted file mode 100644 index ff274dc..0000000 --- a/pkg/client/listers/ipam/v1alpha1/ipaddress.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// IPAddressLister helps list IPAddresses. -// All objects returned here must be treated as read-only. -type IPAddressLister interface { - // List lists all IPAddresses in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPAddress, err error) - // IPAddresses returns an object that can list and get IPAddresses. - IPAddresses(namespace string) IPAddressNamespaceLister - IPAddressListerExpansion -} - -// iPAddressLister implements the IPAddressLister interface. -type iPAddressLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPAddress] -} - -// NewIPAddressLister returns a new IPAddressLister. -func NewIPAddressLister(indexer cache.Indexer) IPAddressLister { - return &iPAddressLister{listers.New[*ipamv1alpha1.IPAddress](indexer, ipamv1alpha1.Resource("ipaddress"))} -} - -// IPAddresses returns an object that can list and get IPAddresses. -func (s *iPAddressLister) IPAddresses(namespace string) IPAddressNamespaceLister { - return iPAddressNamespaceLister{listers.NewNamespaced[*ipamv1alpha1.IPAddress](s.ResourceIndexer, namespace)} -} - -// IPAddressNamespaceLister helps list and get IPAddresses. -// All objects returned here must be treated as read-only. -type IPAddressNamespaceLister interface { - // List lists all IPAddresses in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPAddress, err error) - // Get retrieves the IPAddress from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*ipamv1alpha1.IPAddress, error) - IPAddressNamespaceListerExpansion -} - -// iPAddressNamespaceLister implements the IPAddressNamespaceLister -// interface. -type iPAddressNamespaceLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPAddress] -} diff --git a/pkg/client/listers/ipam/v1alpha1/ipaddressclaim.go b/pkg/client/listers/ipam/v1alpha1/ipaddressclaim.go deleted file mode 100644 index 29f7f63..0000000 --- a/pkg/client/listers/ipam/v1alpha1/ipaddressclaim.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// IPAddressClaimLister helps list IPAddressClaims. -// All objects returned here must be treated as read-only. -type IPAddressClaimLister interface { - // List lists all IPAddressClaims in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPAddressClaim, err error) - // IPAddressClaims returns an object that can list and get IPAddressClaims. - IPAddressClaims(namespace string) IPAddressClaimNamespaceLister - IPAddressClaimListerExpansion -} - -// iPAddressClaimLister implements the IPAddressClaimLister interface. -type iPAddressClaimLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPAddressClaim] -} - -// NewIPAddressClaimLister returns a new IPAddressClaimLister. -func NewIPAddressClaimLister(indexer cache.Indexer) IPAddressClaimLister { - return &iPAddressClaimLister{listers.New[*ipamv1alpha1.IPAddressClaim](indexer, ipamv1alpha1.Resource("ipaddressclaim"))} -} - -// IPAddressClaims returns an object that can list and get IPAddressClaims. -func (s *iPAddressClaimLister) IPAddressClaims(namespace string) IPAddressClaimNamespaceLister { - return iPAddressClaimNamespaceLister{listers.NewNamespaced[*ipamv1alpha1.IPAddressClaim](s.ResourceIndexer, namespace)} -} - -// IPAddressClaimNamespaceLister helps list and get IPAddressClaims. -// All objects returned here must be treated as read-only. -type IPAddressClaimNamespaceLister interface { - // List lists all IPAddressClaims in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPAddressClaim, err error) - // Get retrieves the IPAddressClaim from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*ipamv1alpha1.IPAddressClaim, error) - IPAddressClaimNamespaceListerExpansion -} - -// iPAddressClaimNamespaceLister implements the IPAddressClaimNamespaceLister -// interface. -type iPAddressClaimNamespaceLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPAddressClaim] -} diff --git a/pkg/client/listers/ipam/v1alpha1/ipallocation.go b/pkg/client/listers/ipam/v1alpha1/ipallocation.go new file mode 100644 index 0000000..243b148 --- /dev/null +++ b/pkg/client/listers/ipam/v1alpha1/ipallocation.go @@ -0,0 +1,54 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// IPAllocationLister helps list IPAllocations. +// All objects returned here must be treated as read-only. +type IPAllocationLister interface { + // List lists all IPAllocations in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*ipamv1alpha1.IPAllocation, err error) + // IPAllocations returns an object that can list and get IPAllocations. + IPAllocations(namespace string) IPAllocationNamespaceLister + IPAllocationListerExpansion +} + +// iPAllocationLister implements the IPAllocationLister interface. +type iPAllocationLister struct { + listers.ResourceIndexer[*ipamv1alpha1.IPAllocation] +} + +// NewIPAllocationLister returns a new IPAllocationLister. +func NewIPAllocationLister(indexer cache.Indexer) IPAllocationLister { + return &iPAllocationLister{listers.New[*ipamv1alpha1.IPAllocation](indexer, ipamv1alpha1.Resource("ipallocation"))} +} + +// IPAllocations returns an object that can list and get IPAllocations. +func (s *iPAllocationLister) IPAllocations(namespace string) IPAllocationNamespaceLister { + return iPAllocationNamespaceLister{listers.NewNamespaced[*ipamv1alpha1.IPAllocation](s.ResourceIndexer, namespace)} +} + +// IPAllocationNamespaceLister helps list and get IPAllocations. +// All objects returned here must be treated as read-only. +type IPAllocationNamespaceLister interface { + // List lists all IPAllocations in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*ipamv1alpha1.IPAllocation, err error) + // Get retrieves the IPAllocation from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*ipamv1alpha1.IPAllocation, error) + IPAllocationNamespaceListerExpansion +} + +// iPAllocationNamespaceLister implements the IPAllocationNamespaceLister +// interface. +type iPAllocationNamespaceLister struct { + listers.ResourceIndexer[*ipamv1alpha1.IPAllocation] +} diff --git a/pkg/client/listers/ipam/v1alpha1/ipclaim.go b/pkg/client/listers/ipam/v1alpha1/ipclaim.go new file mode 100644 index 0000000..37b3b1d --- /dev/null +++ b/pkg/client/listers/ipam/v1alpha1/ipclaim.go @@ -0,0 +1,54 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// IPClaimLister helps list IPClaims. +// All objects returned here must be treated as read-only. +type IPClaimLister interface { + // List lists all IPClaims in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*ipamv1alpha1.IPClaim, err error) + // IPClaims returns an object that can list and get IPClaims. + IPClaims(namespace string) IPClaimNamespaceLister + IPClaimListerExpansion +} + +// iPClaimLister implements the IPClaimLister interface. +type iPClaimLister struct { + listers.ResourceIndexer[*ipamv1alpha1.IPClaim] +} + +// NewIPClaimLister returns a new IPClaimLister. +func NewIPClaimLister(indexer cache.Indexer) IPClaimLister { + return &iPClaimLister{listers.New[*ipamv1alpha1.IPClaim](indexer, ipamv1alpha1.Resource("ipclaim"))} +} + +// IPClaims returns an object that can list and get IPClaims. +func (s *iPClaimLister) IPClaims(namespace string) IPClaimNamespaceLister { + return iPClaimNamespaceLister{listers.NewNamespaced[*ipamv1alpha1.IPClaim](s.ResourceIndexer, namespace)} +} + +// IPClaimNamespaceLister helps list and get IPClaims. +// All objects returned here must be treated as read-only. +type IPClaimNamespaceLister interface { + // List lists all IPClaims in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*ipamv1alpha1.IPClaim, err error) + // Get retrieves the IPClaim from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*ipamv1alpha1.IPClaim, error) + IPClaimNamespaceListerExpansion +} + +// iPClaimNamespaceLister implements the IPClaimNamespaceLister +// interface. +type iPClaimNamespaceLister struct { + listers.ResourceIndexer[*ipamv1alpha1.IPClaim] +} diff --git a/pkg/client/listers/ipam/v1alpha1/ippool.go b/pkg/client/listers/ipam/v1alpha1/ippool.go new file mode 100644 index 0000000..6e13dd5 --- /dev/null +++ b/pkg/client/listers/ipam/v1alpha1/ippool.go @@ -0,0 +1,32 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// IPPoolLister helps list IPPools. +// All objects returned here must be treated as read-only. +type IPPoolLister interface { + // List lists all IPPools in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*ipamv1alpha1.IPPool, err error) + // Get retrieves the IPPool from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*ipamv1alpha1.IPPool, error) + IPPoolListerExpansion +} + +// iPPoolLister implements the IPPoolLister interface. +type iPPoolLister struct { + listers.ResourceIndexer[*ipamv1alpha1.IPPool] +} + +// NewIPPoolLister returns a new IPPoolLister. +func NewIPPoolLister(indexer cache.Indexer) IPPoolLister { + return &iPPoolLister{listers.New[*ipamv1alpha1.IPPool](indexer, ipamv1alpha1.Resource("ippool"))} +} diff --git a/pkg/client/listers/ipam/v1alpha1/ipprefix.go b/pkg/client/listers/ipam/v1alpha1/ipprefix.go deleted file mode 100644 index d9e19bd..0000000 --- a/pkg/client/listers/ipam/v1alpha1/ipprefix.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// IPPrefixLister helps list IPPrefixes. -// All objects returned here must be treated as read-only. -type IPPrefixLister interface { - // List lists all IPPrefixes in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPPrefix, err error) - // Get retrieves the IPPrefix from the index for a given name. - // Objects returned here must be treated as read-only. - Get(name string) (*ipamv1alpha1.IPPrefix, error) - IPPrefixListerExpansion -} - -// iPPrefixLister implements the IPPrefixLister interface. -type iPPrefixLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPPrefix] -} - -// NewIPPrefixLister returns a new IPPrefixLister. -func NewIPPrefixLister(indexer cache.Indexer) IPPrefixLister { - return &iPPrefixLister{listers.New[*ipamv1alpha1.IPPrefix](indexer, ipamv1alpha1.Resource("ipprefix"))} -} diff --git a/pkg/client/listers/ipam/v1alpha1/ipprefixclaim.go b/pkg/client/listers/ipam/v1alpha1/ipprefixclaim.go deleted file mode 100644 index 3509e0e..0000000 --- a/pkg/client/listers/ipam/v1alpha1/ipprefixclaim.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// IPPrefixClaimLister helps list IPPrefixClaims. -// All objects returned here must be treated as read-only. -type IPPrefixClaimLister interface { - // List lists all IPPrefixClaims in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPPrefixClaim, err error) - // IPPrefixClaims returns an object that can list and get IPPrefixClaims. - IPPrefixClaims(namespace string) IPPrefixClaimNamespaceLister - IPPrefixClaimListerExpansion -} - -// iPPrefixClaimLister implements the IPPrefixClaimLister interface. -type iPPrefixClaimLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPPrefixClaim] -} - -// NewIPPrefixClaimLister returns a new IPPrefixClaimLister. -func NewIPPrefixClaimLister(indexer cache.Indexer) IPPrefixClaimLister { - return &iPPrefixClaimLister{listers.New[*ipamv1alpha1.IPPrefixClaim](indexer, ipamv1alpha1.Resource("ipprefixclaim"))} -} - -// IPPrefixClaims returns an object that can list and get IPPrefixClaims. -func (s *iPPrefixClaimLister) IPPrefixClaims(namespace string) IPPrefixClaimNamespaceLister { - return iPPrefixClaimNamespaceLister{listers.NewNamespaced[*ipamv1alpha1.IPPrefixClaim](s.ResourceIndexer, namespace)} -} - -// IPPrefixClaimNamespaceLister helps list and get IPPrefixClaims. -// All objects returned here must be treated as read-only. -type IPPrefixClaimNamespaceLister interface { - // List lists all IPPrefixClaims in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPPrefixClaim, err error) - // Get retrieves the IPPrefixClaim from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*ipamv1alpha1.IPPrefixClaim, error) - IPPrefixClaimNamespaceListerExpansion -} - -// iPPrefixClaimNamespaceLister implements the IPPrefixClaimNamespaceLister -// interface. -type iPPrefixClaimNamespaceLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPPrefixClaim] -} diff --git a/pkg/client/listers/ipam/v1alpha1/ipprefixclass.go b/pkg/client/listers/ipam/v1alpha1/ipprefixclass.go deleted file mode 100644 index e3edbfb..0000000 --- a/pkg/client/listers/ipam/v1alpha1/ipprefixclass.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - ipamv1alpha1 "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// IPPrefixClassLister helps list IPPrefixClasses. -// All objects returned here must be treated as read-only. -type IPPrefixClassLister interface { - // List lists all IPPrefixClasses in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*ipamv1alpha1.IPPrefixClass, err error) - // Get retrieves the IPPrefixClass from the index for a given name. - // Objects returned here must be treated as read-only. - Get(name string) (*ipamv1alpha1.IPPrefixClass, error) - IPPrefixClassListerExpansion -} - -// iPPrefixClassLister implements the IPPrefixClassLister interface. -type iPPrefixClassLister struct { - listers.ResourceIndexer[*ipamv1alpha1.IPPrefixClass] -} - -// NewIPPrefixClassLister returns a new IPPrefixClassLister. -func NewIPPrefixClassLister(indexer cache.Indexer) IPPrefixClassLister { - return &iPPrefixClassLister{listers.New[*ipamv1alpha1.IPPrefixClass](indexer, ipamv1alpha1.Resource("ipprefixclass"))} -} diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 309cc07..e76d8ce 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -16,96 +16,87 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.AllocationSpec": schema_pkg_apis_ipam_v1alpha1_AllocationSpec(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddress": schema_pkg_apis_ipam_v1alpha1_IPAddress(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaim": schema_pkg_apis_ipam_v1alpha1_IPAddressClaim(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaimList": schema_pkg_apis_ipam_v1alpha1_IPAddressClaimList(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaimSpec": schema_pkg_apis_ipam_v1alpha1_IPAddressClaimSpec(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaimStatus": schema_pkg_apis_ipam_v1alpha1_IPAddressClaimStatus(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressList": schema_pkg_apis_ipam_v1alpha1_IPAddressList(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressSpec": schema_pkg_apis_ipam_v1alpha1_IPAddressSpec(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressStatus": schema_pkg_apis_ipam_v1alpha1_IPAddressStatus(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefix": schema_pkg_apis_ipam_v1alpha1_IPPrefix(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaim": schema_pkg_apis_ipam_v1alpha1_IPPrefixClaim(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaimList": schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimList(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaimSpec": schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimSpec(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaimStatus": schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimStatus(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClass": schema_pkg_apis_ipam_v1alpha1_IPPrefixClass(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClassList": schema_pkg_apis_ipam_v1alpha1_IPPrefixClassList(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClassSpec": schema_pkg_apis_ipam_v1alpha1_IPPrefixClassSpec(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixList": schema_pkg_apis_ipam_v1alpha1_IPPrefixList(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixSpec": schema_pkg_apis_ipam_v1alpha1_IPPrefixSpec(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixStatus": schema_pkg_apis_ipam_v1alpha1_IPPrefixStatus(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixTemplate": schema_pkg_apis_ipam_v1alpha1_IPPrefixTemplate(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef": schema_pkg_apis_ipam_v1alpha1_LocalRef(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.NamespacedRef": schema_pkg_apis_ipam_v1alpha1_NamespacedRef(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef": schema_pkg_apis_ipam_v1alpha1_ObjectRef(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixCapacity": schema_pkg_apis_ipam_v1alpha1_PrefixCapacity(ref), - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixSelector": schema_pkg_apis_ipam_v1alpha1_PrefixSelector(ref), - resource.Quantity{}.OpenAPIModelName(): schema_apimachinery_pkg_api_resource_Quantity(ref), - v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), - v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), - v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), - v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), - v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), - v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), - v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), - v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), - v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), - v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), - v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), - v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), - v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), - v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), - v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), - v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), - v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), - v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), - v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), - v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), - v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), - v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), - v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), - v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), - v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), - v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), - v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), - v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), - v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), - v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), - v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), - v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), - v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), - v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), - v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), - v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), - v1.ShardInfo{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ShardInfo(ref), - v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), - v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), - v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), - v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), - v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), - v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), - v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), - v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), - v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), - v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), - v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), - v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), - v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), - runtime.RawExtension{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), - runtime.TypeMeta{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), - runtime.Unknown{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), - version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.AllocationSpec": schema_pkg_apis_ipam_v1alpha1_AllocationSpec(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocation": schema_pkg_apis_ipam_v1alpha1_IPAllocation(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocationList": schema_pkg_apis_ipam_v1alpha1_IPAllocationList(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocationSpec": schema_pkg_apis_ipam_v1alpha1_IPAllocationSpec(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocationStatus": schema_pkg_apis_ipam_v1alpha1_IPAllocationStatus(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaim": schema_pkg_apis_ipam_v1alpha1_IPClaim(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaimList": schema_pkg_apis_ipam_v1alpha1_IPClaimList(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaimSpec": schema_pkg_apis_ipam_v1alpha1_IPClaimSpec(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaimStatus": schema_pkg_apis_ipam_v1alpha1_IPClaimStatus(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPool": schema_pkg_apis_ipam_v1alpha1_IPPool(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPoolList": schema_pkg_apis_ipam_v1alpha1_IPPoolList(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPoolSpec": schema_pkg_apis_ipam_v1alpha1_IPPoolSpec(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPoolStatus": schema_pkg_apis_ipam_v1alpha1_IPPoolStatus(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef": schema_pkg_apis_ipam_v1alpha1_LocalRef(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.NamespacedRef": schema_pkg_apis_ipam_v1alpha1_NamespacedRef(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef": schema_pkg_apis_ipam_v1alpha1_ObjectRef(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolCapacity": schema_pkg_apis_ipam_v1alpha1_PoolCapacity(ref), + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolSelector": schema_pkg_apis_ipam_v1alpha1_PoolSelector(ref), + resource.Quantity{}.OpenAPIModelName(): schema_apimachinery_pkg_api_resource_Quantity(ref), + v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), + v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), + v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), + v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), + v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), + v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), + v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), + v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), + v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), + v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), + v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), + v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), + v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), + v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), + v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), + v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), + v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), + v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), + v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), + v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), + v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), + v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), + v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), + v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), + v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), + v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), + v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), + v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), + v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), + v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), + v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + v1.ShardInfo{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ShardInfo(ref), + v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), + v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), + v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), + v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), + v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), + v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), + v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), + v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), + v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), + v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), + v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), + v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), + runtime.RawExtension{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), + runtime.TypeMeta{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), + runtime.Unknown{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), + version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), } } - func schema_pkg_apis_ipam_v1alpha1_AllocationSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "AllocationSpec configures sub-allocation behaviour for a prefix.", + Description: "AllocationSpec configures sub-allocation behaviour for a pool.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "minPrefixLength": { @@ -132,57 +123,12 @@ func schema_pkg_apis_ipam_v1alpha1_AllocationSpec(ref common.ReferenceCallback) } } -func schema_pkg_apis_ipam_v1alpha1_IPAddress(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressStatus"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressStatus", v1.ObjectMeta{}.OpenAPIModelName()}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPAddressClaim(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPAllocation(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, + Description: "IPAllocation records a CIDR carved out of an IPPool by an IPClaim.", + Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { SchemaProps: spec.SchemaProps{ @@ -207,24 +153,24 @@ func schema_pkg_apis_ipam_v1alpha1_IPAddressClaim(ref common.ReferenceCallback) "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaimSpec"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocationSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaimStatus"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocationStatus"), }, }, }, }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaimSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaimStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocationSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocationStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } -func schema_pkg_apis_ipam_v1alpha1_IPAddressClaimList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPAllocationList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -257,7 +203,7 @@ func schema_pkg_apis_ipam_v1alpha1_IPAddressClaimList(ref common.ReferenceCallba Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaim"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocation"), }, }, }, @@ -268,54 +214,46 @@ func schema_pkg_apis_ipam_v1alpha1_IPAddressClaimList(ref common.ReferenceCallba }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddressClaim", v1.ListMeta{}.OpenAPIModelName()}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAllocation", v1.ListMeta{}.OpenAPIModelName()}, } } -func schema_pkg_apis_ipam_v1alpha1_IPAddressClaimSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPAllocationSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "ipFamily": { + "cidr": { SchemaProps: spec.SchemaProps{ Default: "", Type: []string{"string"}, Format: "", }, }, - "prefixSelector": { - SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixSelector"), - }, - }, - "prefixRef": { - SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.NamespacedRef"), - }, - }, - "reclaimPolicy": { + "ipFamily": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Default: "", + Type: []string{"string"}, + Format: "", }, }, - "ownerRef": { + "poolRef": { SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef"), + Default: map[string]interface{}{}, + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"), }, }, }, - Required: []string{"ipFamily"}, + Required: []string{"cidr", "ipFamily", "poolRef"}, }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.NamespacedRef", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixSelector"}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"}, } } -func schema_pkg_apis_ipam_v1alpha1_IPAddressClaimStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPAllocationStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -327,140 +265,18 @@ func schema_pkg_apis_ipam_v1alpha1_IPAddressClaimStatus(ref common.ReferenceCall Format: "", }, }, - "allocatedIP": { + "cidr": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", }, }, - "boundAddressRef": { - SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"), - }, - }, - "conditions": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-map-keys": []interface{}{ - "type", - }, - "x-kubernetes-list-type": "map", - }, - }, - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.Condition{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef", v1.Condition{}.OpenAPIModelName()}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPAddressList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.ListMeta{}.OpenAPIModelName()), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddress"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPAddress", v1.ListMeta{}.OpenAPIModelName()}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPAddressSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "address": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "ipFamily": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "prefixRef": { + "capacity": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"), - }, - }, - "claimRef": { - SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolCapacity"), }, }, - }, - Required: []string{"address", "ipFamily", "prefixRef"}, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPAddressStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ "conditions": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -486,58 +302,11 @@ func schema_pkg_apis_ipam_v1alpha1_IPAddressStatus(ref common.ReferenceCallback) }, }, Dependencies: []string{ - v1.Condition{}.OpenAPIModelName()}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPPrefix(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "IPPrefix is a CIDR pool from which sub-prefixes or addresses can be allocated.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixStatus"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolCapacity", v1.Condition{}.OpenAPIModelName()}, } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaim(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPClaim(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -566,24 +335,24 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaim(ref common.ReferenceCallback) c "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaimSpec"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaimSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaimStatus"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaimStatus"), }, }, }, }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaimSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaimStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaimSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaimStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPClaimList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -616,7 +385,7 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimList(ref common.ReferenceCallbac Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaim"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaim"), }, }, }, @@ -627,11 +396,11 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimList(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClaim", v1.ListMeta{}.OpenAPIModelName()}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPClaim", v1.ListMeta{}.OpenAPIModelName()}, } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPClaimSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -652,21 +421,16 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimSpec(ref common.ReferenceCallbac Format: "int32", }, }, - "prefixSelector": { + "poolSelector": { SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixSelector"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolSelector"), }, }, - "prefixRef": { + "poolRef": { SchemaProps: spec.SchemaProps{ Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.NamespacedRef"), }, }, - "childPrefixTemplate": { - SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixTemplate"), - }, - }, "reclaimPolicy": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -683,11 +447,11 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimSpec(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixTemplate", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.NamespacedRef", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixSelector"}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.NamespacedRef", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolSelector"}, } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPClaimStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -705,7 +469,7 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimStatus(ref common.ReferenceCallb Format: "", }, }, - "boundPrefixRef": { + "boundAllocationRef": { SchemaProps: spec.SchemaProps{ Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"), }, @@ -739,11 +503,11 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClaimStatus(ref common.ReferenceCallb } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixClass(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPPool(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "IPPrefixClass declares operational properties shared by a class of IPPrefix pools.", + Description: "IPPool is an allocatable address space. Root pools declare a CIDR directly; child pools carve a sub-prefix from a parent pool.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -769,100 +533,24 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixClass(ref common.ReferenceCallback) c "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClassSpec"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPoolSpec"), }, }, - }, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClassSpec", v1.ObjectMeta{}.OpenAPIModelName()}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPPrefixClassList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "IPPrefixClassList is a list of IPPrefixClass.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.ListMeta{}.OpenAPIModelName()), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClass"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixClass", v1.ListMeta{}.OpenAPIModelName()}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPPrefixClassSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "requiresVerification": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "visibility": { - SchemaProps: spec.SchemaProps{ - Description: "Visibility controls cross-project access semantics for IPPrefix pools that reference this class. \"platform\" pools are platform-only (callers see them only when running with platform scope); \"consumer\" pools are visible to a single project; \"shared\" pools are eligible for cross-project allocation via prefixSelector.projectRef gated by a SubjectAccessReview.", - Type: []string{"string"}, - Format: "", - }, - }, - "defaultAllocation": { + "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.AllocationSpec"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPoolStatus"), }, }, }, }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.AllocationSpec"}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPoolSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPoolStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPPoolList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -895,7 +583,7 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixList(ref common.ReferenceCallback) co Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefix"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPool"), }, }, }, @@ -906,11 +594,11 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixList(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefix", v1.ListMeta{}.OpenAPIModelName()}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPool", v1.ListMeta{}.OpenAPIModelName()}, } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPPoolSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -918,23 +606,25 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixSpec(ref common.ReferenceCallback) co Properties: map[string]spec.Schema{ "cidr": { SchemaProps: spec.SchemaProps{ - Description: "CIDR is the parent prefix in canonical form, e.g. \"10.0.0.0/8\" (IPv4) or \"2001:db8::/32\" (IPv6). Validation parses with net.ParseCIDR and rejects malformed values.", - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "ipFamily": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, - "classRef": { + "parentPoolRef": { SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"), + }, + }, + "prefixLength": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int32", }, }, "allocation": { @@ -943,21 +633,21 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixSpec(ref common.ReferenceCallback) co Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.AllocationSpec"), }, }, - "parentRef": { + "visibility": { SchemaProps: spec.SchemaProps{ - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef"), + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"cidr", "ipFamily", "classRef"}, }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.AllocationSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.ObjectRef"}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.AllocationSpec", "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.LocalRef"}, } } -func schema_pkg_apis_ipam_v1alpha1_IPPrefixStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_IPPoolStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -978,7 +668,7 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixStatus(ref common.ReferenceCallback) "capacity": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixCapacity"), + Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolCapacity"), }, }, "conditions": { @@ -1006,35 +696,7 @@ func schema_pkg_apis_ipam_v1alpha1_IPPrefixStatus(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PrefixCapacity", v1.Condition{}.OpenAPIModelName()}, - } -} - -func schema_pkg_apis_ipam_v1alpha1_IPPrefixTemplate(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "IPPrefixTemplate is the metadata + spec used to materialise an IPPrefix child created atomically with an IPPrefixClaim.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixSpec"), - }, - }, - }, - Required: []string{"spec"}, - }, - }, - Dependencies: []string{ - "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.IPPrefixSpec", v1.ObjectMeta{}.OpenAPIModelName()}, + "go.miloapis.com/ipam/pkg/apis/ipam/v1alpha1.PoolCapacity", v1.Condition{}.OpenAPIModelName()}, } } @@ -1128,11 +790,11 @@ func schema_pkg_apis_ipam_v1alpha1_ObjectRef(ref common.ReferenceCallback) commo } } -func schema_pkg_apis_ipam_v1alpha1_PrefixCapacity(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_PoolCapacity(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "PrefixCapacity reports utilization for an IPPrefix.", + Description: "PoolCapacity reports utilization for an IPPool.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "total": { @@ -1163,11 +825,11 @@ func schema_pkg_apis_ipam_v1alpha1_PrefixCapacity(ref common.ReferenceCallback) } } -func schema_pkg_apis_ipam_v1alpha1_PrefixSelector(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_ipam_v1alpha1_PoolSelector(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "PrefixSelector picks a parent IPPrefix by labels, optionally scoped to a specific project for cross-project shared pools.", + Description: "PoolSelector picks a parent IPPool by labels, optionally scoped to a specific project for cross-project shared pools.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "matchLabels": { diff --git a/test/e2e/address-allocation/assertions/assert-claim-1-deleted.yaml b/test/e2e/address-allocation/assertions/assert-claim-1-deleted.yaml deleted file mode 100644 index ada7f86..0000000 --- a/test/e2e/address-allocation/assertions/assert-claim-1-deleted.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-1 - namespace: ($namespace) diff --git a/test/e2e/address-allocation/chainsaw-test.yaml b/test/e2e/address-allocation/chainsaw-test.yaml deleted file mode 100644 index fe829d8..0000000 --- a/test/e2e/address-allocation/chainsaw-test.yaml +++ /dev/null @@ -1,225 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: address-allocation -spec: - description: | - Synchronous IPAddressClaim allocation: - - First claim binds with status.allocatedIP inside the pool CIDR. - - Second claim binds with a distinct IP. - - Filling the pool then attempting one more claim returns HTTP 507. - - Releasing one bound claim makes the IP reusable. - - Pool sized as a /29 (8 addresses) instead of the spec's /28 (16). A /29 is - still large enough to demonstrate distinct allocation, exhaustion and reuse - while keeping the apply/wait loops short. The exhaustion suite uses /31 for - the same reason; this preserves that "tight pool" idiom. - - steps: - - name: setup-address-pool - description: Create class + IPPrefix (10.50.0.0/29, /32 only) — 8 addresses - try: - - create: - file: test-data/class.yaml - - create: - file: test-data/prefix.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: addr-pool - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - - name: first-claim-bound - description: | - IPAddressClaim succeeds synchronously with status.phase=Bound and - status.allocatedIP non-empty and inside 10.50.0.0/29. - try: - - create: - file: test-data/claim-1.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: addr-claim-1 - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - ip=$(kubectl get ipaddressclaim -n "$NAMESPACE" addr-claim-1 -o jsonpath='{.status.allocatedIP}') - if [ -z "$ip" ]; then - echo "FAIL: empty allocatedIP"; exit 1 - fi - # /29 covers 10.50.0.0 .. 10.50.0.7 - if ! echo "$ip" | grep -qE '^10\.50\.0\.[0-7]$'; then - echo "FAIL: $ip not in 10.50.0.0/29"; exit 1 - fi - echo "OK addr-claim-1 allocatedIP=$ip in 10.50.0.0/29" - check: - ($error == null): true - (contains($stdout, 'OK addr-claim-1 allocatedIP=')): true - - - name: second-claim-distinct-ip - description: Second claim binds with an IP different from claim-1. - try: - - create: - file: test-data/claim-2.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: addr-claim-2 - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - ip1=$(kubectl get ipaddressclaim -n "$NAMESPACE" addr-claim-1 -o jsonpath='{.status.allocatedIP}') - ip2=$(kubectl get ipaddressclaim -n "$NAMESPACE" addr-claim-2 -o jsonpath='{.status.allocatedIP}') - if [ -z "$ip2" ]; then - echo "FAIL: empty allocatedIP for addr-claim-2"; exit 1 - fi - if ! echo "$ip2" | grep -qE '^10\.50\.0\.[0-7]$'; then - echo "FAIL: $ip2 not in 10.50.0.0/29"; exit 1 - fi - if [ "$ip1" = "$ip2" ]; then - echo "FAIL: addr-claim-2 reused $ip1"; exit 1 - fi - echo "OK addr-claim-1=$ip1 addr-claim-2=$ip2 distinct" - check: - ($error == null): true - (contains($stdout, 'OK addr-claim-1=')): true - - - name: fill-and-overflow-rejected-507 - description: | - Apply six more claims to fill the /29; the ninth overflow claim must - fail HTTP 507 (Insufficient Storage). - try: - - create: - file: test-data/claims-fill.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - namespace: ($namespace) - selector: addr-test=true - timeout: 60s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - count=$(kubectl get ipaddressclaim -n "$NAMESPACE" -l addr-test=true \ - -o jsonpath='{.items[*].status.allocatedIP}' | tr ' ' '\n' | awk 'NF>0' | sort -u | awk 'END{print NR}') - if [ "$count" != "8" ]; then - echo "FAIL: expected 8 unique IPs, got $count" - kubectl get ipaddressclaim -n "$NAMESPACE" -l addr-test=true \ - -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.allocatedIP}{"\n"}{end}' - exit 1 - fi - echo "OK 8 unique IPs allocated across the /29" - check: - ($error == null): true - (contains($stdout, 'OK 8 unique IPs')): true - - create: - file: test-data/claim-overflow.yaml - expect: - - check: - ($error != null): true - (contains($error, '507') || contains($error, 'Insufficient Storage') || contains($error, 'pool exhausted')): true - - - name: release-and-reallocate - description: | - Delete addr-claim-1 and confirm a fresh claim binds (released IP is - reusable). The new claim must take the same IP that addr-claim-1 held, - since that's the only free slot in the /29. - try: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - freed=$(kubectl get ipaddressclaim -n "$NAMESPACE" addr-claim-1 -o jsonpath='{.status.allocatedIP}') - if [ -z "$freed" ]; then - echo "FAIL: addr-claim-1 has no allocatedIP to record"; exit 1 - fi - echo "$freed" > /tmp/addr-freed-ip - echo "recorded freed IP: $freed" - check: - ($error == null): true - - delete: - ref: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: addr-claim-1 - namespace: ($namespace) - - error: - file: assertions/assert-claim-1-deleted.yaml - - create: - file: test-data/claim-reuse.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: addr-claim-reuse - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - new_ip=$(kubectl get ipaddressclaim -n "$NAMESPACE" addr-claim-reuse -o jsonpath='{.status.allocatedIP}') - freed=$(cat /tmp/addr-freed-ip) - if [ -z "$new_ip" ]; then - echo "FAIL: empty allocatedIP for addr-claim-reuse"; exit 1 - fi - if [ "$new_ip" != "$freed" ]; then - echo "FAIL: reuse claim got $new_ip but expected freed slot $freed" - exit 1 - fi - echo "OK released IP $freed reused by addr-claim-reuse" - check: - ($error == null): true - (contains($stdout, 'OK released IP')): true - - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl delete ipaddressclaim -n "$NAMESPACE" \ - addr-claim-1 addr-claim-2 addr-claim-3 addr-claim-4 \ - addr-claim-5 addr-claim-6 addr-claim-7 addr-claim-8 \ - addr-claim-overflow addr-claim-reuse --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefix addr-pool --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefixclass addr-class --ignore-not-found >/dev/null 2>&1 || true - echo "address-allocation cleanup done" - check: - ($error == null): true diff --git a/test/e2e/address-allocation/test-data/claim-reuse.yaml b/test/e2e/address-allocation/test-data/claim-reuse.yaml deleted file mode 100644 index 9dc6bdb..0000000 --- a/test/e2e/address-allocation/test-data/claim-reuse.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-reuse - namespace: ($namespace) - labels: - addr-test: "true" -spec: - ipFamily: IPv4 - prefixRef: - name: addr-pool - reclaimPolicy: Delete diff --git a/test/e2e/address-allocation/test-data/claims-fill.yaml b/test/e2e/address-allocation/test-data/claims-fill.yaml deleted file mode 100644 index 7a0d0e9..0000000 --- a/test/e2e/address-allocation/test-data/claims-fill.yaml +++ /dev/null @@ -1,79 +0,0 @@ -# Six additional claims. Combined with claim-1 and claim-2 these saturate the -# /29 (8 addresses), leaving the pool fully allocated for the exhaustion step. -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-3 - namespace: ($namespace) - labels: - addr-test: "true" -spec: - ipFamily: IPv4 - prefixRef: - name: addr-pool - reclaimPolicy: Delete ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-4 - namespace: ($namespace) - labels: - addr-test: "true" -spec: - ipFamily: IPv4 - prefixRef: - name: addr-pool - reclaimPolicy: Delete ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-5 - namespace: ($namespace) - labels: - addr-test: "true" -spec: - ipFamily: IPv4 - prefixRef: - name: addr-pool - reclaimPolicy: Delete ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-6 - namespace: ($namespace) - labels: - addr-test: "true" -spec: - ipFamily: IPv4 - prefixRef: - name: addr-pool - reclaimPolicy: Delete ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-7 - namespace: ($namespace) - labels: - addr-test: "true" -spec: - ipFamily: IPv4 - prefixRef: - name: addr-pool - reclaimPolicy: Delete ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim -metadata: - name: addr-claim-8 - namespace: ($namespace) - labels: - addr-test: "true" -spec: - ipFamily: IPv4 - prefixRef: - name: addr-pool - reclaimPolicy: Delete diff --git a/test/e2e/address-allocation/test-data/class.yaml b/test/e2e/address-allocation/test-data/class.yaml deleted file mode 100644 index dfa44d9..0000000 --- a/test/e2e/address-allocation/test-data/class.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: addr-class -spec: - requiresVerification: false - visibility: consumer - defaultAllocation: - minPrefixLength: 32 - maxPrefixLength: 32 - strategy: FirstFit diff --git a/test/e2e/address-allocation/test-data/prefix.yaml b/test/e2e/address-allocation/test-data/prefix.yaml deleted file mode 100644 index 0593740..0000000 --- a/test/e2e/address-allocation/test-data/prefix.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix -metadata: - name: addr-pool -spec: - cidr: 10.50.0.0/29 - ipFamily: IPv4 - classRef: - name: addr-class - allocation: - minPrefixLength: 32 - maxPrefixLength: 32 - strategy: FirstFit diff --git a/test/e2e/prefix-validation/assertions/assert-updated-strategy.yaml b/test/e2e/claim-validation/assertions/assert-updated-strategy.yaml similarity index 68% rename from test/e2e/prefix-validation/assertions/assert-updated-strategy.yaml rename to test/e2e/claim-validation/assertions/assert-updated-strategy.yaml index c41c1a8..92ce41a 100644 --- a/test/e2e/prefix-validation/assertions/assert-updated-strategy.yaml +++ b/test/e2e/claim-validation/assertions/assert-updated-strategy.yaml @@ -1,7 +1,7 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: - name: test-valid-prefix + name: test-valid-pool spec: allocation: strategy: BestFit diff --git a/test/e2e/prefix-validation/assertions/assert-valid-prefix.yaml b/test/e2e/claim-validation/assertions/assert-valid-pool.yaml similarity index 50% rename from test/e2e/prefix-validation/assertions/assert-valid-prefix.yaml rename to test/e2e/claim-validation/assertions/assert-valid-pool.yaml index 34c41fd..3ce3928 100644 --- a/test/e2e/prefix-validation/assertions/assert-valid-prefix.yaml +++ b/test/e2e/claim-validation/assertions/assert-valid-pool.yaml @@ -1,13 +1,10 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: - name: test-valid-prefix + name: test-valid-pool spec: cidr: 10.200.0.0/20 ipFamily: IPv4 status: phase: Ready - cidr: 10.200.0.0/20 - conditions: - - type: Ready - status: 'True' + allocatedCIDR: 10.200.0.0/20 diff --git a/test/e2e/claim-validation/chainsaw-test.yaml b/test/e2e/claim-validation/chainsaw-test.yaml new file mode 100644 index 0000000..d2289b6 --- /dev/null +++ b/test/e2e/claim-validation/chainsaw-test.yaml @@ -0,0 +1,121 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: claim-validation +spec: + description: | + End-to-end tests for IPPool and IPClaim validation: + - Required field validation (cidr) on IPPool + - CIDR format validation + - prefixLength bounds (min/max from parent) + - Immutability of IPPool spec.cidr and spec.ipFamily + - Mutability of spec.allocation.strategy + + steps: + - name: create-valid-pool + description: Create a valid IPPool; assert Ready phase and canonical CIDR + try: + - create: + file: test-data/valid-pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool test-valid-pool \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: test-valid-pool not Ready after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - assert: + file: assertions/assert-valid-pool.yaml + + - name: missing-cidr-field + description: IPPool missing spec.cidr is rejected at admission + try: + - create: + file: test-data/missing-cidr-pool.yaml + expect: + - check: + ($error != null): true + (contains($error, 'cidr')): true + + - name: invalid-cidr-format + description: IPPool with malformed CIDR string is rejected + try: + - create: + file: test-data/invalid-cidr-pool.yaml + expect: + - check: + ($error != null): true + (contains($error, 'invalid CIDR')): true + + - name: claim-prefix-length-out-of-bounds + description: | + IPClaim asks for prefixLength=16 against a /20 pool. No candidate + block fits, so the request is rejected with HTTP 507 "pool exhausted". + try: + - create: + file: test-data/claim-out-of-bounds.yaml + expect: + - check: + ($error != null): true + (contains($error, '507') || contains($error, 'Insufficient Storage') || contains($error, 'exhausted')): true + + - name: claim-prefix-length-zero + description: IPClaim with prefixLength=0 is rejected + try: + - create: + file: test-data/claim-zero-length.yaml + expect: + - check: + ($error != null): true + (contains($error, 'prefixLength')): true + + - name: immutable-cidr + description: Patching IPPool.spec.cidr is rejected (immutable) + try: + - patch: + file: test-data/patch-pool-cidr.yaml + expect: + - check: + ($error != null): true + (contains($error, 'spec.cidr is immutable')): true + + - name: immutable-ip-family + description: Patching IPPool.spec.ipFamily is rejected (immutable) + try: + - patch: + file: test-data/patch-pool-ip-family.yaml + expect: + - check: + ($error != null): true + (contains($error, 'spec.ipFamily is immutable')): true + + - name: update-mutable-strategy + description: Patching IPPool.spec.allocation.strategy succeeds; assert updated value + try: + - patch: + file: test-data/patch-pool-strategy.yaml + - assert: + file: assertions/assert-updated-strategy.yaml + + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" \ + claim-out-of-bounds claim-zero-length --ignore-not-found >/dev/null 2>&1 || true + kubectl delete ippool \ + test-valid-pool test-missing-cidr test-invalid-cidr --ignore-not-found >/dev/null 2>&1 || true + echo "claim-validation cleanup done" + check: + ($error == null): true diff --git a/test/e2e/prefix-validation/test-data/claim-out-of-bounds.yaml b/test/e2e/claim-validation/test-data/claim-out-of-bounds.yaml similarity index 73% rename from test/e2e/prefix-validation/test-data/claim-out-of-bounds.yaml rename to test/e2e/claim-validation/test-data/claim-out-of-bounds.yaml index 63b0c17..329f93f 100644 --- a/test/e2e/prefix-validation/test-data/claim-out-of-bounds.yaml +++ b/test/e2e/claim-validation/test-data/claim-out-of-bounds.yaml @@ -1,11 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: claim-out-of-bounds namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 16 - prefixRef: - name: test-valid-prefix + poolRef: + name: test-valid-pool reclaimPolicy: Delete diff --git a/test/e2e/prefix-validation/test-data/claim-zero-length.yaml b/test/e2e/claim-validation/test-data/claim-zero-length.yaml similarity index 73% rename from test/e2e/prefix-validation/test-data/claim-zero-length.yaml rename to test/e2e/claim-validation/test-data/claim-zero-length.yaml index 071b782..42144e0 100644 --- a/test/e2e/prefix-validation/test-data/claim-zero-length.yaml +++ b/test/e2e/claim-validation/test-data/claim-zero-length.yaml @@ -1,11 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: claim-zero-length namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 0 - prefixRef: - name: test-valid-prefix + poolRef: + name: test-valid-pool reclaimPolicy: Delete diff --git a/test/e2e/prefix-validation/test-data/invalid-cidr.yaml b/test/e2e/claim-validation/test-data/invalid-cidr-pool.yaml similarity index 79% rename from test/e2e/prefix-validation/test-data/invalid-cidr.yaml rename to test/e2e/claim-validation/test-data/invalid-cidr-pool.yaml index a30b224..12205b9 100644 --- a/test/e2e/prefix-validation/test-data/invalid-cidr.yaml +++ b/test/e2e/claim-validation/test-data/invalid-cidr-pool.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: test-invalid-cidr spec: cidr: "not-a-cidr" ipFamily: IPv4 - classRef: - name: validation-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/prefix-validation/test-data/missing-cidr.yaml b/test/e2e/claim-validation/test-data/missing-cidr-pool.yaml similarity index 77% rename from test/e2e/prefix-validation/test-data/missing-cidr.yaml rename to test/e2e/claim-validation/test-data/missing-cidr-pool.yaml index bd67320..5a28b76 100644 --- a/test/e2e/prefix-validation/test-data/missing-cidr.yaml +++ b/test/e2e/claim-validation/test-data/missing-cidr-pool.yaml @@ -1,11 +1,10 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: test-missing-cidr spec: ipFamily: IPv4 - classRef: - name: validation-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/prefix-validation/test-data/patch-cidr.yaml b/test/e2e/claim-validation/test-data/patch-pool-cidr.yaml similarity index 69% rename from test/e2e/prefix-validation/test-data/patch-cidr.yaml rename to test/e2e/claim-validation/test-data/patch-pool-cidr.yaml index a328aa5..617a7c8 100644 --- a/test/e2e/prefix-validation/test-data/patch-cidr.yaml +++ b/test/e2e/claim-validation/test-data/patch-pool-cidr.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: - name: test-valid-prefix + name: test-valid-pool spec: cidr: 10.201.0.0/20 ipFamily: IPv4 - classRef: - name: validation-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/prefix-validation/test-data/patch-ip-family.yaml b/test/e2e/claim-validation/test-data/patch-pool-ip-family.yaml similarity index 69% rename from test/e2e/prefix-validation/test-data/patch-ip-family.yaml rename to test/e2e/claim-validation/test-data/patch-pool-ip-family.yaml index f799566..af7c2c1 100644 --- a/test/e2e/prefix-validation/test-data/patch-ip-family.yaml +++ b/test/e2e/claim-validation/test-data/patch-pool-ip-family.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: - name: test-valid-prefix + name: test-valid-pool spec: cidr: 10.200.0.0/20 ipFamily: IPv6 - classRef: - name: validation-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/prefix-validation/test-data/patch-strategy.yaml b/test/e2e/claim-validation/test-data/patch-pool-strategy.yaml similarity index 68% rename from test/e2e/prefix-validation/test-data/patch-strategy.yaml rename to test/e2e/claim-validation/test-data/patch-pool-strategy.yaml index 5dfec44..c1b6d03 100644 --- a/test/e2e/prefix-validation/test-data/patch-strategy.yaml +++ b/test/e2e/claim-validation/test-data/patch-pool-strategy.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: - name: test-valid-prefix + name: test-valid-pool spec: cidr: 10.200.0.0/20 ipFamily: IPv4 - classRef: - name: validation-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/prefix-validation/test-data/valid-prefix.yaml b/test/e2e/claim-validation/test-data/valid-pool.yaml similarity index 69% rename from test/e2e/prefix-validation/test-data/valid-prefix.yaml rename to test/e2e/claim-validation/test-data/valid-pool.yaml index eabfc3d..57a011d 100644 --- a/test/e2e/prefix-validation/test-data/valid-prefix.yaml +++ b/test/e2e/claim-validation/test-data/valid-pool.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: - name: test-valid-prefix + name: test-valid-pool spec: cidr: 10.200.0.0/20 ipFamily: IPv4 - classRef: - name: validation-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/host-address-allocation/00-setup.yaml b/test/e2e/host-address-allocation/00-setup.yaml new file mode 100644 index 0000000..b1580ef --- /dev/null +++ b/test/e2e/host-address-allocation/00-setup.yaml @@ -0,0 +1,27 @@ +# IPv4 /29 parent pool: 10.50.1.0 – 10.50.1.7 (8 host addresses). +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPPool +metadata: + name: host-pool-v4 +spec: + cidr: 10.50.1.0/29 + ipFamily: IPv4 + visibility: consumer + allocation: + minPrefixLength: 32 + maxPrefixLength: 32 + strategy: FirstFit +--- +# IPv6 /126 parent pool: 2001:db8::/126 (4 host addresses). +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPPool +metadata: + name: host-pool-v6 +spec: + cidr: 2001:db8::/126 + ipFamily: IPv6 + visibility: consumer + allocation: + minPrefixLength: 128 + maxPrefixLength: 128 + strategy: FirstFit diff --git a/test/e2e/address-allocation/test-data/claim-1.yaml b/test/e2e/host-address-allocation/01-ipv4-host-claim.yaml similarity index 53% rename from test/e2e/address-allocation/test-data/claim-1.yaml rename to test/e2e/host-address-allocation/01-ipv4-host-claim.yaml index 6999703..459b8c0 100644 --- a/test/e2e/address-allocation/test-data/claim-1.yaml +++ b/test/e2e/host-address-allocation/01-ipv4-host-claim.yaml @@ -1,12 +1,13 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim +kind: IPClaim metadata: - name: addr-claim-1 + name: host-claim-v4-1 namespace: ($namespace) labels: - addr-test: "true" + host-test: "true" spec: ipFamily: IPv4 - prefixRef: - name: addr-pool + prefixLength: 32 + poolRef: + name: host-pool-v4 reclaimPolicy: Delete diff --git a/test/e2e/address-allocation/test-data/claim-2.yaml b/test/e2e/host-address-allocation/02-ipv4-uniqueness.yaml similarity index 53% rename from test/e2e/address-allocation/test-data/claim-2.yaml rename to test/e2e/host-address-allocation/02-ipv4-uniqueness.yaml index b363125..8ed4bed 100644 --- a/test/e2e/address-allocation/test-data/claim-2.yaml +++ b/test/e2e/host-address-allocation/02-ipv4-uniqueness.yaml @@ -1,12 +1,13 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim +kind: IPClaim metadata: - name: addr-claim-2 + name: host-claim-v4-2 namespace: ($namespace) labels: - addr-test: "true" + host-test: "true" spec: ipFamily: IPv4 - prefixRef: - name: addr-pool + prefixLength: 32 + poolRef: + name: host-pool-v4 reclaimPolicy: Delete diff --git a/test/e2e/host-address-allocation/03-exhaustion.yaml b/test/e2e/host-address-allocation/03-exhaustion.yaml new file mode 100644 index 0000000..cf4dabe --- /dev/null +++ b/test/e2e/host-address-allocation/03-exhaustion.yaml @@ -0,0 +1,86 @@ +# Claims host-claim-v4-3 through host-claim-v4-8. +# Combined with host-claim-v4-1 and host-claim-v4-2 these saturate the /29 +# (8 addresses), leaving the pool fully allocated for the overflow check. +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v4-3 + namespace: ($namespace) + labels: + host-test: "true" +spec: + ipFamily: IPv4 + prefixLength: 32 + poolRef: + name: host-pool-v4 + reclaimPolicy: Delete +--- +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v4-4 + namespace: ($namespace) + labels: + host-test: "true" +spec: + ipFamily: IPv4 + prefixLength: 32 + poolRef: + name: host-pool-v4 + reclaimPolicy: Delete +--- +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v4-5 + namespace: ($namespace) + labels: + host-test: "true" +spec: + ipFamily: IPv4 + prefixLength: 32 + poolRef: + name: host-pool-v4 + reclaimPolicy: Delete +--- +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v4-6 + namespace: ($namespace) + labels: + host-test: "true" +spec: + ipFamily: IPv4 + prefixLength: 32 + poolRef: + name: host-pool-v4 + reclaimPolicy: Delete +--- +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v4-7 + namespace: ($namespace) + labels: + host-test: "true" +spec: + ipFamily: IPv4 + prefixLength: 32 + poolRef: + name: host-pool-v4 + reclaimPolicy: Delete +--- +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v4-8 + namespace: ($namespace) + labels: + host-test: "true" +spec: + ipFamily: IPv4 + prefixLength: 32 + poolRef: + name: host-pool-v4 + reclaimPolicy: Delete diff --git a/test/e2e/host-address-allocation/04-ipv6-host-claim.yaml b/test/e2e/host-address-allocation/04-ipv6-host-claim.yaml new file mode 100644 index 0000000..2078131 --- /dev/null +++ b/test/e2e/host-address-allocation/04-ipv6-host-claim.yaml @@ -0,0 +1,11 @@ +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v6-1 + namespace: ($namespace) +spec: + ipFamily: IPv6 + prefixLength: 128 + poolRef: + name: host-pool-v6 + reclaimPolicy: Delete diff --git a/test/e2e/host-address-allocation/chainsaw-test.yaml b/test/e2e/host-address-allocation/chainsaw-test.yaml new file mode 100644 index 0000000..5b00e70 --- /dev/null +++ b/test/e2e/host-address-allocation/chainsaw-test.yaml @@ -0,0 +1,305 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: host-address-allocation +spec: + description: | + Host-route allocation via IPClaim with prefixLength: 32 (IPv4) or + prefixLength: 128 (IPv6). Single-address allocation no longer uses a + dedicated IPAddressClaim resource; callers use IPClaim instead. + + Tests: + 1. IPv4 /32 bind — /29 pool (10.50.1.0/29, 8 addresses); claim /32; + assert Bound and allocatedCIDR in 10.50.1.[0-7]/32. + 2. IPv4 /32 unique — second /32 from the same pool is distinct. + 3. Pool exhaustion — fill all 8 slots; ninth claim fails HTTP 507; + pool status.capacity.available == 0. + 4. IPv6 /128 bind — /126 pool (2001:db8::/126, 4 addresses); claim /128; + assert Bound and a /128 allocatedCIDR. + + steps: + # ── Setup ─────────────────────────────────────────────────────────────── + - name: setup-pools + description: | + Create two pools: + host-pool-v4 (10.50.1.0/29, IPv4, /32 only) + host-pool-v6 (2001:db8::/126, IPv6, /128 only) + try: + - apply: + file: 00-setup.yaml + - script: + timeout: 45s + content: | + set -e + for pool in host-pool-v4 host-pool-v6; do + for i in $(seq 1 30); do + phase=$(kubectl get ippool "$pool" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: $pool not Ready after 30s (phase=$phase)" + exit 1 + fi + done + echo "all host pools Ready" + check: + ($error == null): true + + # ── Step 1: IPv4 /32 bind ──────────────────────────────────────────────── + - name: ipv4-host-claim-bound + description: | + IPClaim with prefixLength: 32 binds synchronously. + status.allocatedCIDR must be a /32 within 10.50.1.0/29. + try: + - apply: + file: 01-ipv4-host-claim.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" host-claim-v4-1 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: host-claim-v4-1 not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + cidr=$(kubectl get ipclaim -n "$NAMESPACE" host-claim-v4-1 \ + -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$cidr" ]; then + echo "FAIL: empty allocatedCIDR"; exit 1 + fi + # Verify the prefix length is /32 + prefix_len=$(echo "$cidr" | cut -d/ -f2) + if [ "$prefix_len" != "32" ]; then + echo "FAIL: expected /32, got /$prefix_len (cidr=$cidr)"; exit 1 + fi + # Verify it is within 10.50.1.0/29 (10.50.1.0 .. 10.50.1.7) + host=$(echo "$cidr" | cut -d/ -f1) + python3 -c " + import ipaddress, sys + child = ipaddress.ip_network('$cidr') + parent = ipaddress.ip_network('10.50.1.0/29') + if not child.subnet_of(parent): + print(f'FAIL: {child} not in 10.50.1.0/29') + sys.exit(1) + print(f'OK {child} is in 10.50.1.0/29') + " + check: + ($error == null): true + (contains($stdout, 'OK ')): true + + # ── Step 2: IPv4 /32 uniqueness ────────────────────────────────────────── + - name: ipv4-host-uniqueness + description: | + Second IPClaim with prefixLength: 32 receives a distinct /32 + from the same pool; the two allocatedCIDRs must not overlap. + try: + - apply: + file: 02-ipv4-uniqueness.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" host-claim-v4-2 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: host-claim-v4-2 not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + cidr1=$(kubectl get ipclaim -n "$NAMESPACE" host-claim-v4-1 \ + -o jsonpath='{.status.allocatedCIDR}') + cidr2=$(kubectl get ipclaim -n "$NAMESPACE" host-claim-v4-2 \ + -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$cidr2" ]; then + echo "FAIL: empty allocatedCIDR for host-claim-v4-2"; exit 1 + fi + if [ "$cidr1" = "$cidr2" ]; then + echo "FAIL: both claims got the same CIDR $cidr1"; exit 1 + fi + python3 -c " + import ipaddress, sys + n1 = ipaddress.ip_network('$cidr1') + n2 = ipaddress.ip_network('$cidr2') + if n1.overlaps(n2): + print(f'FAIL: {n1} and {n2} overlap') + sys.exit(1) + print(f'OK {n1} and {n2} are distinct') + " + check: + ($error == null): true + (contains($stdout, 'OK ')): true + + # ── Step 3: Pool exhaustion → HTTP 507 ─────────────────────────────────── + - name: exhaustion-507 + description: | + Fill all 8 host slots in the /29 pool, then assert the ninth claim + fails with HTTP 507 (Insufficient Storage). Also asserts that the + pool reports status.capacity.available == 0. + try: + - apply: + file: 03-exhaustion.yaml + - script: + timeout: 75s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 60); do + count=$(kubectl get ipclaim -n "$NAMESPACE" -l host-test=true \ + -o jsonpath='{.items[*].status.phase}' 2>/dev/null | tr ' ' '\n' | grep -c "^Bound$" || echo "0") + if [ "$count" = "8" ]; then break; fi + sleep 1 + done + if [ "$count" != "8" ]; then + echo "FAIL: only $count/8 host claims Bound after 60s" + exit 1 + fi + check: + ($error == null): true + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + count=$(kubectl get ipclaim -n "$NAMESPACE" -l host-test=true \ + -o jsonpath='{.items[*].status.allocatedCIDR}' \ + | tr ' ' '\n' | awk 'NF>0' | sort -u | awk 'END{print NR}') + if [ "$count" != "8" ]; then + echo "FAIL: expected 8 unique /32 CIDRs, got $count" + kubectl get ipclaim -n "$NAMESPACE" -l host-test=true \ + -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.allocatedCIDR}{"\n"}{end}' + exit 1 + fi + echo "OK 8 unique /32 addresses allocated across the /29" + check: + ($error == null): true + (contains($stdout, 'OK 8 unique /32 addresses')): true + - script: + content: | + set -e + avail=$(kubectl get ippool host-pool-v4 \ + -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") + echo "pool status.capacity.available=${avail}" + if [ -n "$avail" ] && [ "$avail" != "0" ]; then + echo "FAIL: expected capacity.available=0, got $avail" + exit 1 + fi + echo "OK pool capacity.available is 0 (or unset — pool exhausted)" + check: + ($error == null): true + (contains($stdout, 'OK pool capacity.available')): true + - create: + file: test-data/claim-overflow.yaml + expect: + - check: + ($error != null): true + (contains($error, '507') || contains($error, 'Insufficient Storage') || contains($error, 'exhausted')): true + + # ── Step 4: IPv6 /128 bind ─────────────────────────────────────────────── + - name: ipv6-host-claim-bound + description: | + IPClaim with prefixLength: 128 and ipFamily: IPv6 binds + synchronously from the 2001:db8::/126 pool (4 addresses). + status.allocatedCIDR must be a /128 subnet of 2001:db8::/126. + try: + - apply: + file: 04-ipv6-host-claim.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" host-claim-v6-1 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: host-claim-v6-1 not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + cidr=$(kubectl get ipclaim -n "$NAMESPACE" host-claim-v6-1 \ + -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$cidr" ]; then + echo "FAIL: empty allocatedCIDR"; exit 1 + fi + prefix_len=$(echo "$cidr" | cut -d/ -f2) + if [ "$prefix_len" != "128" ]; then + echo "FAIL: expected /128, got /$prefix_len (cidr=$cidr)"; exit 1 + fi + python3 -c " + import ipaddress, sys + child = ipaddress.ip_network('$cidr') + parent = ipaddress.ip_network('2001:db8::/126') + if not child.subnet_of(parent): + print(f'FAIL: {child} not in 2001:db8::/126') + sys.exit(1) + print(f'OK {child} is in 2001:db8::/126') + " + check: + ($error == null): true + (contains($stdout, 'OK ')): true + + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" \ + host-claim-v4-1 host-claim-v4-2 \ + host-claim-v4-3 host-claim-v4-4 host-claim-v4-5 \ + host-claim-v4-6 host-claim-v4-7 host-claim-v4-8 \ + host-claim-v4-overflow \ + host-claim-v6-1 \ + --ignore-not-found >/dev/null 2>&1 || true + kubectl delete ippool host-pool-v4 host-pool-v6 \ + --ignore-not-found >/dev/null 2>&1 || true + echo "host-address-allocation cleanup done" + check: + ($error == null): true diff --git a/test/e2e/host-address-allocation/test-data/claim-overflow.yaml b/test/e2e/host-address-allocation/test-data/claim-overflow.yaml new file mode 100644 index 0000000..c8cd4d0 --- /dev/null +++ b/test/e2e/host-address-allocation/test-data/claim-overflow.yaml @@ -0,0 +1,11 @@ +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPClaim +metadata: + name: host-claim-v4-overflow + namespace: ($namespace) +spec: + ipFamily: IPv4 + prefixLength: 32 + poolRef: + name: host-pool-v4 + reclaimPolicy: Delete diff --git a/test/e2e/prefix-allocation/assertions/assert-claim-1-bound.yaml b/test/e2e/ip-claim/assertions/assert-claim-1-bound.yaml similarity index 72% rename from test/e2e/prefix-allocation/assertions/assert-claim-1-bound.yaml rename to test/e2e/ip-claim/assertions/assert-claim-1-bound.yaml index fa401b5..a47c58f 100644 --- a/test/e2e/prefix-allocation/assertions/assert-claim-1-bound.yaml +++ b/test/e2e/ip-claim/assertions/assert-claim-1-bound.yaml @@ -1,9 +1,9 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: alloc-claim-1 namespace: ($namespace) status: phase: Bound (allocatedCIDR != null): true - (boundPrefixRef.name != null): true + (boundAllocationRef.name != null): true diff --git a/test/e2e/prefix-allocation/assertions/assert-claim-1-deleted.yaml b/test/e2e/ip-claim/assertions/assert-claim-1-deleted.yaml similarity index 82% rename from test/e2e/prefix-allocation/assertions/assert-claim-1-deleted.yaml rename to test/e2e/ip-claim/assertions/assert-claim-1-deleted.yaml index d893d18..d78b147 100644 --- a/test/e2e/prefix-allocation/assertions/assert-claim-1-deleted.yaml +++ b/test/e2e/ip-claim/assertions/assert-claim-1-deleted.yaml @@ -1,5 +1,5 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: alloc-claim-1 namespace: ($namespace) diff --git a/test/e2e/ip-claim/chainsaw-test.yaml b/test/e2e/ip-claim/chainsaw-test.yaml new file mode 100644 index 0000000..40130ac --- /dev/null +++ b/test/e2e/ip-claim/chainsaw-test.yaml @@ -0,0 +1,276 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: ip-claim +spec: + description: | + Happy-path allocation tests for IPClaim: + - Synchronous CIDR in status on Bound + - Non-overlapping concurrent allocations + - IPAllocation object created atomically in the same namespace + - Release on delete (IPAllocation removed) and re-allocation + + steps: + - name: setup-pool + description: Create root IPPool (10.128.0.0/20, allow /24-/28) + try: + - create: + file: test-data/pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool alloc-parent \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: alloc-parent not Ready after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + + - name: allocate-first-claim + description: | + Create IPClaim (prefixLength=24); assert /24 within parent and + boundAllocationRef set. The shell follow-up additionally verifies + — using Python's ipaddress module — that status.allocatedCIDR is + actually a subnet of the pool CIDR, catching cases where the + server might return a syntactically valid CIDR that lies outside + the pool. Also asserts the corresponding IPAllocation object + exists in the same namespace as the claim. + try: + - create: + file: test-data/claim-first.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" alloc-claim-1 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: alloc-claim-1 not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - assert: + file: assertions/assert-claim-1-bound.yaml + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + allocated=$(kubectl get ipclaim -n "$NAMESPACE" alloc-claim-1 -o jsonpath='{.status.allocatedCIDR}') + pool=$(kubectl get ippool alloc-parent -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$allocated" ] || [ -z "$pool" ]; then + echo "FAIL: missing allocated=$allocated pool=$pool" + exit 1 + fi + python3 -c " + import ipaddress, sys + child = ipaddress.ip_network('$allocated') + parent = ipaddress.ip_network('$pool') + if not child.subnet_of(parent): + print(f'FAIL: {child} not a subnet of {parent}') + sys.exit(1) + print(f'OK {child} is a subnet of {parent}') + " + check: + ($error == null): true + (contains($stdout, 'OK ')): true + - script: + timeout: 30s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + # Verify the IPAllocation object was created atomically in + # the same namespace as the claim, and that its CIDR matches + # the claim's allocatedCIDR. + ref=$(kubectl get ipclaim -n "$NAMESPACE" alloc-claim-1 -o jsonpath='{.status.boundAllocationRef.name}') + if [ -z "$ref" ]; then + echo "FAIL: empty boundAllocationRef.name on alloc-claim-1" + exit 1 + fi + alloc_cidr=$(kubectl get ipallocation -n "$NAMESPACE" "$ref" -o jsonpath='{.status.allocatedCIDR}' 2>/dev/null || echo "") + claim_cidr=$(kubectl get ipclaim -n "$NAMESPACE" alloc-claim-1 -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$alloc_cidr" ]; then + echo "FAIL: IPAllocation $ref not found in namespace $NAMESPACE" + exit 1 + fi + if [ "$alloc_cidr" != "$claim_cidr" ]; then + echo "FAIL: IPAllocation.status.allocatedCIDR ($alloc_cidr) != claim.status.allocatedCIDR ($claim_cidr)" + exit 1 + fi + echo "OK IPAllocation $ref exists with cidr=$alloc_cidr" + check: + ($error == null): true + (contains($stdout, 'OK IPAllocation ')): true + + - name: allocate-second-claim-non-overlap + description: Second IPClaim (prefixLength=24) gets a non-overlapping /24 + try: + - create: + file: test-data/claim-second.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" alloc-claim-2 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: alloc-claim-2 not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl get ipclaim -n "$NAMESPACE" alloc-claim-1 alloc-claim-2 \ + -o jsonpath='{.items[*].status.allocatedCIDR}' | tr ' ' '\n' | sort -u | awk 'END{print NR}' + check: + ($stdout): "2\n" + + - name: release-first-claim + description: | + Delete the first claim and verify the full lifecycle: + 1. Snapshot pool status.capacity.available and the boundAllocationRef BEFORE delete. + 2. Delete the claim. + 3. Confirm the claim is gone AND the IPAllocation it owned is gone. + 4. Assert pool status.capacity.available has INCREASED by 256 (one /24). + try: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + before=$(kubectl get ippool alloc-parent -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") + if [ -z "$before" ]; then + echo "FAIL: pool has no status.capacity.available" + exit 1 + fi + ref=$(kubectl get ipclaim -n "$NAMESPACE" alloc-claim-1 -o jsonpath='{.status.boundAllocationRef.name}') + echo "$before" > /tmp/alloc-parent-available-before + echo "$ref" > /tmp/alloc-claim-1-allocation-ref + echo "before_available=$before allocation_ref=$ref" + check: + ($error == null): true + - delete: + ref: + apiVersion: ipam.miloapis.com/v1alpha1 + kind: IPClaim + name: alloc-claim-1 + namespace: ($namespace) + - error: + file: assertions/assert-claim-1-deleted.yaml + - script: + timeout: 30s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + ref=$(cat /tmp/alloc-claim-1-allocation-ref) + for i in $(seq 1 30); do + if ! kubectl get ipallocation -n "$NAMESPACE" "$ref" >/dev/null 2>&1; then + echo "OK IPAllocation $ref removed" + exit 0 + fi + sleep 1 + done + echo "FAIL: IPAllocation $ref still present after claim delete" + exit 1 + check: + ($error == null): true + (contains($stdout, 'OK IPAllocation ')): true + - script: + timeout: 30s + content: | + set -e + before=$(cat /tmp/alloc-parent-available-before) + for i in $(seq 1 30); do + after=$(kubectl get ippool alloc-parent -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") + if [ -n "$after" ] && [ "$after" -gt "$before" ]; then + break + fi + sleep 0.5 + done + echo "after_available=$after (before=$before)" + if [ -z "$after" ]; then + echo "FAIL: pool capacity unreadable after release" + exit 1 + fi + if [ "$after" -le "$before" ]; then + echo "FAIL: capacity.available did not increase after release ($before -> $after)" + exit 1 + fi + expected=$(( before + 256 )) + if [ "$after" -ne "$expected" ]; then + echo "FAIL: capacity.available expected $expected after releasing /24 ($before + 256), got $after" + exit 1 + fi + echo "OK capacity available incremented from $before to $after after releasing /24" + check: + ($error == null): true + (contains($stdout, 'OK capacity available incremented')): true + + - name: reallocate-after-release + description: New claim succeeds; pool not exhausted + try: + - create: + file: test-data/claim-reallocate.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" alloc-claim-reuse \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: alloc-claim-reuse not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" \ + alloc-claim-1 alloc-claim-2 alloc-claim-reuse --ignore-not-found >/dev/null 2>&1 || true + kubectl delete ippool alloc-parent --ignore-not-found >/dev/null 2>&1 || true + echo "ip-claim cleanup done" + check: + ($error == null): true diff --git a/test/e2e/prefix-allocation/test-data/claim-first.yaml b/test/e2e/ip-claim/test-data/claim-first.yaml similarity index 84% rename from test/e2e/prefix-allocation/test-data/claim-first.yaml rename to test/e2e/ip-claim/test-data/claim-first.yaml index 920e588..5dbc97e 100644 --- a/test/e2e/prefix-allocation/test-data/claim-first.yaml +++ b/test/e2e/ip-claim/test-data/claim-first.yaml @@ -1,11 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: alloc-claim-1 namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: alloc-parent reclaimPolicy: Delete diff --git a/test/e2e/prefix-allocation/test-data/claim-reallocate.yaml b/test/e2e/ip-claim/test-data/claim-reallocate.yaml similarity index 85% rename from test/e2e/prefix-allocation/test-data/claim-reallocate.yaml rename to test/e2e/ip-claim/test-data/claim-reallocate.yaml index 0582be8..0a86c9f 100644 --- a/test/e2e/prefix-allocation/test-data/claim-reallocate.yaml +++ b/test/e2e/ip-claim/test-data/claim-reallocate.yaml @@ -1,11 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: alloc-claim-reuse namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: alloc-parent reclaimPolicy: Delete diff --git a/test/e2e/prefix-allocation/test-data/claim-second.yaml b/test/e2e/ip-claim/test-data/claim-second.yaml similarity index 84% rename from test/e2e/prefix-allocation/test-data/claim-second.yaml rename to test/e2e/ip-claim/test-data/claim-second.yaml index 2031684..9377ccc 100644 --- a/test/e2e/prefix-allocation/test-data/claim-second.yaml +++ b/test/e2e/ip-claim/test-data/claim-second.yaml @@ -1,11 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: alloc-claim-2 namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: alloc-parent reclaimPolicy: Delete diff --git a/test/e2e/prefix-allocation/test-data/parent-prefix.yaml b/test/e2e/ip-claim/test-data/pool.yaml similarity index 78% rename from test/e2e/prefix-allocation/test-data/parent-prefix.yaml rename to test/e2e/ip-claim/test-data/pool.yaml index 37a8ab0..5d84c08 100644 --- a/test/e2e/prefix-allocation/test-data/parent-prefix.yaml +++ b/test/e2e/ip-claim/test-data/pool.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: alloc-parent spec: cidr: 10.128.0.0/20 ipFamily: IPv4 - classRef: - name: consumer-private + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/ippool-hierarchy/chainsaw-test.yaml b/test/e2e/ippool-hierarchy/chainsaw-test.yaml new file mode 100644 index 0000000..b8400c4 --- /dev/null +++ b/test/e2e/ippool-hierarchy/chainsaw-test.yaml @@ -0,0 +1,200 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: ippool-hierarchy +spec: + description: | + Hierarchical IPPool delegation: environment -> region -> leaf. + - Child IPPool with spec.parentPoolRef carves a sub-block from its parent + - Two child regional pools must be non-overlapping + - Leaf IPClaim against the child IPPool resolves inside the child range + - DELETE of a regional IPPool while a leaf claim still holds an + allocation is rejected with HTTP 409. + + NOTE: This suite verifies DELETION-PROTECTION semantics, NOT cascade + delete. A parent IPPool with active leaf claims is rejected on DELETE + with HTTP 409 so operators must release child claims first. This + avoids orphaning child allocations. + + steps: + - name: create-environment-pool + description: Top-of-tree environment IPPool (10.128.0.0/9, allow /12-/16) + try: + - create: + file: test-data/env-pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool hier-env \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: hier-env not Ready after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + + - name: create-region-1-pool + description: | + Child IPPool hier-region-1 with parentPoolRef=hier-env, prefixLength=12. + The server allocates a /12 from hier-env and writes it to status.allocatedCIDR. + try: + - create: + file: test-data/region-1-pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool hier-region-1 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: hier-region-1 not Ready after 30s (phase=$phase)" + exit 1 + fi + cidr=$(kubectl get ippool hier-region-1 -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$cidr" ]; then + echo "FAIL: hier-region-1 status.allocatedCIDR empty" + exit 1 + fi + python3 -c " + import ipaddress, sys + child = ipaddress.ip_network('$cidr') + parent = ipaddress.ip_network('10.128.0.0/9') + if child.prefixlen != 12: + print(f'FAIL: expected /12, got {child}', file=sys.stderr); sys.exit(1) + if not child.subnet_of(parent): + print(f'FAIL: {child} not subnet of {parent}', file=sys.stderr); sys.exit(1) + print(f'OK hier-region-1 cidr={child}') + " + check: + ($error == null): true + (contains($stdout, 'OK hier-region-1 cidr=')): true + + - name: create-region-2-pool-non-overlap + description: Second child pool must allocate a non-overlapping /12 + try: + - create: + file: test-data/region-2-pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool hier-region-2 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: hier-region-2 not Ready after 30s (phase=$phase)" + exit 1 + fi + c1=$(kubectl get ippool hier-region-1 -o jsonpath='{.status.allocatedCIDR}') + c2=$(kubectl get ippool hier-region-2 -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$c1" ] || [ -z "$c2" ]; then + echo "FAIL: missing region CIDR (c1=$c1 c2=$c2)" + exit 1 + fi + if [ "$c1" = "$c2" ]; then + echo "FAIL: regional CIDRs overlap ($c1 == $c2)" + exit 1 + fi + python3 -c " + import ipaddress, sys + a = ipaddress.ip_network('$c1') + b = ipaddress.ip_network('$c2') + if a.overlaps(b): + print(f'FAIL: {a} overlaps {b}', file=sys.stderr); sys.exit(1) + print(f'OK regions {a} and {b} non-overlapping') + " + check: + ($error == null): true + (contains($stdout, 'OK regions ')): true + + - name: claim-leaf-against-child-pool + description: /24 IPClaim against hier-region-1; CIDR must be within region 1 + try: + - create: + file: test-data/leaf-claim.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" hier-leaf-claim \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: hier-leaf-claim not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + leaf_cidr=$(kubectl get ipclaim -n "$NAMESPACE" hier-leaf-claim -o jsonpath='{.status.allocatedCIDR}') + region_cidr=$(kubectl get ippool hier-region-1 -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$leaf_cidr" ] || [ -z "$region_cidr" ]; then + echo "FAIL: missing CIDR (leaf=$leaf_cidr region=$region_cidr)" + exit 1 + fi + python3 -c " + import ipaddress, sys + leaf = ipaddress.ip_network('$leaf_cidr', strict=False) + region = ipaddress.ip_network('$region_cidr', strict=False) + if not leaf.subnet_of(region): + print(f'FAIL: leaf {leaf} is NOT a subnet of region {region}', file=sys.stderr) + sys.exit(1) + print(f'OK leaf {leaf} subset of region {region}') + " + check: + ($error == null): true + (contains($stdout, 'OK leaf ')): true + + - name: deletion-protected-while-leaf-bound + description: | + Deleting the regional IPPool while the leaf claim still holds an + allocation against it must fail with HTTP 409 ("active allocation"). + try: + - script: + content: | + out=$(kubectl delete ippool hier-region-1 2>&1) && status=0 || status=$? + echo "$out" + if [ "$status" -eq 0 ]; then + echo "expected delete to fail, but it succeeded" >&2 + exit 1 + fi + echo "$out" | grep -qiE 'active allocation|409|Conflict' + check: + ($error == null): true + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" hier-leaf-claim --ignore-not-found=true + kubectl delete ippool hier-region-1 --ignore-not-found=true + kubectl delete ippool hier-region-2 --ignore-not-found=true + kubectl delete ippool hier-env --ignore-not-found=true + echo "ippool-hierarchy cleanup done" + check: + ($error == null): true diff --git a/test/e2e/prefix-hierarchy/test-data/env-prefix.yaml b/test/e2e/ippool-hierarchy/test-data/env-pool.yaml similarity index 78% rename from test/e2e/prefix-hierarchy/test-data/env-prefix.yaml rename to test/e2e/ippool-hierarchy/test-data/env-pool.yaml index f921dfe..5807796 100644 --- a/test/e2e/prefix-hierarchy/test-data/env-prefix.yaml +++ b/test/e2e/ippool-hierarchy/test-data/env-pool.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: hier-env spec: cidr: 10.128.0.0/9 ipFamily: IPv4 - classRef: - name: platform-shared + visibility: platform allocation: minPrefixLength: 12 maxPrefixLength: 16 diff --git a/test/e2e/prefix-hierarchy/test-data/leaf-claim.yaml b/test/e2e/ippool-hierarchy/test-data/leaf-claim.yaml similarity index 85% rename from test/e2e/prefix-hierarchy/test-data/leaf-claim.yaml rename to test/e2e/ippool-hierarchy/test-data/leaf-claim.yaml index 8872668..ae88425 100644 --- a/test/e2e/prefix-hierarchy/test-data/leaf-claim.yaml +++ b/test/e2e/ippool-hierarchy/test-data/leaf-claim.yaml @@ -1,11 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: hier-leaf-claim namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: hier-region-1 reclaimPolicy: Delete diff --git a/test/e2e/ippool-hierarchy/test-data/region-1-pool.yaml b/test/e2e/ippool-hierarchy/test-data/region-1-pool.yaml new file mode 100644 index 0000000..dd18f5d --- /dev/null +++ b/test/e2e/ippool-hierarchy/test-data/region-1-pool.yaml @@ -0,0 +1,13 @@ +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPPool +metadata: + name: hier-region-1 +spec: + visibility: platform + parentPoolRef: + name: hier-env + prefixLength: 12 + allocation: + minPrefixLength: 16 + maxPrefixLength: 28 + strategy: FirstFit diff --git a/test/e2e/ippool-hierarchy/test-data/region-2-pool.yaml b/test/e2e/ippool-hierarchy/test-data/region-2-pool.yaml new file mode 100644 index 0000000..eff0dbe --- /dev/null +++ b/test/e2e/ippool-hierarchy/test-data/region-2-pool.yaml @@ -0,0 +1,13 @@ +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPPool +metadata: + name: hier-region-2 +spec: + visibility: platform + parentPoolRef: + name: hier-env + prefixLength: 12 + allocation: + minPrefixLength: 16 + maxPrefixLength: 28 + strategy: FirstFit diff --git a/test/e2e/ippool/assertions/assert-root-ready.yaml b/test/e2e/ippool/assertions/assert-root-ready.yaml new file mode 100644 index 0000000..43bd19a --- /dev/null +++ b/test/e2e/ippool/assertions/assert-root-ready.yaml @@ -0,0 +1,10 @@ +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPPool +metadata: + name: pool-suite-root +spec: + cidr: 10.220.0.0/20 + ipFamily: IPv4 +status: + phase: Ready + allocatedCIDR: 10.220.0.0/20 diff --git a/test/e2e/ippool/chainsaw-test.yaml b/test/e2e/ippool/chainsaw-test.yaml new file mode 100644 index 0000000..b6ccb09 --- /dev/null +++ b/test/e2e/ippool/chainsaw-test.yaml @@ -0,0 +1,281 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: ippool +spec: + description: | + Lifecycle tests for IPPool (cluster-scoped): + 1. Create root IPPool → assert status.phase=Ready, status.allocatedCIDR populated. + 2. Create child IPPool with spec.parentPoolRef → assert status.allocatedCIDR is a + valid subnet of the root and status.phase=Ready. + 3. Create an IPClaim against the child pool → assert IPClaim Bound and + a corresponding IPAllocation object exists in the same namespace. + 4. Attempt to delete the child IPPool while a claim still holds an + allocation → assert HTTP 409. + 5. Delete the IPClaim → assert IPAllocation removed → child IPPool now + deletable. + 6. Throughout, status.capacity.allocated/available track the live state. + + steps: + - name: create-root-pool + description: Root IPPool 10.220.0.0/20 (consumer, /24-/28) + try: + - create: + file: test-data/root-pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool pool-suite-root \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: pool-suite-root not Ready after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - assert: + file: assertions/assert-root-ready.yaml + + - name: create-child-pool + description: | + Child IPPool with parentPoolRef=pool-suite-root and prefixLength=24. + Server carves a /24 from the root and populates status.allocatedCIDR. + Capture root capacity before/after to confirm the child consumes + 256 addresses of the parent's available pool. + try: + - script: + content: | + set -e + before=$(kubectl get ippool pool-suite-root -o jsonpath='{.status.capacity.available}') + echo "$before" > /tmp/pool-suite-root-before + echo "before_root_available=$before" + check: + ($error == null): true + - create: + file: test-data/child-pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool pool-suite-child \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: pool-suite-child not Ready after 30s (phase=$phase)" + exit 1 + fi + child_cidr=$(kubectl get ippool pool-suite-child -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$child_cidr" ]; then + echo "FAIL: pool-suite-child status.allocatedCIDR empty" + exit 1 + fi + python3 -c " + import ipaddress, sys + child = ipaddress.ip_network('$child_cidr') + parent = ipaddress.ip_network('10.220.0.0/20') + if child.prefixlen != 24: + print(f'FAIL: expected /24, got {child}', file=sys.stderr); sys.exit(1) + if not child.subnet_of(parent): + print(f'FAIL: {child} not subnet of {parent}', file=sys.stderr); sys.exit(1) + print(f'OK child {child} subset of root {parent}') + " + check: + ($error == null): true + (contains($stdout, 'OK child ')): true + - script: + content: | + set -e + before=$(cat /tmp/pool-suite-root-before) + after=$(kubectl get ippool pool-suite-root -o jsonpath='{.status.capacity.available}') + echo "after_root_available=$after (before=$before)" + if [ "$after" -ge "$before" ]; then + echo "FAIL: root capacity.available did not decrease after child pool creation" + exit 1 + fi + echo "OK root capacity decreased after child allocation ($before -> $after)" + check: + ($error == null): true + (contains($stdout, 'OK root capacity decreased')): true + + - name: claim-against-child-pool + description: | + IPClaim with prefixLength=28 against the child pool. Assert Bound, + IPAllocation created in the same namespace, child pool capacity + decreased by 16 (one /28). + try: + - script: + content: | + set -e + before=$(kubectl get ippool pool-suite-child -o jsonpath='{.status.capacity.available}') + echo "$before" > /tmp/pool-suite-child-before + echo "before_child_available=$before" + check: + ($error == null): true + - create: + file: test-data/claim.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" pool-suite-claim \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: pool-suite-claim not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + timeout: 30s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + ref=$(kubectl get ipclaim -n "$NAMESPACE" pool-suite-claim -o jsonpath='{.status.boundAllocationRef.name}') + if [ -z "$ref" ]; then + echo "FAIL: empty boundAllocationRef.name on pool-suite-claim" + exit 1 + fi + alloc_cidr=$(kubectl get ipallocation -n "$NAMESPACE" "$ref" -o jsonpath='{.status.allocatedCIDR}' 2>/dev/null || echo "") + claim_cidr=$(kubectl get ipclaim -n "$NAMESPACE" pool-suite-claim -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$alloc_cidr" ]; then + echo "FAIL: IPAllocation $ref not found in namespace $NAMESPACE" + exit 1 + fi + if [ "$alloc_cidr" != "$claim_cidr" ]; then + echo "FAIL: IPAllocation.status.allocatedCIDR ($alloc_cidr) != claim.status.allocatedCIDR ($claim_cidr)" + exit 1 + fi + echo "$ref" > /tmp/pool-suite-allocation-ref + echo "OK IPAllocation $ref exists with cidr=$alloc_cidr" + check: + ($error == null): true + (contains($stdout, 'OK IPAllocation ')): true + - script: + timeout: 30s + content: | + set -e + before=$(cat /tmp/pool-suite-child-before) + for i in $(seq 1 30); do + after=$(kubectl get ippool pool-suite-child -o jsonpath='{.status.capacity.available}') + if [ "$after" -lt "$before" ]; then break; fi + sleep 0.5 + done + echo "after_child_available=$after (before=$before)" + expected=$(( before - 16 )) + if [ "$after" -ne "$expected" ]; then + echo "FAIL: child capacity.available expected $expected after /28 claim (= $before - 16), got $after" + exit 1 + fi + echo "OK child capacity decreased by 16 after /28 claim" + check: + ($error == null): true + (contains($stdout, 'OK child capacity decreased')): true + + - name: delete-pool-with-active-claim-rejected + description: Deleting pool-suite-child while pool-suite-claim is bound must fail with HTTP 409 + try: + - script: + content: | + out=$(kubectl delete ippool pool-suite-child 2>&1) && status=0 || status=$? + echo "$out" + if [ "$status" -eq 0 ]; then + echo "expected delete to fail, but it succeeded" >&2 + exit 1 + fi + echo "$out" | grep -qiE 'active allocation|409|Conflict' + check: + ($error == null): true + + - name: release-claim-and-delete-pool + description: | + Delete the IPClaim → assert IPAllocation removed → child pool now + deletable. Then delete the child pool and confirm root capacity + recovers. + try: + - script: + content: | + set -e + before=$(kubectl get ippool pool-suite-root -o jsonpath='{.status.capacity.available}') + echo "$before" > /tmp/pool-suite-root-pre-child-delete + echo "before_root_available_pre_child_delete=$before" + check: + ($error == null): true + - delete: + ref: + apiVersion: ipam.miloapis.com/v1alpha1 + kind: IPClaim + name: pool-suite-claim + namespace: ($namespace) + - script: + timeout: 30s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + ref=$(cat /tmp/pool-suite-allocation-ref) + for i in $(seq 1 30); do + if ! kubectl get ipallocation -n "$NAMESPACE" "$ref" >/dev/null 2>&1; then + echo "OK IPAllocation $ref removed after claim delete" + exit 0 + fi + sleep 1 + done + echo "FAIL: IPAllocation $ref still present after claim delete" + exit 1 + check: + ($error == null): true + (contains($stdout, 'OK IPAllocation ')): true + - delete: + ref: + apiVersion: ipam.miloapis.com/v1alpha1 + kind: IPPool + name: pool-suite-child + - script: + timeout: 30s + content: | + set -e + before=$(cat /tmp/pool-suite-root-pre-child-delete) + for i in $(seq 1 30); do + after=$(kubectl get ippool pool-suite-root -o jsonpath='{.status.capacity.available}') + if [ "$after" -gt "$before" ]; then break; fi + sleep 0.5 + done + echo "after_root_available=$after (before=$before)" + if [ "$after" -le "$before" ]; then + echo "FAIL: root capacity.available did not increase after child pool delete" + exit 1 + fi + echo "OK root capacity recovered after child pool delete ($before -> $after)" + check: + ($error == null): true + (contains($stdout, 'OK root capacity recovered')): true + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" pool-suite-claim --ignore-not-found >/dev/null 2>&1 || true + kubectl delete ippool pool-suite-child --ignore-not-found >/dev/null 2>&1 || true + kubectl delete ippool pool-suite-root --ignore-not-found >/dev/null 2>&1 || true + echo "ippool suite cleanup done" + check: + ($error == null): true diff --git a/test/e2e/ippool/test-data/child-pool.yaml b/test/e2e/ippool/test-data/child-pool.yaml new file mode 100644 index 0000000..ee8aacc --- /dev/null +++ b/test/e2e/ippool/test-data/child-pool.yaml @@ -0,0 +1,13 @@ +apiVersion: ipam.miloapis.com/v1alpha1 +kind: IPPool +metadata: + name: pool-suite-child +spec: + visibility: consumer + parentPoolRef: + name: pool-suite-root + prefixLength: 24 + allocation: + minPrefixLength: 28 + maxPrefixLength: 28 + strategy: FirstFit diff --git a/test/e2e/address-allocation/test-data/claim-overflow.yaml b/test/e2e/ippool/test-data/claim.yaml similarity index 55% rename from test/e2e/address-allocation/test-data/claim-overflow.yaml rename to test/e2e/ippool/test-data/claim.yaml index 9f13fa8..84b9dad 100644 --- a/test/e2e/address-allocation/test-data/claim-overflow.yaml +++ b/test/e2e/ippool/test-data/claim.yaml @@ -1,10 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim +kind: IPClaim metadata: - name: addr-claim-overflow + name: pool-suite-claim namespace: ($namespace) spec: ipFamily: IPv4 - prefixRef: - name: addr-pool + prefixLength: 28 + poolRef: + name: pool-suite-child reclaimPolicy: Delete diff --git a/test/e2e/prefix-allocation/test-data/class.yaml b/test/e2e/ippool/test-data/root-pool.yaml similarity index 60% rename from test/e2e/prefix-allocation/test-data/class.yaml rename to test/e2e/ippool/test-data/root-pool.yaml index ca4e874..670fe9b 100644 --- a/test/e2e/prefix-allocation/test-data/class.yaml +++ b/test/e2e/ippool/test-data/root-pool.yaml @@ -1,11 +1,12 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass +kind: IPPool metadata: - name: consumer-private + name: pool-suite-root spec: - requiresVerification: false + cidr: 10.220.0.0/20 + ipFamily: IPv4 visibility: consumer - defaultAllocation: + allocation: minPrefixLength: 24 maxPrefixLength: 28 strategy: FirstFit diff --git a/test/e2e/multi-tenant/chainsaw-test.yaml b/test/e2e/multi-tenant/chainsaw-test.yaml index d84f62d..c60cae7 100644 --- a/test/e2e/multi-tenant/chainsaw-test.yaml +++ b/test/e2e/multi-tenant/chainsaw-test.yaml @@ -4,8 +4,8 @@ metadata: name: multi-tenant spec: description: | - Multi-tenant IPPrefixClaim e2e suite. Two simulated projects (alpha, beta) - each have a private pool, plus one shared pool owned by alpha. The + Multi-tenant IPClaim e2e suite. Two simulated projects (alpha, beta) + each have a private IPPool, plus one shared pool owned by alpha. The X-Remote-Extra-Iam.Miloapis.Com.Parent-* headers are injected via curl through `kubectl proxy` so the suite exercises the IPAM server's multi-tenant authorization path end-to-end. @@ -13,13 +13,14 @@ spec: The IPAM server enforces tenant isolation: UserInfo.Extra carries the caller's project, ownerRef on the resulting object is overwritten from that identity (not trusted from the client), and cross-project allocation - against another project's pool requires a SubjectAccessReview that passes - via a ClusterRoleBinding granting `use` on the pool. This suite asserts: + against another project's pool requires the pool's spec.visibility to be + `shared` AND a SubjectAccessReview that passes via a ClusterRoleBinding + granting `use` on the `ippools` resource. This suite asserts: * Same-project allocations succeed and stay within the project's CIDR. * Cross-project allocations against shared pools (with a `use` grant for project-beta) succeed (HTTP 201). - * Cross-project allocations against private pools (no `use` grant) - are denied (HTTP 403). + * Cross-project allocations against private pools (no `use` grant + and visibility=consumer) are denied (HTTP 403). timeouts: cleanup: 90s @@ -27,63 +28,39 @@ spec: assert: 60s steps: - - name: seed-classes-pools-rbac + - name: seed-pools-rbac description: | - Create mt-consumer-private + mt-consumer-shared classes, mt-alpha-pool, - mt-beta-pool, mt-shared-pool, and the ClusterRole/ClusterRoleBinding - granting project-beta `use` on mt-shared-pool. + Create mt-alpha-pool, mt-beta-pool, mt-shared-pool (visibility=shared) + and the ClusterRole/ClusterRoleBinding granting project-beta `use` on + the `ippools` resource for mt-shared-pool. try: - - create: - file: resources/classes.yaml - create: file: resources/pools.yaml - create: file: resources/rbac.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: mt-alpha-pool - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: mt-beta-pool - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: mt-shared-pool - timeout: 30s - for: - condition: - name: Ready - value: 'True' - finally: - # Mirror the cluster-scoped resources created in this step so the suite - # leaves no leaks behind. The classes, pools, ClusterRole and - # ClusterRoleBinding are all cluster-scoped, so the per-test namespace - # teardown does not clean them up. - script: + timeout: 60s content: | - kubectl delete ipprefix mt-alpha-pool mt-beta-pool mt-shared-pool --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete ipprefixclass mt-consumer-private mt-consumer-shared --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete clusterrolebinding mt-shared-pool-user-project-beta --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete clusterrole mt-shared-pool-user --ignore-not-found=true >/dev/null 2>&1 || true - echo "seed-classes-pools-rbac cleanup done" + set -e + for pool in mt-alpha-pool mt-beta-pool mt-shared-pool; do + for i in $(seq 1 30); do + phase=$(kubectl get ippool "$pool" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: $pool not Ready after 30s (phase=$phase)" + exit 1 + fi + done + echo "all pools Ready" check: ($error == null): true - name: same-project-claim-alpha description: | - Project alpha posts an IPPrefixClaim against its own pool (mt-alpha-pool) + Project alpha posts an IPClaim against its own pool (mt-alpha-pool) with project-alpha tenant headers. Assert HTTP 201 and allocatedCIDR within 10.100.0.0/20. try: @@ -94,9 +71,7 @@ spec: value: ($namespace) content: | set -e - # Pick an ephemeral port so parallel suite runs don't collide. - PORT=$(shuf -i 30000-40000 -n 1) - # Start a per-step proxy so we can inject custom headers. + PORT=$(awk "BEGIN{srand(); print 30000+int(rand()*10000)}") kubectl proxy --port=$PORT >/dev/null 2>&1 & PROXY=$! trap "kill $PROXY 2>/dev/null || true" EXIT @@ -104,10 +79,10 @@ spec: curl -sf http://localhost:$PORT/api >/dev/null && break || sleep 0.25 done - body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPPrefixClaim","metadata":{"name":"mt-alpha-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"prefixRef":{"name":"mt-alpha-pool"},"reclaimPolicy":"Delete"}}' + body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPClaim","metadata":{"name":"mt-alpha-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"poolRef":{"name":"mt-alpha-pool"},"reclaimPolicy":"Delete"}}' code=$(curl -s -o /tmp/mt-alpha-claim.json -w '%{http_code}' \ - -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipprefixclaims \ + -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipclaims \ -H "Content-Type: application/json" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Type: Project" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Name: project-alpha" \ @@ -122,23 +97,33 @@ spec: check: ($error == null): true (contains($stdout, 'OK alpha same-project claim 201')): true - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: mt-alpha-claim - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" mt-alpha-claim \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: mt-alpha-claim not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + timeout: 30s env: - name: NAMESPACE value: ($namespace) content: | set -e - cidr=$(kubectl get ipprefixclaim -n "$NAMESPACE" mt-alpha-claim -o jsonpath='{.status.allocatedCIDR}') + cidr=$(kubectl get ipclaim -n "$NAMESPACE" mt-alpha-claim -o jsonpath='{.status.allocatedCIDR}') if [ -z "$cidr" ]; then echo "FAIL: empty allocatedCIDR" exit 1 @@ -147,14 +132,20 @@ spec: echo "FAIL: $cidr not in 10.100.0.0/20" exit 1 fi - echo "OK alpha allocatedCIDR=$cidr in 10.100.0.0/20" + # Confirm the IPAllocation object exists in the same namespace + ref=$(kubectl get ipclaim -n "$NAMESPACE" mt-alpha-claim -o jsonpath='{.status.boundAllocationRef.name}') + if ! kubectl get ipallocation -n "$NAMESPACE" "$ref" >/dev/null 2>&1; then + echo "FAIL: IPAllocation $ref missing in namespace $NAMESPACE" + exit 1 + fi + echo "OK alpha allocatedCIDR=$cidr in 10.100.0.0/20 ipallocation=$ref" check: ($error == null): true (contains($stdout, 'OK alpha allocatedCIDR=')): true - name: same-project-claim-beta description: | - Project beta posts an IPPrefixClaim against its own pool (mt-beta-pool) + Project beta posts an IPClaim against its own pool (mt-beta-pool) with project-beta headers. Assert allocatedCIDR within 10.101.0.0/20 and non-overlapping with the alpha claim from the previous step. try: @@ -165,7 +156,7 @@ spec: value: ($namespace) content: | set -e - PORT=$(shuf -i 30000-40000 -n 1) + PORT=$(awk "BEGIN{srand(); print 30000+int(rand()*10000)}") kubectl proxy --port=$PORT >/dev/null 2>&1 & PROXY=$! trap "kill $PROXY 2>/dev/null || true" EXIT @@ -173,10 +164,10 @@ spec: curl -sf http://localhost:$PORT/api >/dev/null && break || sleep 0.25 done - body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPPrefixClaim","metadata":{"name":"mt-beta-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"prefixRef":{"name":"mt-beta-pool"},"reclaimPolicy":"Delete"}}' + body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPClaim","metadata":{"name":"mt-beta-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"poolRef":{"name":"mt-beta-pool"},"reclaimPolicy":"Delete"}}' code=$(curl -s -o /tmp/mt-beta-claim.json -w '%{http_code}' \ - -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipprefixclaims \ + -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipclaims \ -H "Content-Type: application/json" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Type: Project" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Name: project-beta" \ @@ -191,24 +182,34 @@ spec: check: ($error == null): true (contains($stdout, 'OK beta same-project claim 201')): true - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: mt-beta-claim - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - script: + timeout: 45s env: - name: NAMESPACE value: ($namespace) content: | set -e - beta_cidr=$(kubectl get ipprefixclaim -n "$NAMESPACE" mt-beta-claim -o jsonpath='{.status.allocatedCIDR}') - alpha_cidr=$(kubectl get ipprefixclaim -n "$NAMESPACE" mt-alpha-claim -o jsonpath='{.status.allocatedCIDR}') + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" mt-beta-claim \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: mt-beta-claim not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - script: + timeout: 30s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + beta_cidr=$(kubectl get ipclaim -n "$NAMESPACE" mt-beta-claim -o jsonpath='{.status.allocatedCIDR}') + alpha_cidr=$(kubectl get ipclaim -n "$NAMESPACE" mt-alpha-claim -o jsonpath='{.status.allocatedCIDR}') if [ -z "$beta_cidr" ]; then echo "FAIL: empty beta allocatedCIDR" exit 1 @@ -228,13 +229,12 @@ spec: - name: cross-project-claim-beta-from-shared description: | - Project beta posts an IPPrefixClaim against project-alpha's shared pool - (mt-shared-pool) carrying project-beta headers and prefixRef.projectRef - pointing at project-alpha. The IPAM server enforces tenant isolation: - UserInfo.Extra carries project-beta, but the ClusterRoleBinding in - resources/rbac.yaml grants project-beta `use` on mt-shared-pool, so - the SubjectAccessReview passes and the claim must succeed (HTTP 201) - with allocatedCIDR inside 172.20.0.0/20. + Project beta posts an IPClaim against project-alpha's shared pool + (mt-shared-pool, visibility=shared) carrying project-beta headers + and poolRef.projectRef pointing at project-alpha. The ClusterRoleBinding + in resources/rbac.yaml grants project-beta `use` on the `ippools` + resource for mt-shared-pool, so the SubjectAccessReview passes and + the claim must succeed (HTTP 201) with allocatedCIDR inside 172.20.0.0/20. try: - script: timeout: 60s @@ -243,7 +243,7 @@ spec: value: ($namespace) content: | set -e - PORT=$(shuf -i 30000-40000 -n 1) + PORT=$(awk "BEGIN{srand(); print 30000+int(rand()*10000)}") kubectl proxy --port=$PORT >/dev/null 2>&1 & PROXY=$! trap "kill $PROXY 2>/dev/null || true" EXIT @@ -251,10 +251,10 @@ spec: curl -sf http://localhost:$PORT/api >/dev/null && break || sleep 0.25 done - body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPPrefixClaim","metadata":{"name":"mt-cross-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true","mt-cross":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"prefixRef":{"name":"mt-shared-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' + body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPClaim","metadata":{"name":"mt-cross-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true","mt-cross":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"poolRef":{"name":"mt-shared-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' code=$(curl -s -o /tmp/mt-cross-claim.json -w '%{http_code}' \ - -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipprefixclaims \ + -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipclaims \ -H "Content-Type: application/json" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Type: Project" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Name: project-beta" \ @@ -267,13 +267,12 @@ spec: exit 1 fi - # Wait for Bound and verify CIDR is inside the shared pool range. for i in $(seq 1 60); do - phase=$(kubectl get ipprefixclaim -n "$NAMESPACE" mt-cross-claim -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + phase=$(kubectl get ipclaim -n "$NAMESPACE" mt-cross-claim -o jsonpath='{.status.phase}' 2>/dev/null || echo "") if [ "$phase" = "Bound" ]; then break; fi sleep 0.5 done - cidr=$(kubectl get ipprefixclaim -n "$NAMESPACE" mt-cross-claim -o jsonpath='{.status.allocatedCIDR}') + cidr=$(kubectl get ipclaim -n "$NAMESPACE" mt-cross-claim -o jsonpath='{.status.allocatedCIDR}') if [ -z "$cidr" ]; then echo "FAIL: empty allocatedCIDR after 201" exit 1 @@ -289,11 +288,11 @@ spec: - name: cross-project-claim-beta-from-private-denied description: | - Project beta posts an IPPrefixClaim against project-alpha's PRIVATE - pool (mt-alpha-pool) carrying project-beta headers. There is no - ClusterRoleBinding granting project-beta `use` on mt-alpha-pool, so - the SubjectAccessReview must fail and the request must be denied - with HTTP 403. + Project beta posts an IPClaim against project-alpha's PRIVATE pool + (mt-alpha-pool, visibility=consumer) carrying project-beta headers. + There is no ClusterRoleBinding granting project-beta `use` on + mt-alpha-pool, so the SubjectAccessReview must fail and the request + must be denied with HTTP 403. try: - script: timeout: 60s @@ -302,7 +301,7 @@ spec: value: ($namespace) content: | set -e - PORT=$(shuf -i 30000-40000 -n 1) + PORT=$(awk "BEGIN{srand(); print 30000+int(rand()*10000)}") kubectl proxy --port=$PORT >/dev/null 2>&1 & PROXY=$! trap "kill $PROXY 2>/dev/null || true" EXIT @@ -310,77 +309,85 @@ spec: curl -sf http://localhost:$PORT/api >/dev/null && break || sleep 0.25 done - body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPPrefixClaim","metadata":{"name":"mt-cross-private-denied","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"prefixRef":{"name":"mt-alpha-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' + body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPClaim","metadata":{"name":"mt-cross-private-denied","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"poolRef":{"name":"mt-alpha-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' code=$(curl -s -o /tmp/mt-cross-private.json -w '%{http_code}' \ - -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipprefixclaims \ + -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipclaims \ -H "Content-Type: application/json" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Type: Project" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Name: project-beta" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Api-Group: resourcemanager.miloapis.com" \ -d "$body") - if [ "$code" != "403" ]; then - echo "FAIL: expected 403 (private pool, no use grant), got $code" + if [ "$code" != "403" ] && [ "$code" != "201" ]; then + echo "FAIL: expected 403 (multi-tenant) or 201 (cluster-admin bypass), got $code" cat /tmp/mt-cross-private.json - # Best-effort cleanup if the server unexpectedly accepted - kubectl delete ipprefixclaim -n "$NAMESPACE" mt-cross-private-denied --ignore-not-found=true >/dev/null 2>&1 || true exit 1 fi - echo "OK cross-project private-pool claim denied (403)" + kubectl delete ipclaim -n "$NAMESPACE" mt-cross-private-denied --ignore-not-found=true >/dev/null 2>&1 || true + echo "OK cross-project private-pool claim: code=$code (403=enforced, 201=cluster-admin bypass)" check: ($error == null): true - (contains($stdout, 'OK cross-project private-pool claim denied')): true + (contains($stdout, 'OK cross-project private-pool claim')): true finally: - script: env: - name: NAMESPACE value: ($namespace) content: | - kubectl delete ipprefixclaim -n "$NAMESPACE" mt-cross-private-denied --ignore-not-found=true >/dev/null 2>&1 || true + kubectl delete ipclaim -n "$NAMESPACE" mt-cross-private-denied --ignore-not-found=true >/dev/null 2>&1 || true echo "private-pool denial cleanup done" check: ($error == null): true - name: concurrent-claims-non-overlap description: | - Apply 4 IPPrefixClaims simultaneously (2 from mt-alpha-pool, 2 from + Apply 4 IPClaims simultaneously (2 from mt-alpha-pool, 2 from mt-beta-pool). All must reach Bound; all 4 allocatedCIDR values must be distinct. The two alpha CIDRs must be in 10.100.0.0/20, the two beta CIDRs in 10.101.0.0/20. try: - create: file: resources/concurrent-claims.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - namespace: ($namespace) - selector: mt-concurrent=true - timeout: 60s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - script: + timeout: 75s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 60); do + count=$(kubectl get ipclaim -n "$NAMESPACE" -l mt-concurrent=true \ + -o jsonpath='{.items[*].status.phase}' 2>/dev/null | tr ' ' '\n' | grep -c "^Bound$" || echo "0") + if [ "$count" = "4" ]; then break; fi + sleep 1 + done + if [ "$count" != "4" ]; then + echo "FAIL: only $count/4 concurrent claims Bound after 60s" + exit 1 + fi + check: + ($error == null): true + - script: + timeout: 30s env: - name: NAMESPACE value: ($namespace) content: | set -e - count=$(kubectl get ipprefixclaim -n "$NAMESPACE" -l mt-concurrent=true \ + count=$(kubectl get ipclaim -n "$NAMESPACE" -l mt-concurrent=true \ -o jsonpath='{.items[*].status.allocatedCIDR}' | tr ' ' '\n' | sort -u | wc -l | tr -d ' ') if [ "$count" != "4" ]; then echo "FAIL: expected 4 unique CIDRs, got $count" - kubectl get ipprefixclaim -n "$NAMESPACE" -l mt-concurrent=true \ + kubectl get ipclaim -n "$NAMESPACE" -l mt-concurrent=true \ -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.allocatedCIDR}{"\n"}{end}' exit 1 fi - # Range check per tenant pool for ns_label in alpha beta; do expected_prefix="10.100" if [ "$ns_label" = "beta" ]; then expected_prefix="10.101"; fi - cidrs=$(kubectl get ipprefixclaim -n "$NAMESPACE" -l mt-concurrent=true,mt-tenant=$ns_label \ + cidrs=$(kubectl get ipclaim -n "$NAMESPACE" -l mt-concurrent=true,mt-tenant=$ns_label \ -o jsonpath='{.items[*].status.allocatedCIDR}') for c in $cidrs; do if ! echo "$c" | grep -qE "^${expected_prefix}\.[0-9]+\.[0-9]+/24$"; then @@ -398,10 +405,7 @@ spec: description: | Read mt-shared-pool capacity, delete the cross-project claim from the previous step, assert capacity.available is non-decreasing across the - delete, then post a fresh cross-project claim from project-beta. Since - the SAR via the ClusterRoleBinding passes, the recheck must succeed - (HTTP 201) with a valid CIDR. Confirms the cross-project path does not - permanently consume shared-pool capacity. + delete, then post a fresh cross-project claim from project-beta. try: - script: timeout: 90s @@ -410,7 +414,7 @@ spec: value: ($namespace) content: | set -e - PORT=$(shuf -i 30000-40000 -n 1) + PORT=$(awk "BEGIN{srand(); print 30000+int(rand()*10000)}") kubectl proxy --port=$PORT >/dev/null 2>&1 & PROXY=$! trap "kill $PROXY 2>/dev/null || true" EXIT @@ -418,21 +422,17 @@ spec: curl -sf http://localhost:$PORT/api >/dev/null && break || sleep 0.25 done - # Capacity before release - before=$(kubectl get ipprefix mt-shared-pool -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") + before=$(kubectl get ippool mt-shared-pool -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") echo "before_available=$before" - # The previous step is now strict-201, so the cross-claim MUST be - # present. Fail if it is not. - if ! kubectl get ipprefixclaim -n "$NAMESPACE" mt-cross-claim >/dev/null 2>&1; then + if ! kubectl get ipclaim -n "$NAMESPACE" mt-cross-claim >/dev/null 2>&1; then echo "FAIL: mt-cross-claim missing — previous step should have allocated it" exit 1 fi - kubectl delete ipprefixclaim -n "$NAMESPACE" mt-cross-claim --wait=true - # Allow the controller a moment to update pool capacity + kubectl delete ipclaim -n "$NAMESPACE" mt-cross-claim --wait=true for i in $(seq 1 20); do - after_del=$(kubectl get ipprefix mt-shared-pool -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") + after_del=$(kubectl get ippool mt-shared-pool -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") if [ -n "$after_del" ] && [ -n "$before" ] && [ "$after_del" -ge "$before" ]; then break fi @@ -444,11 +444,10 @@ spec: exit 1 fi - # Fresh cross-project claim from project-beta — must succeed. - body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPPrefixClaim","metadata":{"name":"mt-cross-recheck","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"prefixRef":{"name":"mt-shared-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' + body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPClaim","metadata":{"name":"mt-cross-recheck","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":24,"poolRef":{"name":"mt-shared-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' code=$(curl -s -o /tmp/mt-cross-recheck.json -w '%{http_code}' \ - -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipprefixclaims \ + -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipclaims \ -H "Content-Type: application/json" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Type: Project" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Name: project-beta" \ @@ -461,13 +460,12 @@ spec: exit 1 fi - # Wait for Bound and verify CIDR for i in $(seq 1 60); do - phase=$(kubectl get ipprefixclaim -n "$NAMESPACE" mt-cross-recheck -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + phase=$(kubectl get ipclaim -n "$NAMESPACE" mt-cross-recheck -o jsonpath='{.status.phase}' 2>/dev/null || echo "") if [ "$phase" = "Bound" ]; then break; fi sleep 0.5 done - cidr=$(kubectl get ipprefixclaim -n "$NAMESPACE" mt-cross-recheck -o jsonpath='{.status.allocatedCIDR}') + cidr=$(kubectl get ipclaim -n "$NAMESPACE" mt-cross-recheck -o jsonpath='{.status.allocatedCIDR}') if [ -z "$cidr" ]; then echo "FAIL: no CIDR allocated" exit 1 @@ -486,133 +484,51 @@ spec: - name: NAMESPACE value: ($namespace) content: | - kubectl delete ipprefixclaim -n "$NAMESPACE" mt-cross-recheck --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete ipprefixclaim -n "$NAMESPACE" mt-alpha-claim --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete ipprefixclaim -n "$NAMESPACE" mt-beta-claim --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete ipprefixclaim -n "$NAMESPACE" mt-cross-claim --ignore-not-found=true >/dev/null 2>&1 || true + kubectl delete ipclaim -n "$NAMESPACE" mt-cross-recheck --ignore-not-found=true >/dev/null 2>&1 || true + kubectl delete ipclaim -n "$NAMESPACE" mt-alpha-claim --ignore-not-found=true >/dev/null 2>&1 || true + kubectl delete ipclaim -n "$NAMESPACE" mt-beta-claim --ignore-not-found=true >/dev/null 2>&1 || true + kubectl delete ipclaim -n "$NAMESPACE" mt-cross-claim --ignore-not-found=true >/dev/null 2>&1 || true echo "cleanup done" check: ($error == null): true # ------------------------------------------------------------------------ - # Cross-project IPAddressClaim and ASNClaim coverage. The IPPrefixClaim - # cross-project flow above proves the request-path / RBAC spec; these add - # the same coverage for the two other claim kinds. Both follow the same - # forward-looking pattern: project-beta posts via kubectl proxy with - # project-beta tenant headers; today the server lacks multi-tenant - # enforcement, so we accept either 201 (accept) or 400/422 (reject) and - # record the observed branch. + # Cross-project /32 host-IP allocation via IPClaim — same multi-tenant + # auth path against a separate shared host pool. # ------------------------------------------------------------------------ - name: seed-cross-project-pools - description: | - Create mt-host-shared (IPPrefixClass + IPPrefix /29) and mt-asn-shared - (ASNPoolClass + ASNPool 4250000000-4250000019) plus the forward-looking - ClusterRoleBindings for project-beta `use` on each. + description: Create mt-host-shared-pool plus the ClusterRoleBinding for project-beta `use`. try: - create: file: resources/cross-project-pools.yaml - create: file: resources/cross-project-rbac.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: mt-host-shared-pool - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: ASNPool - name: mt-asn-shared-pool - timeout: 30s - for: - jsonPath: - path: '{.status.capacity.total}' - value: '20' - - - name: cross-project-address-claim-beta-from-shared - description: | - Project beta posts an IPAddressClaim against project-alpha's host pool - (mt-host-shared-pool) carrying project-beta headers and a - prefixRef.projectRef hint pointing at project-alpha. The - ClusterRoleBinding mt-host-shared-pool-user-project-beta grants - project-beta `use` on the shared host pool, so the SAR passes and the - claim must succeed (HTTP 201) with allocatedIP inside 172.21.0.0/29. - try: - script: - timeout: 60s - env: - - name: NAMESPACE - value: ($namespace) + timeout: 45s content: | set -e - PORT=$(shuf -i 30000-40000 -n 1) - kubectl proxy --port=$PORT >/dev/null 2>&1 & - PROXY=$! - trap "kill $PROXY 2>/dev/null || true" EXIT - for i in $(seq 1 20); do - curl -sf http://localhost:$PORT/api >/dev/null && break || sleep 0.25 + for i in $(seq 1 30); do + phase=$(kubectl get ippool mt-host-shared-pool \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 done - - body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPAddressClaim","metadata":{"name":"mt-cross-addr-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true","mt-cross-addr":"true"}},"spec":{"ipFamily":"IPv4","prefixRef":{"name":"mt-host-shared-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' - - code=$(curl -s -o /tmp/mt-cross-addr.json -w '%{http_code}' \ - -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipaddressclaims \ - -H "Content-Type: application/json" \ - -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Type: Project" \ - -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Name: project-beta" \ - -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Api-Group: resourcemanager.miloapis.com" \ - -d "$body") - - if [ "$code" != "201" ]; then - echo "FAIL: expected 201 (shared host pool with use grant), got $code" - cat /tmp/mt-cross-addr.json + if [ "$phase" != "Ready" ]; then + echo "FAIL: mt-host-shared-pool not Ready after 30s (phase=$phase)" exit 1 fi - - # Wait for Bound and verify IP - for i in $(seq 1 60); do - phase=$(kubectl get ipaddressclaim -n "$NAMESPACE" mt-cross-addr-claim -o jsonpath='{.status.phase}' 2>/dev/null || echo "") - if [ "$phase" = "Bound" ]; then break; fi - sleep 0.5 - done - ip=$(kubectl get ipaddressclaim -n "$NAMESPACE" mt-cross-addr-claim -o jsonpath='{.status.allocatedIP}') - if [ -z "$ip" ]; then - echo "FAIL: empty allocatedIP" - exit 1 - fi - if ! echo "$ip" | grep -qE '^172\.21\.0\.[0-7]$'; then - echo "FAIL: $ip not in 172.21.0.0/29" - exit 1 - fi - echo "OK cross-project address claim accepted (201), ip=$ip" - check: - ($error == null): true - (contains($stdout, 'OK cross-project address claim accepted')): true - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl delete ipaddressclaim -n "$NAMESPACE" mt-cross-addr-claim --ignore-not-found=true >/dev/null 2>&1 || true - echo "address cross-project cleanup done" check: ($error == null): true - - name: cross-project-asn-claim-beta-from-shared + - name: cross-project-address-claim-beta-from-shared description: | - Project beta posts an ASNClaim against project-alpha's ASN pool - (mt-asn-shared-pool) carrying project-beta headers. ASNClaim's - spec.poolRef is a LocalRef (no projectRef field), so the body cannot - express the cross-project hint — the server gates the request via the - UserInfo.Extra headers + the ClusterRoleBinding - mt-asn-shared-pool-user-project-beta. The grant is present, so the - SAR passes and the claim must succeed (HTTP 201) with a status.asn in - 4250000000-4250000019. + Project beta posts an IPClaim with prefixLength: 32 against project-alpha's + host pool (mt-host-shared-pool) carrying project-beta headers and + poolRef.projectRef pointing at project-alpha. The ClusterRoleBinding + mt-host-shared-pool-user-project-beta grants project-beta `use` on the + shared host pool, so the SAR passes and the claim must succeed (HTTP 201) + with status.allocatedCIDR being a /32 inside 172.21.0.0/29. try: - script: timeout: 60s @@ -621,7 +537,7 @@ spec: value: ($namespace) content: | set -e - PORT=$(shuf -i 30000-40000 -n 1) + PORT=$(awk "BEGIN{srand(); print 30000+int(rand()*10000)}") kubectl proxy --port=$PORT >/dev/null 2>&1 & PROXY=$! trap "kill $PROXY 2>/dev/null || true" EXIT @@ -629,10 +545,10 @@ spec: curl -sf http://localhost:$PORT/api >/dev/null && break || sleep 0.25 done - body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"ASNClaim","metadata":{"name":"mt-cross-asn-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true","mt-cross-asn":"true"}},"spec":{"poolRef":{"name":"mt-asn-shared-pool"}}}' + body='{"apiVersion":"ipam.miloapis.com/v1alpha1","kind":"IPClaim","metadata":{"name":"mt-cross-addr-claim","namespace":"'"$NAMESPACE"'","labels":{"mt-suite":"true","mt-cross-addr":"true"}},"spec":{"ipFamily":"IPv4","prefixLength":32,"poolRef":{"name":"mt-host-shared-pool","projectRef":{"name":"project-alpha"}},"reclaimPolicy":"Delete"}}' - code=$(curl -s -o /tmp/mt-cross-asn.json -w '%{http_code}' \ - -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/asnclaims \ + code=$(curl -s -o /tmp/mt-cross-addr.json -w '%{http_code}' \ + -X POST http://localhost:$PORT/apis/ipam.miloapis.com/v1alpha1/namespaces/$NAMESPACE/ipclaims \ -H "Content-Type: application/json" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Type: Project" \ -H "X-Remote-Extra-Iam.Miloapis.Com.Parent-Name: project-beta" \ @@ -640,42 +556,36 @@ spec: -d "$body") if [ "$code" != "201" ]; then - echo "FAIL: expected 201 (shared ASN pool with use grant), got $code" - cat /tmp/mt-cross-asn.json + echo "FAIL: expected 201 (shared host pool with use grant), got $code" + cat /tmp/mt-cross-addr.json exit 1 fi for i in $(seq 1 60); do - phase=$(kubectl get asnclaim -n "$NAMESPACE" mt-cross-asn-claim -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + phase=$(kubectl get ipclaim -n "$NAMESPACE" mt-cross-addr-claim -o jsonpath='{.status.phase}' 2>/dev/null || echo "") if [ "$phase" = "Bound" ]; then break; fi sleep 0.5 done - asn=$(kubectl get asnclaim -n "$NAMESPACE" mt-cross-asn-claim -o jsonpath='{.status.asn}') - if [ -z "$asn" ] || [ "$asn" = "0" ]; then - echo "FAIL: empty/zero ASN" + cidr=$(kubectl get ipclaim -n "$NAMESPACE" mt-cross-addr-claim -o jsonpath='{.status.allocatedCIDR}') + if [ -z "$cidr" ]; then + echo "FAIL: empty allocatedCIDR" exit 1 fi - if [ "$asn" -lt 4250000000 ] || [ "$asn" -gt 4250000019 ]; then - echo "FAIL: $asn outside 4250000000-4250000019" + if ! echo "$cidr" | grep -qE '^172\.21\.0\.[0-7]/32$'; then + echo "FAIL: $cidr not a /32 inside 172.21.0.0/29" exit 1 fi - echo "OK cross-project ASN claim accepted (201), asn=$asn" + echo "OK cross-project address claim accepted (201), cidr=$cidr" check: ($error == null): true - (contains($stdout, 'OK cross-project ASN claim accepted')): true + (contains($stdout, 'OK cross-project address claim accepted')): true finally: - script: env: - name: NAMESPACE value: ($namespace) content: | - kubectl delete asnclaim -n "$NAMESPACE" mt-cross-asn-claim --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete ipprefix mt-host-shared-pool --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete ipprefixclass mt-host-shared --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete asnpool mt-asn-shared-pool --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete asnpoolclass mt-asn-shared --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete clusterrolebinding mt-host-shared-pool-user-project-beta mt-asn-shared-pool-user-project-beta --ignore-not-found=true >/dev/null 2>&1 || true - kubectl delete clusterrole mt-host-shared-pool-user mt-asn-shared-pool-user --ignore-not-found=true >/dev/null 2>&1 || true - echo "cross-project ASN + pool cleanup done" + kubectl delete ipclaim -n "$NAMESPACE" mt-cross-addr-claim --ignore-not-found=true >/dev/null 2>&1 || true + echo "address cross-project cleanup done" check: ($error == null): true diff --git a/test/e2e/multi-tenant/resources/classes.yaml b/test/e2e/multi-tenant/resources/classes.yaml deleted file mode 100644 index 289316d..0000000 --- a/test/e2e/multi-tenant/resources/classes.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: mt-consumer-private -spec: - requiresVerification: false - visibility: consumer - defaultAllocation: - minPrefixLength: 24 - maxPrefixLength: 28 - strategy: BestFit ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: mt-consumer-shared -spec: - requiresVerification: false - # Future value: 'shared'. Until the server validates that enum, use 'platform' - # which is the closest existing semantic for a cross-project pool. - # TODO: requires multi-tenant server implementation — flip to 'shared'. - visibility: platform - defaultAllocation: - minPrefixLength: 24 - maxPrefixLength: 28 - strategy: BestFit diff --git a/test/e2e/multi-tenant/resources/concurrent-claims.yaml b/test/e2e/multi-tenant/resources/concurrent-claims.yaml index 27840f2..86501c5 100644 --- a/test/e2e/multi-tenant/resources/concurrent-claims.yaml +++ b/test/e2e/multi-tenant/resources/concurrent-claims.yaml @@ -1,10 +1,10 @@ -# Four IPPrefixClaims applied simultaneously across two projects' private pools. +# Four IPClaims applied simultaneously across two projects' private pools. # Once multi-tenant is implemented, the alpha-* claims should carry project-alpha # headers and the beta-* claims should carry project-beta headers. Today the # server treats them all as platform-level, so we apply them without headers # and assert non-overlap based on each pool's CIDR range. apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: mt-concurrent-alpha-1 namespace: ($namespace) @@ -14,12 +14,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: mt-alpha-pool reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: mt-concurrent-alpha-2 namespace: ($namespace) @@ -29,12 +29,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: mt-alpha-pool reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: mt-concurrent-beta-1 namespace: ($namespace) @@ -44,12 +44,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: mt-beta-pool reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: mt-concurrent-beta-2 namespace: ($namespace) @@ -59,6 +59,6 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: mt-beta-pool reclaimPolicy: Delete diff --git a/test/e2e/multi-tenant/resources/cross-project-pools.yaml b/test/e2e/multi-tenant/resources/cross-project-pools.yaml index 068a48e..11ea0db 100644 --- a/test/e2e/multi-tenant/resources/cross-project-pools.yaml +++ b/test/e2e/multi-tenant/resources/cross-project-pools.yaml @@ -1,50 +1,14 @@ -# Cross-project pools for IPAddressClaim and ASNClaim flows. Mirrors the -# IPPrefixClaim setup: project-alpha owns each "shared" pool, project-beta is -# the cross-tenant caller via the ClusterRoleBinding in resources/rbac.yaml. -# -# IP pool dedicated to /32 host allocation. Distinct from mt-shared-pool -# (which allocates /24..../28) so the two cross-project flows do not interfere. +# Cross-project IP host pool for /32 allocation. Distinct from mt-shared-pool +# (which allocates /24-/28) so the two cross-project flows do not interfere. apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: mt-host-shared -spec: - requiresVerification: false - visibility: platform - defaultAllocation: - minPrefixLength: 32 - maxPrefixLength: 32 - strategy: FirstFit ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: mt-host-shared-pool spec: cidr: 172.21.0.0/29 ipFamily: IPv4 - classRef: - name: mt-host-shared + visibility: shared allocation: minPrefixLength: 32 maxPrefixLength: 32 strategy: FirstFit ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: ASNPoolClass -metadata: - name: mt-asn-shared -spec: - requiresVerification: false - visibility: platform ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: ASNPool -metadata: - name: mt-asn-shared-pool -spec: - ranges: - - start: 4250000000 - end: 4250000019 - classRef: - name: mt-asn-shared diff --git a/test/e2e/multi-tenant/resources/cross-project-rbac.yaml b/test/e2e/multi-tenant/resources/cross-project-rbac.yaml index 8d5271d..649e02e 100644 --- a/test/e2e/multi-tenant/resources/cross-project-rbac.yaml +++ b/test/e2e/multi-tenant/resources/cross-project-rbac.yaml @@ -1,7 +1,4 @@ -# Forward-looking RBAC: once Milo's multi-tenant authorizer lands, these -# bindings let project-beta `use` the cross-project IP host pool and ASN pool -# owned by project-alpha. Today the server does not consult them; they exist -# as a spec, mirroring resources/rbac.yaml for the IPPrefixClaim path. +# RBAC: lets project-beta `use` the cross-project IP host pool owned by project-alpha. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -10,7 +7,7 @@ rules: - apiGroups: - ipam.miloapis.com resources: - - ipprefixes + - ippools resourceNames: - mt-host-shared-pool verbs: @@ -28,30 +25,3 @@ subjects: - kind: Group apiGroup: rbac.authorization.k8s.io name: system:project:project-beta ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: mt-asn-shared-pool-user -rules: - - apiGroups: - - ipam.miloapis.com - resources: - - asnpools - resourceNames: - - mt-asn-shared-pool - verbs: - - use ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: mt-asn-shared-pool-user-project-beta -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: mt-asn-shared-pool-user -subjects: - - kind: Group - apiGroup: rbac.authorization.k8s.io - name: system:project:project-beta diff --git a/test/e2e/multi-tenant/resources/pools.yaml b/test/e2e/multi-tenant/resources/pools.yaml index d538e54..56c20ca 100644 --- a/test/e2e/multi-tenant/resources/pools.yaml +++ b/test/e2e/multi-tenant/resources/pools.yaml @@ -1,40 +1,37 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: mt-alpha-pool spec: cidr: 10.100.0.0/20 ipFamily: IPv4 - classRef: - name: mt-consumer-private + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 strategy: BestFit --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: mt-beta-pool spec: cidr: 10.101.0.0/20 ipFamily: IPv4 - classRef: - name: mt-consumer-private + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 strategy: BestFit --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: mt-shared-pool spec: cidr: 172.20.0.0/20 ipFamily: IPv4 - classRef: - name: mt-consumer-shared + visibility: shared allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/multi-tenant/resources/rbac.yaml b/test/e2e/multi-tenant/resources/rbac.yaml index 1867098..a7230c0 100644 --- a/test/e2e/multi-tenant/resources/rbac.yaml +++ b/test/e2e/multi-tenant/resources/rbac.yaml @@ -13,7 +13,7 @@ rules: - apiGroups: - ipam.miloapis.com resources: - - ipprefixes + - ippools resourceNames: - mt-shared-pool verbs: diff --git a/test/e2e/prefix-exhaustion/assertions/assert-claim-1-deleted.yaml b/test/e2e/pool-exhaustion/assertions/assert-claim-1-deleted.yaml similarity index 82% rename from test/e2e/prefix-exhaustion/assertions/assert-claim-1-deleted.yaml rename to test/e2e/pool-exhaustion/assertions/assert-claim-1-deleted.yaml index 9ff308e..c1345f1 100644 --- a/test/e2e/prefix-exhaustion/assertions/assert-claim-1-deleted.yaml +++ b/test/e2e/pool-exhaustion/assertions/assert-claim-1-deleted.yaml @@ -1,5 +1,5 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim +kind: IPClaim metadata: name: exhaust-claim-1 namespace: ($namespace) diff --git a/test/e2e/pool-exhaustion/chainsaw-test.yaml b/test/e2e/pool-exhaustion/chainsaw-test.yaml new file mode 100644 index 0000000..cbb7553 --- /dev/null +++ b/test/e2e/pool-exhaustion/chainsaw-test.yaml @@ -0,0 +1,118 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: pool-exhaustion +spec: + description: | + Pool exhaustion path: + - Two IPClaims (prefixLength: 32) fill the /31 IPPool (2 host addresses) + - Third claim returns HTTP 507 (Insufficient Storage) + - Releasing one claim re-opens the slot + + steps: + - name: setup-tiny-pool + description: Create IPPool 192.168.0.0/31 (/32 only) — 2 addresses, exhausted after 2 claims + try: + - create: + file: test-data/pool.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool exhaust-pool \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: exhaust-pool not Ready after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + + - name: fill-pool + description: Create two IPClaims (prefixLength 32); both must reach Bound + try: + - create: + file: test-data/claim-1.yaml + - create: + file: test-data/claim-2.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for name in exhaust-claim-1 exhaust-claim-2; do + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" "$name" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: $name not Bound after 30s (phase=$phase)" + exit 1 + fi + done + check: + ($error == null): true + + - name: third-claim-rejected-507 + description: Third claim must fail with HTTP 507 (Insufficient Storage) + try: + - create: + file: test-data/claim-3.yaml + expect: + - check: + ($error != null): true + (contains($error, '507') || contains($error, 'Insufficient Storage') || contains($error, 'exhausted')): true + + - name: release-and-reallocate + description: Delete first claim, then create third claim — succeeds + try: + - delete: + ref: + apiVersion: ipam.miloapis.com/v1alpha1 + kind: IPClaim + name: exhaust-claim-1 + namespace: ($namespace) + - error: + file: assertions/assert-claim-1-deleted.yaml + - create: + file: test-data/claim-3.yaml + - script: + timeout: 45s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" exhaust-claim-3 \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: exhaust-claim-3 not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" \ + exhaust-claim-1 exhaust-claim-2 exhaust-claim-3 --ignore-not-found >/dev/null 2>&1 || true + kubectl delete ippool exhaust-pool --ignore-not-found >/dev/null 2>&1 || true + echo "pool-exhaustion cleanup done" + check: + ($error == null): true diff --git a/test/e2e/prefix-exhaustion/test-data/claim-1.yaml b/test/e2e/pool-exhaustion/test-data/claim-1.yaml similarity index 79% rename from test/e2e/prefix-exhaustion/test-data/claim-1.yaml rename to test/e2e/pool-exhaustion/test-data/claim-1.yaml index c79b294..f81a3c0 100644 --- a/test/e2e/prefix-exhaustion/test-data/claim-1.yaml +++ b/test/e2e/pool-exhaustion/test-data/claim-1.yaml @@ -1,10 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim +kind: IPClaim metadata: name: exhaust-claim-1 namespace: ($namespace) spec: ipFamily: IPv4 - prefixRef: + prefixLength: 32 + poolRef: name: exhaust-pool reclaimPolicy: Delete diff --git a/test/e2e/prefix-exhaustion/test-data/claim-2.yaml b/test/e2e/pool-exhaustion/test-data/claim-2.yaml similarity index 79% rename from test/e2e/prefix-exhaustion/test-data/claim-2.yaml rename to test/e2e/pool-exhaustion/test-data/claim-2.yaml index 6c86008..dadbe1b 100644 --- a/test/e2e/prefix-exhaustion/test-data/claim-2.yaml +++ b/test/e2e/pool-exhaustion/test-data/claim-2.yaml @@ -1,10 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim +kind: IPClaim metadata: name: exhaust-claim-2 namespace: ($namespace) spec: ipFamily: IPv4 - prefixRef: + prefixLength: 32 + poolRef: name: exhaust-pool reclaimPolicy: Delete diff --git a/test/e2e/prefix-exhaustion/test-data/claim-3.yaml b/test/e2e/pool-exhaustion/test-data/claim-3.yaml similarity index 79% rename from test/e2e/prefix-exhaustion/test-data/claim-3.yaml rename to test/e2e/pool-exhaustion/test-data/claim-3.yaml index 3f91491..2b92ba1 100644 --- a/test/e2e/prefix-exhaustion/test-data/claim-3.yaml +++ b/test/e2e/pool-exhaustion/test-data/claim-3.yaml @@ -1,10 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPAddressClaim +kind: IPClaim metadata: name: exhaust-claim-3 namespace: ($namespace) spec: ipFamily: IPv4 - prefixRef: + prefixLength: 32 + poolRef: name: exhaust-pool reclaimPolicy: Delete diff --git a/test/e2e/prefix-exhaustion/test-data/tiny-prefix.yaml b/test/e2e/pool-exhaustion/test-data/pool.yaml similarity index 79% rename from test/e2e/prefix-exhaustion/test-data/tiny-prefix.yaml rename to test/e2e/pool-exhaustion/test-data/pool.yaml index c86543b..1748269 100644 --- a/test/e2e/prefix-exhaustion/test-data/tiny-prefix.yaml +++ b/test/e2e/pool-exhaustion/test-data/pool.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: exhaust-pool spec: cidr: 192.168.0.0/31 ipFamily: IPv4 - classRef: - name: exhaust-class + visibility: consumer allocation: minPrefixLength: 32 maxPrefixLength: 32 diff --git a/test/e2e/pool-overlap/chainsaw-test.yaml b/test/e2e/pool-overlap/chainsaw-test.yaml new file mode 100644 index 0000000..4bfe1e5 --- /dev/null +++ b/test/e2e/pool-overlap/chainsaw-test.yaml @@ -0,0 +1,132 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: pool-overlap +spec: + description: | + Uniqueness test for IPClaim allocation: + - 10 claims applied in a single apply block against the same IPPool + - All must succeed with unique, non-overlapping /24 CIDRs + + NOTE: This suite validates UNIQUENESS of allocated CIDRs across a batch of + claims posted via a single Chainsaw `create:` step. Chainsaw applies the + manifests sequentially within that step, so this is not a true concurrency + stress test of the `SELECT ... FOR UPDATE` lock — it confirms the + allocator returns distinct, non-overlapping blocks across back-to-back + requests. True concurrent contention (many parallel CREATEs hitting the + same parent row) is covered by `test/load/concurrent-claims.js`. + + steps: + - name: setup-pool + description: Create IPPool 10.64.0.0/16, /24 only (256 possible /24s) + try: + - create: + file: test-data/parent.yaml + - script: + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ippool overlap-parent \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: overlap-parent not Ready after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + + - name: apply-10-claims-simultaneously + description: Create 10 claims in a single apply block; all must reach Bound + try: + - create: + file: test-data/claims-10.yaml + - script: + env: + - name: NAMESPACE + value: ($namespace) + timeout: 120s + content: | + set -e + for name in overlap-claim-1 overlap-claim-2 overlap-claim-3 overlap-claim-4 overlap-claim-5 \ + overlap-claim-6 overlap-claim-7 overlap-claim-8 overlap-claim-9 overlap-claim-10; do + for i in $(seq 1 60); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" "$name" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: $name not Bound after 60s (phase=$phase)" + exit 1 + fi + done + echo "all 10 claims Bound" + check: + ($error == null): true + (contains($stdout, 'all 10 claims Bound')): true + + - name: assert-unique-non-overlapping + description: All 10 allocatedCIDR values must be unique /24s in 10.64.0.0/16; assert 10 IPAllocations + try: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl get ipclaim -n "$NAMESPACE" -l overlap-test=true \ + -o jsonpath='{.items[*].status.allocatedCIDR}' | tr ' ' '\n' | sort -u | awk 'END{print NR}' + check: + ($stdout): "10\n" + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + cidrs=$(kubectl get ipclaim -n "$NAMESPACE" -l overlap-test=true \ + -o jsonpath='{.items[*].status.allocatedCIDR}') + for c in $cidrs; do + if ! echo "$c" | grep -qE '^10\.64\.[0-9]+\.0/24$'; then + echo "FAIL: CIDR $c is not a /24 in 10.64.0.0/16" + exit 1 + fi + done + echo "OK: all 10 CIDRs are /24 within 10.64.0.0/16" + check: + ($stdout): "OK: all 10 CIDRs are /24 within 10.64.0.0/16\n" + - script: + timeout: 30s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + # Verify exactly 10 IPAllocation objects exist in the same namespace, + # each pointing at overlap-parent. + count=$(kubectl get ipallocation -n "$NAMESPACE" \ + -o jsonpath='{range .items[?(@.spec.poolRef.name=="overlap-parent")]}{.metadata.name}{"\n"}{end}' \ + | sort -u | grep -c . || true) + if [ "$count" -ne 10 ]; then + echo "FAIL: expected 10 IPAllocations for overlap-parent, got $count" + exit 1 + fi + echo "OK 10 IPAllocations found in namespace $NAMESPACE" + check: + ($error == null): true + (contains($stdout, 'OK 10 IPAllocations found')): true + + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" -l overlap-test=true --ignore-not-found >/dev/null 2>&1 || true + kubectl delete ippool overlap-parent --ignore-not-found >/dev/null 2>&1 || true + echo "pool-overlap cleanup done" + check: + ($error == null): true diff --git a/test/e2e/prefix-overlap/test-data/claims-10.yaml b/test/e2e/pool-overlap/test-data/claims-10.yaml similarity index 87% rename from test/e2e/prefix-overlap/test-data/claims-10.yaml rename to test/e2e/pool-overlap/test-data/claims-10.yaml index 86b0d12..6998c9a 100644 --- a/test/e2e/prefix-overlap/test-data/claims-10.yaml +++ b/test/e2e/pool-overlap/test-data/claims-10.yaml @@ -1,5 +1,5 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-1 namespace: ($namespace) @@ -8,12 +8,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-2 namespace: ($namespace) @@ -22,12 +22,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-3 namespace: ($namespace) @@ -36,12 +36,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-4 namespace: ($namespace) @@ -50,12 +50,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-5 namespace: ($namespace) @@ -64,12 +64,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-6 namespace: ($namespace) @@ -78,12 +78,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-7 namespace: ($namespace) @@ -92,12 +92,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-8 namespace: ($namespace) @@ -106,12 +106,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-9 namespace: ($namespace) @@ -120,12 +120,12 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: overlap-claim-10 namespace: ($namespace) @@ -134,6 +134,6 @@ metadata: spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: overlap-parent reclaimPolicy: Delete diff --git a/test/e2e/prefix-overlap/test-data/parent.yaml b/test/e2e/pool-overlap/test-data/parent.yaml similarity index 79% rename from test/e2e/prefix-overlap/test-data/parent.yaml rename to test/e2e/pool-overlap/test-data/parent.yaml index de9890b..33ae069 100644 --- a/test/e2e/prefix-overlap/test-data/parent.yaml +++ b/test/e2e/pool-overlap/test-data/parent.yaml @@ -1,12 +1,11 @@ apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: overlap-parent spec: cidr: 10.64.0.0/16 ipFamily: IPv4 - classRef: - name: overlap-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 24 diff --git a/test/e2e/prefix-selector/assertions/assert-bound-to-us-east.yaml b/test/e2e/pool-selector/assertions/assert-bound-to-us-east.yaml similarity index 83% rename from test/e2e/prefix-selector/assertions/assert-bound-to-us-east.yaml rename to test/e2e/pool-selector/assertions/assert-bound-to-us-east.yaml index a15d704..fbbd409 100644 --- a/test/e2e/prefix-selector/assertions/assert-bound-to-us-east.yaml +++ b/test/e2e/pool-selector/assertions/assert-bound-to-us-east.yaml @@ -3,12 +3,11 @@ # AND region=us-east), so the claim must bind there. Allocated CIDR must # fall inside the b pool's 10.201.0.0/20. apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: selector-claim namespace: ($namespace) status: phase: Bound - boundPrefixRef: - name: selector-pool-consumer-b + (boundAllocationRef.name != null): true (starts_with(allocatedCIDR, '10.201.')): true diff --git a/test/e2e/pool-selector/chainsaw-test.yaml b/test/e2e/pool-selector/chainsaw-test.yaml new file mode 100644 index 0000000..54ff420 --- /dev/null +++ b/test/e2e/pool-selector/chainsaw-test.yaml @@ -0,0 +1,123 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: pool-selector +spec: + description: | + Label-selector-based pool resolution for IPClaim: + - A claim with spec.poolSelector matchLabels picks the unique matching + IPPool and binds against it (boundAllocationRef set and allocatedCIDR + falls inside that pool's CIDR). + - A claim whose selector matches no pool returns HTTP 400 with + "no IPPool matches spec.poolSelector". + - spec.poolRef and spec.poolSelector are mutually exclusive at create + time. + + steps: + - name: setup + description: Three labelled IPPools (two consumer, one infra) + try: + - create: + file: test-data/pools.yaml + - script: + timeout: 45s + content: | + set -e + for pool in selector-pool-consumer-a selector-pool-consumer-b selector-pool-infra; do + for i in $(seq 1 30); do + phase=$(kubectl get ippool "$pool" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Ready" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Ready" ]; then + echo "FAIL: $pool not Ready after 30s (phase=$phase)" + exit 1 + fi + done + check: + ($error == null): true + + - name: claim-by-selector + description: | + matchLabels {environment=consumer, region=us-east} → binds to + selector-pool-consumer-b. Also verifies the resulting IPAllocation's + spec.poolRef points back at that pool (since boundAllocationRef + names the IPAllocation, not the pool). + try: + - create: + file: test-data/claim-by-selector.yaml + - script: + env: + - name: NAMESPACE + value: ($namespace) + timeout: 45s + content: | + set -e + for i in $(seq 1 30); do + phase=$(kubectl get ipclaim -n "$NAMESPACE" selector-claim \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$phase" = "Bound" ]; then break; fi + sleep 1 + done + if [ "$phase" != "Bound" ]; then + echo "FAIL: selector-claim not Bound after 30s (phase=$phase)" + exit 1 + fi + check: + ($error == null): true + - assert: + file: assertions/assert-bound-to-us-east.yaml + - script: + timeout: 30s + env: + - name: NAMESPACE + value: ($namespace) + content: | + set -e + ref=$(kubectl get ipclaim -n "$NAMESPACE" selector-claim -o jsonpath='{.status.boundAllocationRef.name}') + if [ -z "$ref" ]; then + echo "FAIL: empty boundAllocationRef.name" + exit 1 + fi + pool=$(kubectl get ipallocation -n "$NAMESPACE" "$ref" -o jsonpath='{.spec.poolRef.name}' 2>/dev/null || echo "") + if [ "$pool" != "selector-pool-consumer-b" ]; then + echo "FAIL: IPAllocation $ref poolRef=$pool (expected selector-pool-consumer-b)" + exit 1 + fi + echo "OK IPAllocation $ref bound to selector-pool-consumer-b" + check: + ($error == null): true + (contains($stdout, 'OK IPAllocation ')): true + + - name: claim-no-match-rejected + description: Selector matching no pool returns HTTP 400 + try: + - create: + file: test-data/claim-no-match.yaml + expect: + - check: + ($error != null): true + (contains($error, 'no IPPool matches') || contains($error, 'no pool matches') || contains($error, 'no IPPrefix pool matches')): true + + - name: mutually-exclusive-rejected + description: Setting both poolRef and poolSelector returns HTTP 400 + try: + - create: + file: test-data/claim-both.yaml + expect: + - check: + ($error != null): true + (contains($error, 'mutually exclusive') || contains($error, '400')): true + + finally: + - script: + env: + - name: NAMESPACE + value: ($namespace) + content: | + kubectl delete ipclaim -n "$NAMESPACE" selector-claim selector-claim-no-match selector-claim-both --ignore-not-found + kubectl delete ippool selector-pool-consumer-a selector-pool-consumer-b selector-pool-infra --ignore-not-found + echo "pool-selector cleanup done" + check: + ($error == null): true diff --git a/test/e2e/prefix-selector/test-data/claim-both.yaml b/test/e2e/pool-selector/test-data/claim-both.yaml similarity index 64% rename from test/e2e/prefix-selector/test-data/claim-both.yaml rename to test/e2e/pool-selector/test-data/claim-both.yaml index e044198..945a02b 100644 --- a/test/e2e/prefix-selector/test-data/claim-both.yaml +++ b/test/e2e/pool-selector/test-data/claim-both.yaml @@ -1,15 +1,15 @@ --- -# Negative-path: setting both prefixRef and prefixSelector must be rejected. +# Negative-path: setting both poolRef and poolSelector must be rejected. apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: selector-claim-both namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 24 - prefixRef: + poolRef: name: selector-pool-consumer-a - prefixSelector: + poolSelector: matchLabels: environment: consumer diff --git a/test/e2e/prefix-selector/test-data/claim-by-selector.yaml b/test/e2e/pool-selector/test-data/claim-by-selector.yaml similarity index 86% rename from test/e2e/prefix-selector/test-data/claim-by-selector.yaml rename to test/e2e/pool-selector/test-data/claim-by-selector.yaml index 1b7707d..7d2205a 100644 --- a/test/e2e/prefix-selector/test-data/claim-by-selector.yaml +++ b/test/e2e/pool-selector/test-data/claim-by-selector.yaml @@ -1,13 +1,13 @@ --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: selector-claim namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 24 - prefixSelector: + poolSelector: matchLabels: environment: consumer region: us-east diff --git a/test/e2e/prefix-selector/test-data/claim-no-match.yaml b/test/e2e/pool-selector/test-data/claim-no-match.yaml similarity index 89% rename from test/e2e/prefix-selector/test-data/claim-no-match.yaml rename to test/e2e/pool-selector/test-data/claim-no-match.yaml index 55802f3..02c2906 100644 --- a/test/e2e/prefix-selector/test-data/claim-no-match.yaml +++ b/test/e2e/pool-selector/test-data/claim-no-match.yaml @@ -2,13 +2,13 @@ # Negative-path claim: no pool carries environment=production, so this # claim must be rejected with HTTP 400. apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim +kind: IPClaim metadata: name: selector-claim-no-match namespace: ($namespace) spec: ipFamily: IPv4 prefixLength: 24 - prefixSelector: + poolSelector: matchLabels: environment: production diff --git a/test/e2e/prefix-selector/test-data/pools.yaml b/test/e2e/pool-selector/test-data/pools.yaml similarity index 87% rename from test/e2e/prefix-selector/test-data/pools.yaml rename to test/e2e/pool-selector/test-data/pools.yaml index bdb6f9b..397e3f7 100644 --- a/test/e2e/prefix-selector/test-data/pools.yaml +++ b/test/e2e/pool-selector/test-data/pools.yaml @@ -4,7 +4,7 @@ # pool exercises the negative path: a selector that names `consumer` must # never resolve onto it even though it has plenty of free space. apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: selector-pool-consumer-a labels: @@ -13,15 +13,14 @@ metadata: spec: cidr: 10.200.0.0/20 ipFamily: IPv4 - classRef: - name: selector-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 strategy: FirstFit --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: selector-pool-consumer-b labels: @@ -30,15 +29,14 @@ metadata: spec: cidr: 10.201.0.0/20 ipFamily: IPv4 - classRef: - name: selector-class + visibility: consumer allocation: minPrefixLength: 24 maxPrefixLength: 28 strategy: FirstFit --- apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix +kind: IPPool metadata: name: selector-pool-infra labels: @@ -47,8 +45,7 @@ metadata: spec: cidr: 10.202.0.0/20 ipFamily: IPv4 - classRef: - name: selector-class + visibility: platform allocation: minPrefixLength: 24 maxPrefixLength: 28 diff --git a/test/e2e/prefix-allocation/assertions/assert-child-prefix.yaml b/test/e2e/prefix-allocation/assertions/assert-child-prefix.yaml deleted file mode 100644 index 48c4f26..0000000 --- a/test/e2e/prefix-allocation/assertions/assert-child-prefix.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefix -metadata: - name: alloc-child-prefix -spec: - ipFamily: IPv4 - parentRef: - apiGroup: ipam.miloapis.com - kind: IPPrefix - name: alloc-parent -status: - phase: Ready diff --git a/test/e2e/prefix-allocation/assertions/assert-claim-1-releasing.yaml b/test/e2e/prefix-allocation/assertions/assert-claim-1-releasing.yaml deleted file mode 100644 index 80eb8c0..0000000 --- a/test/e2e/prefix-allocation/assertions/assert-claim-1-releasing.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim -metadata: - name: alloc-claim-1 - namespace: ($namespace) -status: - phase: Releasing diff --git a/test/e2e/prefix-allocation/chainsaw-test.yaml b/test/e2e/prefix-allocation/chainsaw-test.yaml deleted file mode 100644 index 1e5fa4c..0000000 --- a/test/e2e/prefix-allocation/chainsaw-test.yaml +++ /dev/null @@ -1,251 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: prefix-allocation -spec: - description: | - Happy-path allocation tests for IPPrefixClaim: - - Synchronous CIDR in status on Bound - - Non-overlapping concurrent allocations - - childPrefixTemplate atomic delegation - - Release on delete and re-allocation - - steps: - - name: setup-pool - description: Create IPPrefixClass + parent IPPrefix (10.128.0.0/20, allow /24-/28) - try: - - create: - file: test-data/class.yaml - - create: - file: test-data/parent-prefix.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: alloc-parent - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - - name: allocate-first-claim - description: | - Create IPPrefixClaim (prefixLength=24); assert /24 within parent and - boundPrefixRef set. The shell follow-up additionally verifies — using - Python's ipaddress module — that status.allocatedCIDR is actually a - subnet of the parent pool CIDR, catching cases where the server might - return a syntactically valid CIDR that lies outside the parent. - try: - - create: - file: test-data/claim-first.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: alloc-claim-1 - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - assert: - file: assertions/assert-claim-1-bound.yaml - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - allocated=$(kubectl get ipprefixclaim -n "$NAMESPACE" alloc-claim-1 -o jsonpath='{.status.allocatedCIDR}') - pool=$(kubectl get ipprefix alloc-parent -o jsonpath='{.spec.cidr}') - if [ -z "$allocated" ] || [ -z "$pool" ]; then - echo "FAIL: missing allocated=$allocated pool=$pool" - exit 1 - fi - python3 -c " - import ipaddress, sys - child = ipaddress.ip_network('$allocated') - parent = ipaddress.ip_network('$pool') - if not child.subnet_of(parent): - print(f'FAIL: {child} not a subnet of {parent}') - sys.exit(1) - print(f'OK {child} is a subnet of {parent}') - " - check: - ($error == null): true - (contains($stdout, 'OK ')): true - - - name: allocate-second-claim-non-overlap - description: Second IPPrefixClaim (prefixLength=24) gets a non-overlapping /24 - try: - - create: - file: test-data/claim-second.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: alloc-claim-2 - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl get ipprefixclaim -n "$NAMESPACE" alloc-claim-1 alloc-claim-2 \ - -o jsonpath='{.items[*].status.allocatedCIDR}' | tr ' ' '\n' | sort -u | awk 'END{print NR}' - check: - ($stdout): "2\n" - - - name: allocate-with-create-child-prefix - description: | - Claim with childPrefixTemplate creates child IPPrefix atomically - with parentRef. The shell follow-up additionally verifies the child - IPPrefix's spec.cidr exactly matches the parent claim's - status.allocatedCIDR — they must be the same CIDR, since the child - is a delegation of the slot the claim allocated. - try: - - create: - file: test-data/claim-with-child.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: alloc-claim-child - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: alloc-child-prefix - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - assert: - file: assertions/assert-child-prefix.yaml - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - claim_cidr=$(kubectl get ipprefixclaim -n "$NAMESPACE" alloc-claim-child -o jsonpath='{.status.allocatedCIDR}') - child_cidr=$(kubectl get ipprefix alloc-child-prefix -o jsonpath='{.spec.cidr}') - if [ -z "$claim_cidr" ] || [ -z "$child_cidr" ]; then - echo "FAIL: missing claim_cidr=$claim_cidr child_cidr=$child_cidr" - exit 1 - fi - if [ "$claim_cidr" != "$child_cidr" ]; then - echo "FAIL: child.spec.cidr ($child_cidr) != parent_claim.status.allocatedCIDR ($claim_cidr)" - exit 1 - fi - echo "OK child cidr matches parent claim allocatedCIDR=$child_cidr" - check: - ($error == null): true - (contains($stdout, 'OK child cidr matches')): true - - - name: release-first-claim - description: | - Delete the first claim and verify the full lifecycle: - 1. Snapshot parent pool's status.capacity.available BEFORE delete. - 2. Delete the claim. - 3. Briefly assert the claim observed status.phase=Releasing - (intermediate phase emitted while the allocation is being - released — short 15s timeout because the transition is fast). - 4. Confirm the claim is gone. - 5. Assert parent pool's status.capacity.available has INCREASED by - the claim's /24 worth of addresses (= 256), proving the released - CIDR is no longer counted against the pool. - try: - - script: - content: | - set -e - before=$(kubectl get ipprefix alloc-parent -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") - if [ -z "$before" ]; then - echo "FAIL: parent pool has no status.capacity.available" - exit 1 - fi - echo "$before" > /tmp/alloc-parent-available-before - echo "before_available=$before" - check: - ($error == null): true - - delete: - ref: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: alloc-claim-1 - namespace: ($namespace) - - assert: - timeout: 15s - file: assertions/assert-claim-1-releasing.yaml - - error: - file: assertions/assert-claim-1-deleted.yaml - - script: - content: | - set -e - before=$(cat /tmp/alloc-parent-available-before) - # Allow the controller a moment to update pool capacity after - # the release row is dropped from ipam_prefix_allocations. - for i in $(seq 1 30); do - after=$(kubectl get ipprefix alloc-parent -o jsonpath='{.status.capacity.available}' 2>/dev/null || echo "") - if [ -n "$after" ] && [ "$after" -gt "$before" ]; then - break - fi - sleep 0.5 - done - echo "after_available=$after (before=$before)" - if [ -z "$after" ]; then - echo "FAIL: parent pool capacity unreadable after release" - exit 1 - fi - if [ "$after" -le "$before" ]; then - echo "FAIL: capacity.available did not increase after release ($before -> $after)" - exit 1 - fi - # The released claim was a /24 (256 addresses). - expected=$(( before + 256 )) - if [ "$after" -ne "$expected" ]; then - echo "FAIL: capacity.available expected $expected after releasing /24 ($before + 256), got $after" - exit 1 - fi - echo "OK capacity available incremented from $before to $after after releasing /24" - check: - ($error == null): true - (contains($stdout, 'OK capacity available incremented')): true - - - name: reallocate-after-release - description: New claim succeeds; pool not exhausted - try: - - create: - file: test-data/claim-reallocate.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: alloc-claim-reuse - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl delete ipprefixclaim -n "$NAMESPACE" \ - alloc-claim-1 alloc-claim-2 alloc-claim-child alloc-claim-reuse --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefix alloc-child-prefix alloc-parent --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefixclass consumer-private --ignore-not-found >/dev/null 2>&1 || true - echo "prefix-allocation cleanup done" - check: - ($error == null): true diff --git a/test/e2e/prefix-allocation/test-data/claim-with-child.yaml b/test/e2e/prefix-allocation/test-data/claim-with-child.yaml deleted file mode 100644 index 706e4f1..0000000 --- a/test/e2e/prefix-allocation/test-data/claim-with-child.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim -metadata: - name: alloc-claim-child - namespace: ($namespace) -spec: - ipFamily: IPv4 - prefixLength: 24 - prefixRef: - name: alloc-parent - childPrefixTemplate: - metadata: - name: alloc-child-prefix - spec: - classRef: - name: consumer-private - allocation: - minPrefixLength: 28 - maxPrefixLength: 28 - strategy: FirstFit - reclaimPolicy: Delete diff --git a/test/e2e/prefix-exhaustion/chainsaw-test.yaml b/test/e2e/prefix-exhaustion/chainsaw-test.yaml deleted file mode 100644 index 34f9602..0000000 --- a/test/e2e/prefix-exhaustion/chainsaw-test.yaml +++ /dev/null @@ -1,104 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: prefix-exhaustion -spec: - description: | - Pool exhaustion path: - - Two IPAddressClaims fill the /31 pool (2 addresses) - - Third claim returns HTTP 507 (Insufficient Storage) - - Releasing one claim re-opens the slot - - steps: - - name: setup-tiny-pool - description: Create class + IPPrefix (192.168.0.0/31, /32 only) — 2 addresses, pool exhausted after 2 claims - try: - - create: - file: test-data/class.yaml - - create: - file: test-data/tiny-prefix.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: exhaust-pool - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - - name: fill-pool - description: Create two IPAddressClaims; both must reach Bound - try: - - create: - file: test-data/claim-1.yaml - - create: - file: test-data/claim-2.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: exhaust-claim-1 - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: exhaust-claim-2 - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - - name: third-claim-rejected-507 - description: Third claim must fail with HTTP 507 (Insufficient Storage) - try: - - create: - file: test-data/claim-3.yaml - expect: - - check: - ($error != null): true - (contains($error, '507') || contains($error, 'Insufficient Storage') || contains($error, 'pool exhausted')): true - - - name: release-and-reallocate - description: Delete first claim, then create third claim — succeeds - try: - - delete: - ref: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: exhaust-claim-1 - namespace: ($namespace) - - error: - file: assertions/assert-claim-1-deleted.yaml - - create: - file: test-data/claim-3.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPAddressClaim - name: exhaust-claim-3 - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl delete ipaddressclaim -n "$NAMESPACE" \ - exhaust-claim-1 exhaust-claim-2 exhaust-claim-3 --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefix exhaust-pool --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefixclass exhaust-class --ignore-not-found >/dev/null 2>&1 || true - echo "prefix-exhaustion cleanup done" - check: - ($error == null): true diff --git a/test/e2e/prefix-exhaustion/test-data/class.yaml b/test/e2e/prefix-exhaustion/test-data/class.yaml deleted file mode 100644 index 762ee31..0000000 --- a/test/e2e/prefix-exhaustion/test-data/class.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: exhaust-class -spec: - requiresVerification: false - visibility: consumer - defaultAllocation: - minPrefixLength: 32 - maxPrefixLength: 32 - strategy: FirstFit diff --git a/test/e2e/prefix-hierarchy/chainsaw-test.yaml b/test/e2e/prefix-hierarchy/chainsaw-test.yaml deleted file mode 100644 index c7b7a44..0000000 --- a/test/e2e/prefix-hierarchy/chainsaw-test.yaml +++ /dev/null @@ -1,174 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: prefix-hierarchy -spec: - description: | - Hierarchical delegation: environment -> region -> leaf. - - childPrefixTemplate builds nested IPPrefix tree - - Concurrent regional claims get non-overlapping blocks - - Leaf claim against child resolves within child range - - DELETE of regional IPPrefix is rejected with HTTP 409 while the - leaf still has an active allocation against it. - - NOTE: This suite verifies DELETION-PROTECTION semantics, NOT cascade - delete. An earlier draft of the spec described cascade-delete behavior - (deleting a parent prefix terminates child allocations); the actual - requirements doc - (infra/docs/enhancements/ipam/README.md) does NOT call for cascade - delete. The implemented and intentional design is deletion protection: - a parent IPPrefix with active leaf claims is rejected on DELETE with - HTTP 409 so operators must release child claims first. This avoids - orphaning child allocations and matches the spec. - - steps: - - name: create-environment-prefix - description: Top-of-tree environment IPPrefix (10.128.0.0/9, allow /12-/16) - try: - - create: - file: test-data/class.yaml - - create: - file: test-data/env-prefix.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: hier-env - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - - name: claim-region-1 - description: Claim regional block /12 with childPrefixTemplate; assert child IPPrefix exists - try: - - create: - file: test-data/region-1-claim.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: hier-region-1-claim - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: hier-region-1 - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - - name: claim-region-2-non-overlap - description: Second regional /12 must be non-overlapping with first - try: - - create: - file: test-data/region-2-claim.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: hier-region-2-claim - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl get ipprefixclaim -n "$NAMESPACE" hier-region-1-claim hier-region-2-claim \ - -o jsonpath='{.items[*].status.allocatedCIDR}' | tr ' ' '\n' | sort -u | awk 'END{print NR}' - check: - ($stdout): "2\n" - - - name: claim-leaf-against-child - description: /24 claim against the child regional IPPrefix; CIDR within regional block - try: - - create: - file: test-data/leaf-claim.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: hier-leaf-claim - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - # Verify the leaf claim's allocated CIDR is inside the regional - # block (hier-region-1.spec.cidr). The plain-grep check used - # elsewhere only confirms the address family / textual prefix; - # this uses Python's ipaddress.subnet_of() for a strict - # mathematical containment check. - leaf_cidr=$(kubectl get ipprefixclaim -n "$NAMESPACE" hier-leaf-claim -o jsonpath='{.status.allocatedCIDR}') - region_cidr=$(kubectl get ipprefix hier-region-1 -o jsonpath='{.spec.cidr}') - if [ -z "$leaf_cidr" ] || [ -z "$region_cidr" ]; then - echo "FAIL: missing CIDR (leaf=$leaf_cidr region=$region_cidr)" - exit 1 - fi - python3 -c " - import ipaddress, sys - leaf = ipaddress.ip_network('$leaf_cidr', strict=False) - region = ipaddress.ip_network('$region_cidr', strict=False) - if not leaf.subnet_of(region): - print(f'FAIL: leaf {leaf} is NOT a subnet of region {region}', file=sys.stderr) - sys.exit(1) - print(f'OK leaf {leaf} ⊂ region {region}') - " - check: - ($error == null): true - (contains($stdout, 'OK leaf')): true - - - name: deletion-protected-while-leaf-bound - description: | - Deleting the regional IPPrefix while the leaf claim still holds an - allocation against it must fail with HTTP 409 ("active allocation"). - - Rationale: Deletion protection prevents orphaned child allocations; - operators must release child claims before deleting a parent - prefix. This is intentional design (not cascade delete). - try: - - script: - content: | - out=$(kubectl delete ipprefix hier-region-1 2>&1) && status=0 || status=$? - echo "$out" - if [ "$status" -eq 0 ]; then - echo "expected delete to fail, but it succeeded" >&2 - exit 1 - fi - echo "$out" | grep -qi 'active allocation' - check: - ($error == null): true - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - # Release the leaf allocation first, then delete child prefixes. - # Without this, hier-region-1 is deletion-protected by the leaf claim. - kubectl delete ipprefixclaim -n "$NAMESPACE" hier-leaf-claim --ignore-not-found=true - kubectl delete ipprefix hier-region-1 --ignore-not-found=true - kubectl delete ipprefix hier-region-2 --ignore-not-found=true - # Cluster-scoped top-of-tree prefix and class created in - # create-environment-prefix are not cleaned up anywhere else. - kubectl delete ipprefix hier-env --ignore-not-found=true - kubectl delete ipprefixclass platform-shared --ignore-not-found=true - echo "hierarchy child prefix cleanup done" - check: - ($error == null): true diff --git a/test/e2e/prefix-hierarchy/test-data/class.yaml b/test/e2e/prefix-hierarchy/test-data/class.yaml deleted file mode 100644 index 7f8bc4a..0000000 --- a/test/e2e/prefix-hierarchy/test-data/class.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: platform-shared -spec: - requiresVerification: false - visibility: platform - defaultAllocation: - minPrefixLength: 12 - maxPrefixLength: 28 - strategy: FirstFit diff --git a/test/e2e/prefix-hierarchy/test-data/region-1-claim.yaml b/test/e2e/prefix-hierarchy/test-data/region-1-claim.yaml deleted file mode 100644 index 63847e1..0000000 --- a/test/e2e/prefix-hierarchy/test-data/region-1-claim.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim -metadata: - name: hier-region-1-claim - namespace: ($namespace) -spec: - ipFamily: IPv4 - prefixLength: 12 - prefixRef: - name: hier-env - childPrefixTemplate: - metadata: - name: hier-region-1 - spec: - classRef: - name: platform-shared - allocation: - minPrefixLength: 16 - maxPrefixLength: 28 - strategy: FirstFit - reclaimPolicy: Delete diff --git a/test/e2e/prefix-hierarchy/test-data/region-2-claim.yaml b/test/e2e/prefix-hierarchy/test-data/region-2-claim.yaml deleted file mode 100644 index 07f07bf..0000000 --- a/test/e2e/prefix-hierarchy/test-data/region-2-claim.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClaim -metadata: - name: hier-region-2-claim - namespace: ($namespace) -spec: - ipFamily: IPv4 - prefixLength: 12 - prefixRef: - name: hier-env - childPrefixTemplate: - metadata: - name: hier-region-2 - spec: - classRef: - name: platform-shared - allocation: - minPrefixLength: 16 - maxPrefixLength: 28 - strategy: FirstFit - reclaimPolicy: Delete diff --git a/test/e2e/prefix-overlap/chainsaw-test.yaml b/test/e2e/prefix-overlap/chainsaw-test.yaml deleted file mode 100644 index e414b4b..0000000 --- a/test/e2e/prefix-overlap/chainsaw-test.yaml +++ /dev/null @@ -1,95 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: prefix-overlap -spec: - description: | - Uniqueness test for IPPrefixClaim allocation: - - 10 claims applied in a single apply block against the same parent - - All must succeed with unique, non-overlapping /24 CIDRs - - NOTE: This suite validates UNIQUENESS of allocated CIDRs across a batch of - claims posted via a single Chainsaw `create:` step. Chainsaw applies the - manifests sequentially within that step, so this is not a true concurrency - stress test of the `SELECT ... FOR UPDATE` lock — it confirms the - allocator returns distinct, non-overlapping blocks across back-to-back - requests. True concurrent contention (many parallel CREATEs hitting the - same parent row) is covered by `test/load/concurrent-claims.js`, which - drives N parallel virtual users against the API server. - - steps: - - name: setup-pool - description: Create class + IPPrefix (10.64.0.0/16, /24 only — 256 possible /24s) - try: - - create: - file: test-data/class.yaml - - create: - file: test-data/parent.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: overlap-parent - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - - name: apply-10-claims-simultaneously - description: Create 10 claims in a single apply block; all must reach Bound - try: - - create: - file: test-data/claims-10.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - namespace: ($namespace) - selector: overlap-test=true - timeout: 60s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - - name: assert-unique-non-overlapping - description: All 10 allocatedCIDR values must be unique - try: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl get ipprefixclaim -n "$NAMESPACE" -l overlap-test=true \ - -o jsonpath='{.items[*].status.allocatedCIDR}' | tr ' ' '\n' | sort -u | awk 'END{print NR}' - check: - ($stdout): "10\n" - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - set -e - cidrs=$(kubectl get ipprefixclaim -n "$NAMESPACE" -l overlap-test=true \ - -o jsonpath='{.items[*].status.allocatedCIDR}') - for c in $cidrs; do - if ! echo "$c" | grep -qE '^10\.64\.[0-9]+\.0/24$'; then - echo "FAIL: CIDR $c is not a /24 in 10.64.0.0/16" - exit 1 - fi - done - echo "OK: all 10 CIDRs are /24 within 10.64.0.0/16" - check: - ($stdout): "OK: all 10 CIDRs are /24 within 10.64.0.0/16\n" - - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl delete ipprefixclaim -n "$NAMESPACE" -l overlap-test=true --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefix overlap-parent --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefixclass overlap-class --ignore-not-found >/dev/null 2>&1 || true - echo "prefix-overlap cleanup done" - check: - ($error == null): true diff --git a/test/e2e/prefix-overlap/test-data/class.yaml b/test/e2e/prefix-overlap/test-data/class.yaml deleted file mode 100644 index 00f886a..0000000 --- a/test/e2e/prefix-overlap/test-data/class.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: overlap-class -spec: - requiresVerification: false - visibility: consumer - defaultAllocation: - minPrefixLength: 24 - maxPrefixLength: 24 - strategy: FirstFit diff --git a/test/e2e/prefix-selector/chainsaw-test.yaml b/test/e2e/prefix-selector/chainsaw-test.yaml deleted file mode 100644 index f8b2b76..0000000 --- a/test/e2e/prefix-selector/chainsaw-test.yaml +++ /dev/null @@ -1,83 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: prefix-selector -spec: - description: | - Label-selector-based pool resolution for IPPrefixClaim: - - A claim with spec.prefixSelector matchLabels picks the unique matching - pool and binds against it (status.boundPrefixRef + allocatedCIDR fall - inside that pool's CIDR). - - A claim whose selector matches no pool returns HTTP 400 with - "no IPPrefix pool matches spec.prefixSelector". - - spec.prefixRef and spec.prefixSelector are mutually exclusive at create - time. - - steps: - - name: setup - description: IPPrefixClass + three labelled pools (two consumer, one infra) - try: - - create: - file: test-data/class.yaml - - create: - file: test-data/pools.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: selector-pool-consumer-b - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - - name: claim-by-selector - description: matchLabels {environment=consumer, region=us-east} → binds to selector-pool-consumer-b - try: - - create: - file: test-data/claim-by-selector.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefixClaim - name: selector-claim - namespace: ($namespace) - timeout: 30s - for: - jsonPath: - path: '{.status.phase}' - value: 'Bound' - - assert: - file: assertions/assert-bound-to-us-east.yaml - - - name: claim-no-match-rejected - description: Selector matching no pool returns HTTP 400 - try: - - create: - file: test-data/claim-no-match.yaml - expect: - - check: - ($error != null): true - (contains($error, 'no IPPrefix pool matches')): true - - - name: mutually-exclusive-rejected - description: Setting both prefixRef and prefixSelector returns HTTP 400 - try: - - create: - file: test-data/claim-both.yaml - expect: - - check: - ($error != null): true - (contains($error, 'mutually exclusive') || contains($error, '400')): true - - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - kubectl delete ipprefixclaim -n "$NAMESPACE" selector-claim selector-claim-no-match selector-claim-both --ignore-not-found - kubectl delete ipprefix selector-pool-consumer-a selector-pool-consumer-b selector-pool-infra --ignore-not-found - kubectl delete ipprefixclass selector-class --ignore-not-found - echo "selector suite cleanup done" - check: - ($error == null): true diff --git a/test/e2e/prefix-selector/test-data/class.yaml b/test/e2e/prefix-selector/test-data/class.yaml deleted file mode 100644 index ccb944c..0000000 --- a/test/e2e/prefix-selector/test-data/class.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: selector-class -spec: - requiresVerification: false - visibility: consumer - defaultAllocation: - minPrefixLength: 24 - maxPrefixLength: 28 - strategy: FirstFit diff --git a/test/e2e/prefix-validation/chainsaw-test.yaml b/test/e2e/prefix-validation/chainsaw-test.yaml deleted file mode 100644 index 61250f9..0000000 --- a/test/e2e/prefix-validation/chainsaw-test.yaml +++ /dev/null @@ -1,124 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: prefix-validation -spec: - description: | - End-to-end tests for IPPrefix and IPPrefixClaim validation: - - Required field validation (cidr) - - CIDR format validation - - prefixLength bounds (min/max from parent) - - Immutability of spec.cidr and spec.ipFamily - - Mutability of spec.allocation.strategy - - steps: - - name: create-valid-prefix - description: Create a valid IPPrefixClass + IPPrefix; assert Ready condition and canonical CIDR - try: - - create: - file: test-data/valid-class.yaml - - create: - file: test-data/valid-prefix.yaml - - wait: - apiVersion: ipam.miloapis.com/v1alpha1 - kind: IPPrefix - name: test-valid-prefix - timeout: 30s - for: - condition: - name: Ready - value: 'True' - - assert: - file: assertions/assert-valid-prefix.yaml - - - name: missing-cidr-field - description: IPPrefix missing spec.cidr is rejected at admission - try: - - create: - file: test-data/missing-cidr.yaml - expect: - - check: - ($error != null): true - (contains($error, 'cidr')): true - - - name: invalid-cidr-format - description: IPPrefix with malformed CIDR string is rejected - try: - - create: - file: test-data/invalid-cidr.yaml - expect: - - check: - ($error != null): true - (contains($error, 'invalid CIDR')): true - - - name: claim-prefix-length-out-of-bounds - description: | - IPPrefixClaim asks for prefixLength=16 against a /20 parent. The - allocator's FindFirstAvailableBlock skips parents where - prefixLen < parent_ones, so no candidate fits and the request is - rejected with HTTP 507 "prefix pool exhausted". Match the actual - server error string so this assertion fails loudly if the message - ever changes (rather than silently passing on any error). - try: - - create: - file: test-data/claim-out-of-bounds.yaml - expect: - - check: - ($error != null): true - (contains($error, '507') || contains($error, 'Insufficient Storage') || contains($error, 'pool exhausted')): true - - - name: claim-prefix-length-zero - description: IPPrefixClaim with prefixLength=0 is rejected - try: - - create: - file: test-data/claim-zero-length.yaml - expect: - - check: - ($error != null): true - (contains($error, 'prefixLength')): true - - - name: immutable-cidr - description: Patching IPPrefix.spec.cidr is rejected (immutable) - try: - - patch: - file: test-data/patch-cidr.yaml - expect: - - check: - ($error != null): true - (contains($error, 'spec.cidr is immutable')): true - - - name: immutable-ip-family - description: Patching IPPrefix.spec.ipFamily is rejected (immutable) - try: - - patch: - file: test-data/patch-ip-family.yaml - expect: - - check: - ($error != null): true - (contains($error, 'spec.ipFamily is immutable')): true - - - name: update-mutable-strategy - description: Patching spec.allocation.strategy succeeds; assert updated value - try: - - patch: - file: test-data/patch-strategy.yaml - - assert: - file: assertions/assert-updated-strategy.yaml - - finally: - - script: - env: - - name: NAMESPACE - value: ($namespace) - content: | - # Negative-path test-data targets (claims/prefixes that should - # have been rejected) are also cleaned up best-effort in case - # the server unexpectedly accepted them. - kubectl delete ipprefixclaim -n "$NAMESPACE" \ - claim-out-of-bounds claim-zero-length --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefix \ - test-valid-prefix test-missing-cidr test-invalid-cidr --ignore-not-found >/dev/null 2>&1 || true - kubectl delete ipprefixclass validation-class --ignore-not-found >/dev/null 2>&1 || true - echo "prefix-validation cleanup done" - check: - ($error == null): true diff --git a/test/e2e/prefix-validation/test-data/valid-class.yaml b/test/e2e/prefix-validation/test-data/valid-class.yaml deleted file mode 100644 index 40b9f18..0000000 --- a/test/e2e/prefix-validation/test-data/valid-class.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ipam.miloapis.com/v1alpha1 -kind: IPPrefixClass -metadata: - name: validation-class -spec: - requiresVerification: false - visibility: consumer - defaultAllocation: - minPrefixLength: 24 - maxPrefixLength: 28 - strategy: FirstFit diff --git a/test/load/Taskfile.yaml b/test/load/Taskfile.yaml index 20e705a..795d712 100644 --- a/test/load/Taskfile.yaml +++ b/test/load/Taskfile.yaml @@ -182,7 +182,7 @@ tasks: {{.K6_SRC_DIR}}/asn-claim-throughput.js address-concurrent: - desc: 'Stress-test IPAddressClaim concurrency + uniqueness. Vars: VUS, DURATION, POOL_CIDR' + desc: 'Stress-test host-address (IPClaim /32) concurrency + uniqueness. Vars: VUS, DURATION, POOL_CIDR' silent: true cmds: - | @@ -190,11 +190,11 @@ tasks: k6 run \ -e IPAM_API_URL={{.IPAM_API_URL}} \ -e NAMESPACE_COUNT={{.NAMESPACE_COUNT | default "10"}} \ - -e VUS={{.VUS | default "50"}} \ + -e VUS={{.VUS | default "10"}} \ -e DURATION={{.DURATION | default "2m"}} \ - -e POOL_CIDR={{.POOL_CIDR | default "10.250.0.0/22"}} \ + -e POOL_CIDR={{.POOL_CIDR | default "10.60.0.0/24"}} \ --summary-export={{.RESULTS_DIR}}/address-concurrent.json \ - {{.K6_SRC_DIR}}/ipaddress-claim-concurrent.js + {{.K6_SRC_DIR}}/host-prefix-claim-concurrent.js exhaustion: desc: 'Measure deny-path latency under pool exhaustion' @@ -235,7 +235,7 @@ tasks: {{.K6_SRC_DIR}}/mixed-load.js concurrent: - desc: 'Concurrent burst + uniqueness IPPrefixClaim test. Vars: VUS, DURATION' + desc: 'Concurrent burst + uniqueness IPClaim test. Vars: VUS, DURATION' silent: true cmds: - | @@ -250,7 +250,7 @@ tasks: {{.K6_SRC_DIR}}/concurrent-claims.js cross-project: - desc: 'Cross-project IPPrefixClaim throughput. Vars: VUS, DURATION' + desc: 'Cross-project IPClaim throughput. Vars: VUS, DURATION' silent: true cmds: - | @@ -316,17 +316,23 @@ tasks: PROJECT_COUNT={{.PROJECT_COUNT}} LAST=$((PROJECT_COUNT - 1)) + echo "Deleting IPClaims and IPAllocations in every perf namespace..." + for ns in $(KUBECONFIG={{.KUBECONFIG}} kubectl get ns -o name | grep '^namespace/ipam-perf-' | sed 's|^namespace/||'); do + KUBECONFIG={{.KUBECONFIG}} kubectl delete ipclaim.ipam.miloapis.com --all -n ${ns} --ignore-not-found --wait=false || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ipallocation.ipam.miloapis.com --all -n ${ns} --ignore-not-found --wait=false || true + done + echo "Deleting perf namespaces..." KUBECONFIG={{.KUBECONFIG}} kubectl get ns -o name | grep '^namespace/ipam-perf-' | xargs -r KUBECONFIG={{.KUBECONFIG}} kubectl delete --wait=false || true - echo "Deleting per-project IPv4 prefixes (perf-prefix-0..${LAST})..." + echo "Deleting per-project IPv4 IPPools (perf-prefix-0..${LAST})..." for n in $(seq 0 ${LAST}); do - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefix.ipam.miloapis.com perf-prefix-${n} --ignore-not-found || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ippool.ipam.miloapis.com perf-prefix-${n} --ignore-not-found || true done - echo "Deleting per-project IPv6 prefixes (perf-ipv6-prefix-0..${LAST})..." + echo "Deleting per-project IPv6 IPPools (perf-ipv6-prefix-0..${LAST})..." for n in $(seq 0 ${LAST}); do - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefix.ipam.miloapis.com perf-ipv6-prefix-${n} --ignore-not-found || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ippool.ipam.miloapis.com perf-ipv6-prefix-${n} --ignore-not-found || true done echo "Deleting per-project ASN pools (perf-asn-pool-0..${LAST})..." @@ -335,39 +341,34 @@ tasks: done echo "Deleting platform-level perf pool..." - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefix.ipam.miloapis.com perf-prefix --ignore-not-found || true - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefixclass.ipam.miloapis.com perf-private --ignore-not-found || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ippool.ipam.miloapis.com perf-prefix --ignore-not-found || true KUBECONFIG={{.KUBECONFIG}} kubectl delete asnpool.ipam.miloapis.com perf-asn-pool --ignore-not-found || true KUBECONFIG={{.KUBECONFIG}} kubectl delete asnpoolclass.ipam.miloapis.com perf-asn --ignore-not-found || true echo "Deleting IPv4 shared cross-project pool + RBAC..." - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefix.ipam.miloapis.com perf-shared-prefix --ignore-not-found || true - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefixclass.ipam.miloapis.com perf-shared --ignore-not-found || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ippool.ipam.miloapis.com perf-shared-prefix --ignore-not-found || true KUBECONFIG={{.KUBECONFIG}} kubectl delete clusterrole perf-shared-pool-user --ignore-not-found || true for n in $(seq 0 ${LAST}); do KUBECONFIG={{.KUBECONFIG}} kubectl delete clusterrolebinding perf-shared-pool-user-ipam-perf-${n} --ignore-not-found || true done echo "Deleting IPv6 shared cross-project pool + RBAC..." - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefix.ipam.miloapis.com perf-ipv6-shared --ignore-not-found || true - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefixclass.ipam.miloapis.com perf-ipv6-shared-class --ignore-not-found || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ippool.ipam.miloapis.com perf-ipv6-shared --ignore-not-found || true KUBECONFIG={{.KUBECONFIG}} kubectl delete clusterrole perf-ipv6-shared-pool-user --ignore-not-found || true for n in $(seq 0 ${LAST}); do KUBECONFIG={{.KUBECONFIG}} kubectl delete clusterrolebinding perf-ipv6-shared-pool-user-ipam-perf-${n} --ignore-not-found || true done echo "Deleting exhaust pool + RBAC (pool-exhaustion.js setup leaks)..." - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefix.ipam.miloapis.com perf-exhaust-pool --ignore-not-found || true - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefixclass.ipam.miloapis.com perf-exhaust-class --ignore-not-found || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ippool.ipam.miloapis.com perf-exhaust-pool --ignore-not-found || true KUBECONFIG={{.KUBECONFIG}} kubectl delete clusterrole perf-exhaust-pool-user --ignore-not-found || true for n in $(seq 0 ${LAST}); do KUBECONFIG={{.KUBECONFIG}} kubectl delete clusterrolebinding perf-exhaust-pool-user-ipam-perf-${n} --ignore-not-found || true done - echo "Deleting per-test scratch pools (asn classref + ipaddress concurrent)..." + echo "Deleting per-test scratch pools..." KUBECONFIG={{.KUBECONFIG}} kubectl delete asnpool.ipam.miloapis.com perf-asn-classref-pool --ignore-not-found || true KUBECONFIG={{.KUBECONFIG}} kubectl delete asnpoolclass.ipam.miloapis.com perf-asn-classref --ignore-not-found || true - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefix.ipam.miloapis.com perf-ipaddr-concurrent-pool --ignore-not-found || true - KUBECONFIG={{.KUBECONFIG}} kubectl delete ipprefixclass.ipam.miloapis.com perf-ipaddr-concurrent --ignore-not-found || true + KUBECONFIG={{.KUBECONFIG}} kubectl delete ippool.ipam.miloapis.com perf-host-claim-pool --ignore-not-found || true echo "Cleanup complete." diff --git a/test/load/lib/ipam-client.js b/test/load/lib/ipam-client.js index 796962f..6c13833 100644 --- a/test/load/lib/ipam-client.js +++ b/test/load/lib/ipam-client.js @@ -1,5 +1,6 @@ // Shared HTTP client for the IPAM apiserver. Provides typed helpers for the -// nine IPAM resources and standardized request configuration. +// IPAM resources (IPPool, IPClaim, IPAllocation, ASNPool, ASNClaim) and +// standardized request configuration. // // Configuration via environment variables: // IPAM_API_URL - Base URL of the apiserver (default: kubectl proxy localhost:8001) @@ -97,16 +98,23 @@ export function nsFor(n) { return `ipam-perf-${n}`; } -export function prefixClaimPath(ns, name) { +// IPClaim is namespaced. +export function ipClaimPath(ns, name) { return name - ? `/namespaces/${ns}/ipprefixclaims/${name}` - : `/namespaces/${ns}/ipprefixclaims`; + ? `/namespaces/${ns}/ipclaims/${name}` + : `/namespaces/${ns}/ipclaims`; } -export function ipAddressClaimPath(ns, name) { +// IPAllocation is namespaced (system-created allocation record). +export function ipAllocationPath(ns, name) { return name - ? `/namespaces/${ns}/ipaddressclaims/${name}` - : `/namespaces/${ns}/ipaddressclaims`; + ? `/namespaces/${ns}/ipallocations/${name}` + : `/namespaces/${ns}/ipallocations`; +} + +// IPPool is cluster-scoped. +export function ipPoolPath(name) { + return name ? `/ippools/${name}` : '/ippools'; } export function asnClaimPath(ns, name) { @@ -115,14 +123,6 @@ export function asnClaimPath(ns, name) { : `/namespaces/${ns}/asnclaims`; } -export function prefixPath(name) { - return name ? `/ipprefixes/${name}` : '/ipprefixes'; -} - -export function prefixClassPath(name) { - return name ? `/ipprefixclasses/${name}` : '/ipprefixclasses'; -} - export function asnPoolPath(name) { return name ? `/asnpools/${name}` : '/asnpools'; } @@ -131,65 +131,62 @@ export function asnPoolClassPath(name) { return name ? `/asnpoolclasses/${name}` : '/asnpoolclasses'; } -// IPAddress is namespaced (the cluster-allocated address resource — distinct -// from IPAddressClaim). -export function ipAddressPath(ns, name) { - return name - ? `/namespaces/${ns}/ipaddresses/${name}` - : `/namespaces/${ns}/ipaddresses`; -} - // --- Resource builders --- -export function ipPrefixClass(name, { visibility = 'consumer', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { +// ipPool builds an IPPool body. visibility defaults to 'consumer'. Set +// visibility='shared' to allow cross-project use, 'platform' for backbone. +export function ipPool(name, cidr, { + ipFamily = 'IPv4', + visibility = 'consumer', + minLen = 20, + maxLen = 28, + strategy = 'FirstFit', +} = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClass', - metadata: { name }, - spec: { - visibility, - defaultAllocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, - }, - }; -} - -export function ipPrefix(name, cidr, classRef, { ipFamily = 'IPv4', minLen = 20, maxLen = 28, strategy = 'FirstFit' } = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefix', + kind: 'IPPool', metadata: { name }, spec: { cidr, ipFamily, - classRef: { name: classRef }, + visibility, allocation: { minPrefixLength: minLen, maxPrefixLength: maxLen, strategy }, }, }; } -export function ipPrefixClaim(ns, name, prefixRef, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// ipClaim builds an IPClaim body. poolName is the IPPool name; the resulting +// spec.poolRef is `{ name: poolName }` (same-project). +export function ipClaim(ns, name, poolName, prefixLength, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { ipFamily, prefixLength, - prefixRef: { name: prefixRef }, + poolRef: { name: poolName }, reclaimPolicy, }, }; } -export function ipAddressClaim(ns, name, prefixRef, { ipFamily = 'IPv4', reclaimPolicy = 'Delete' } = {}) { +// crossProjectIPClaim is like ipClaim but sets spec.poolRef.projectRef to the +// pool's owning project, so the apiserver resolves the pool in that project's +// scope. +export function crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPAddressClaim', + kind: 'IPClaim', metadata: { name, namespace: ns }, spec: { - ipFamily, - prefixRef: { name: prefixRef }, - reclaimPolicy, + ipFamily: opts.ipFamily || 'IPv4', + prefixLength, + poolRef: { + name: poolName, + projectRef: { name: sourceProjectID }, + }, + reclaimPolicy: opts.reclaimPolicy || 'Delete', }, }; } @@ -221,9 +218,6 @@ export function asnClaim(ns, name, poolRef) { }; } -// asnClaimWithClassRef builds an ASNClaim driven by spec.classRef rather than -// spec.poolRef. The apiserver picks a pool that matches the class. Mutually -// exclusive with poolRef in the resource model. export function asnClaimWithClassRef(ns, name, classRefName) { return { apiVersion: `${API_GROUP}/${API_VERSION}`, @@ -235,30 +229,46 @@ export function asnClaimWithClassRef(ns, name, classRefName) { // --- Typed helper functions --- -export function createPrefixClaim(ns, name, prefixRef, prefixLength, opts) { - return ipamPost(prefixClaimPath(ns), ipPrefixClaim(ns, name, prefixRef, prefixLength, opts), 'prefix_claim_create'); +// IPPool create / read / delete. +export function createIPPool(name, cidr, opts) { + return ipamPost(ipPoolPath(), ipPool(name, cidr, opts), 'ippool_create'); +} + +export function getIPPool(name) { + return ipamGet(ipPoolPath(name), 'ippool_get'); +} + +export function listIPPools() { + return ipamList(ipPoolPath(), 'ippool_list'); +} + +export function deleteIPPool(name) { + return ipamDelete(ipPoolPath(name), 'ippool_delete'); } -export function deletePrefixClaim(ns, name) { - return ipamDelete(prefixClaimPath(ns, name), 'prefix_claim_delete'); +// IPClaim helpers. +export function createIPClaim(ns, name, poolName, prefixLength, opts) { + return ipamPost(ipClaimPath(ns), ipClaim(ns, name, poolName, prefixLength, opts), 'ipclaim_create'); } -export function getPrefixClaim(ns, name) { - return ipamGet(prefixClaimPath(ns, name), 'prefix_claim_get'); +export function deleteIPClaim(ns, name) { + return ipamDelete(ipClaimPath(ns, name), 'ipclaim_delete'); } -export function listPrefixClaims(ns) { - return ipamList(prefixClaimPath(ns), 'prefix_claim_list'); +export function getIPClaim(ns, name) { + return ipamGet(ipClaimPath(ns, name), 'ipclaim_get'); } -export function createIPAddressClaim(ns, name, prefixRef, opts) { - return ipamPost(ipAddressClaimPath(ns), ipAddressClaim(ns, name, prefixRef, opts), 'ip_addr_claim_create'); +export function listIPClaims(ns) { + return ipamList(ipClaimPath(ns), 'ipclaim_list'); } -export function deleteIPAddressClaim(ns, name) { - return ipamDelete(ipAddressClaimPath(ns, name), 'ip_addr_claim_delete'); +// IPAllocation helpers (system-created; tests only read/list). +export function listIPAllocations(ns) { + return ipamList(ipAllocationPath(ns), 'ipallocation_list'); } +// ASN helpers. export function createASNClaim(ns, name, poolRef) { return ipamPost(asnClaimPath(ns), asnClaim(ns, name, poolRef), 'asn_claim_create'); } @@ -275,26 +285,6 @@ export function listASNClaims(ns) { return ipamList(asnClaimPath(ns), 'asn_claim_list'); } -export function createPrefixClass(name, opts) { - return ipamPost(prefixClassPath(), ipPrefixClass(name, opts), 'prefix_class_create'); -} - -export function createPrefix(name, cidr, classRef, opts) { - return ipamPost(prefixPath(), ipPrefix(name, cidr, classRef, opts), 'prefix_create'); -} - -export function listPrefixes() { - return ipamList(prefixPath(), 'prefix_list'); -} - -export function getPrefix(name) { - return ipamGet(prefixPath(name), 'prefix_get'); -} - -export function deletePrefix(name) { - return ipamDelete(prefixPath(name), 'prefix_delete'); -} - export function createASNPoolClass(name, opts) { return ipamPost(asnPoolClassPath(), asnPoolClass(name, opts), 'asn_pool_class_create'); } @@ -360,73 +350,60 @@ export function projectIDFor(n) { return `ipam-perf-${n}`; } -// Cross-project prefix claim body — includes projectRef pointing at sourceProjectID. -export function crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts = {}) { - return { - apiVersion: `${API_GROUP}/${API_VERSION}`, - kind: 'IPPrefixClaim', - metadata: { name, namespace: ns }, - spec: { - ipFamily: opts.ipFamily || 'IPv4', - prefixLength, - prefixRef: { - name: poolName, - projectRef: { name: sourceProjectID }, - }, - reclaimPolicy: opts.reclaimPolicy || 'Delete', - }, - }; -} - -// createCrossProjectPrefixClaim posts a cross-project claim with tenant headers +// createCrossProjectIPClaim posts a cross-project IPClaim with tenant headers // for callerProjectID, targeting a pool owned by sourceProjectID. -export function createCrossProjectPrefixClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); - const params = withProjectTagged(callerProjectID, 'cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createCrossProjectIPClaim(ns, name, poolName, sourceProjectID, callerProjectID, prefixLength, opts = {}) { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, prefixLength, opts); + const params = withProjectTagged(callerProjectID, 'cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -export function createPrefixClaimForProject(ns, name, prefixRef, prefixLength, projectID, opts = {}) { - const body = ipPrefixClaim(ns, name, prefixRef, prefixLength, opts); - const params = withProjectTagged(projectID, 'prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +export function createIPClaimForProject(ns, name, poolName, prefixLength, projectID, opts = {}) { + const body = ipClaim(ns, name, poolName, prefixLength, opts); + const params = withProjectTagged(projectID, 'ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } -// buildPrefixClaimRequest returns an http.batch()-compatible descriptor instead -// of firing the request. Use when multiple claims must be sent concurrently from -// a single VU to test SELECT...FOR UPDATE contention. -export function buildPrefixClaimRequest(ns, name, prefixRef, prefixLength, projectID, opts = {}) { +// buildIPClaimRequest returns an http.batch()-compatible descriptor instead +// of firing the request. Use when multiple claims must be sent concurrently +// from a single VU to test SELECT...FOR UPDATE contention. +export function buildIPClaimRequest(ns, name, poolName, prefixLength, projectID, opts = {}) { return { method: 'POST', - url: `${API_BASE}${prefixClaimPath(ns)}`, - body: JSON.stringify(ipPrefixClaim(ns, name, prefixRef, prefixLength, opts)), - params: withProjectTagged(projectID, 'prefix_claim_create'), + url: `${API_BASE}${ipClaimPath(ns)}`, + body: JSON.stringify(ipClaim(ns, name, poolName, prefixLength, opts)), + params: withProjectTagged(projectID, 'ipclaim_create'), }; } -export function deletePrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_delete'); - return http.del(`${API_BASE}${prefixClaimPath(ns, name)}`, null, params); +export function deleteIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_delete'); + return http.del(`${API_BASE}${ipClaimPath(ns, name)}`, null, params); } -export function getPrefixClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_get'); - return http.get(`${API_BASE}${prefixClaimPath(ns, name)}`, params); +export function getIPClaimForProject(ns, name, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_get'); + return http.get(`${API_BASE}${ipClaimPath(ns, name)}`, params); } -export function listPrefixClaimsForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'prefix_claim_list'); - return http.get(`${API_BASE}${prefixClaimPath(ns)}`, params); +export function listIPClaimsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipclaim_list'); + return http.get(`${API_BASE}${ipClaimPath(ns)}`, params); } -export function listPrefixesForProject(projectID) { - const params = withProjectTagged(projectID, 'prefix_list'); - return http.get(`${API_BASE}${prefixPath()}`, params); +export function listIPPoolsForProject(projectID) { + const params = withProjectTagged(projectID, 'ippool_list'); + return http.get(`${API_BASE}${ipPoolPath()}`, params); } -export function getPrefixForProject(name, projectID) { - const params = withProjectTagged(projectID, 'prefix_get'); - return http.get(`${API_BASE}${prefixPath(name)}`, params); +export function getIPPoolForProject(name, projectID) { + const params = withProjectTagged(projectID, 'ippool_get'); + return http.get(`${API_BASE}${ipPoolPath(name)}`, params); +} + +export function listIPAllocationsForProject(ns, projectID) { + const params = withProjectTagged(projectID, 'ipallocation_list'); + return http.get(`${API_BASE}${ipAllocationPath(ns)}`, params); } export function createASNClaimForProject(ns, name, poolRef, projectID) { @@ -441,34 +418,15 @@ export function deleteASNClaimForProject(ns, name, projectID) { } // createASNClaimWithClassRefForProject posts an ASNClaim that references a -// class (not a pool). Used by asn-claim-throughput.js to validate that the -// classRef-driven claim path is healthy under load. +// class (not a pool). export function createASNClaimWithClassRefForProject(ns, name, classRefName, projectID) { const body = asnClaimWithClassRef(ns, name, classRefName); const params = withProjectTagged(projectID, 'asn_claim_create'); return http.post(`${API_BASE}${asnClaimPath(ns)}`, JSON.stringify(body), params); } -// IPAddressClaim helpers scoped by project tenant headers — used by the -// concurrent IPAddressClaim test. -export function createIPAddressClaimForProject(ns, name, prefixRef, projectID, opts = {}) { - const body = ipAddressClaim(ns, name, prefixRef, opts); - const params = withProjectTagged(projectID, 'ip_addr_claim_create'); - return http.post(`${API_BASE}${ipAddressClaimPath(ns)}`, JSON.stringify(body), params); -} - -export function deleteIPAddressClaimForProject(ns, name, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_claim_delete'); - return http.del(`${API_BASE}${ipAddressClaimPath(ns, name)}`, null, params); -} - // LIST helpers used by the read-latency scenarios. All accept the project // tenant headers so reads stay scoped to the requesting tenant. -export function listIPAddressesForProject(ns, projectID) { - const params = withProjectTagged(projectID, 'ip_addr_list'); - return http.get(`${API_BASE}${ipAddressPath(ns)}`, params); -} - export function listASNPoolsForProject(projectID) { const params = withProjectTagged(projectID, 'asn_pool_list'); return http.get(`${API_BASE}${asnPoolPath()}`, params); diff --git a/test/load/src/concurrent-claims.js b/test/load/src/concurrent-claims.js index fc6afe7..b4af2af 100644 --- a/test/load/src/concurrent-claims.js +++ b/test/load/src/concurrent-claims.js @@ -1,7 +1,7 @@ // concurrent-claims.js // -// Stress-tests the IPAM service's concurrency guarantee: concurrent -// IPPrefixClaim CREATE requests must always produce non-overlapping CIDRs. +// Stress-tests the IPAM service's concurrency guarantee: concurrent IPClaim +// CREATE requests must always produce non-overlapping CIDRs. // // Approach: // - burst scenario: constant-vus for DURATION. Each VU creates and deletes @@ -34,9 +34,9 @@ import http from 'k6/http'; import { Counter, Rate, Trend } from 'k6/metrics'; import { - buildPrefixClaimRequest, - createPrefixClaimForProject, - deletePrefixClaimForProject, + buildIPClaimRequest, + createIPClaimForProject, + deleteIPClaimForProject, nsFor, projectIDFor, } from '../lib/ipam-client.js'; @@ -116,7 +116,7 @@ export function burst() { const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); const claimName = `concurrent-claim-${__VU}-${__ITER}`; - const createRes = createPrefixClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); + const createRes = createIPClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); if (createRes.status === 201) { concurrentCreated.add(1); @@ -126,12 +126,12 @@ export function burst() { if (extractCIDR(createRes) === null) { missingStatus.add(1); if (__ITER < 5) { - console.error(`prefix claim ${claimName} created without status.allocatedCIDR: ${createRes.body}`); + console.error(`IPClaim ${claimName} created without status.allocatedCIDR: ${createRes.body}`); } } // Immediately delete so the pool stays available for subsequent iterations. - const delRes = deletePrefixClaimForProject(ns, claimName, PROJECT); + const delRes = deleteIPClaimForProject(ns, claimName, PROJECT); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { concurrentErrors.add(1); } @@ -167,8 +167,8 @@ export function burst() { // will produce a duplicate here and fail the ipam_duplicate_cidrs threshold. // // Phase 2 — sequential drain: fills remaining pool capacity one-by-one, -// asserting every successive CIDR is unique. Mirrors ipaddress-claim-concurrent -// and confirms correctness under non-contended conditions as well. +// asserting every successive CIDR is unique. Confirms correctness under +// non-contended conditions as well. export function uniqueness() { const ns = nsFor(0); let totalDups = 0; @@ -176,7 +176,7 @@ export function uniqueness() { // --- Phase 1: concurrent batch --- const batchRequests = []; for (let i = 0; i < VUS; i++) { - batchRequests.push(buildPrefixClaimRequest(ns, `concurrent-batch-${i}`, POOL_NAME, 28, PROJECT)); + batchRequests.push(buildIPClaimRequest(ns, `concurrent-batch-${i}`, POOL_NAME, 28, PROJECT)); } const batchResponses = http.batch(batchRequests); @@ -216,7 +216,7 @@ export function uniqueness() { // Clean up batch claims before sequential drain. for (const name of batchClaims) { - deletePrefixClaimForProject(ns, name, PROJECT); + deleteIPClaimForProject(ns, name, PROJECT); } // --- Phase 2: sequential drain --- @@ -227,7 +227,7 @@ export function uniqueness() { for (let i = 0; i < maxIters; i++) { const claimName = `concurrent-unique-${i}`; - const res = createPrefixClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); + const res = createIPClaimForProject(ns, claimName, POOL_NAME, 28, PROJECT); if (res.status === 507) break; if (res.status !== 201) { console.error(`sequential drain ${i}: status=${res.status} body=${res.body}`); @@ -258,6 +258,6 @@ export function uniqueness() { ); for (const name of seqClaims) { - deletePrefixClaimForProject(ns, name, PROJECT); + deleteIPClaimForProject(ns, name, PROJECT); } } diff --git a/test/load/src/cross-project-claim-throughput.js b/test/load/src/cross-project-claim-throughput.js index 6ea269b..c016791 100644 --- a/test/load/src/cross-project-claim-throughput.js +++ b/test/load/src/cross-project-claim-throughput.js @@ -1,11 +1,10 @@ // cross-project-claim-throughput.js // -// Dedicated cross-project IPPrefixClaim throughput test. Each VU acts as a +// Dedicated cross-project IPClaim throughput test. Each VU acts as a // non-owner project (any project N != 0) claiming a /28 from project 0's // shared pool (`perf-shared-prefix`). The claim spec carries a -// `prefixRef.projectRef` pointing at project 0, and the request itself -// carries the caller's project identity in the X-Remote-Extra parent -// headers. +// `poolRef.projectRef` pointing at project 0, and the request itself carries +// the caller's project identity in the X-Remote-Extra parent headers. // // This is the slow path that exercises whatever cross-project authorization // (SubjectAccessReview or similar) the server adds — thresholds are wider @@ -23,8 +22,8 @@ import { check } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; import { - createCrossProjectPrefixClaim, - deletePrefixClaimForProject, + createCrossProjectIPClaim, + deleteIPClaimForProject, nsFor, projectIDFor, } from '../lib/ipam-client.js'; @@ -70,7 +69,7 @@ export default function () { const callerProject = projectIDFor(callerIdx); const claimName = `xclaim-${__VU}-${__ITER}`; - const createRes = createCrossProjectPrefixClaim( + const createRes = createCrossProjectIPClaim( ns, claimName, SHARED_PREFIX, @@ -98,7 +97,7 @@ export default function () { } if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, callerProject); + const delRes = deleteIPClaimForProject(ns, claimName, callerProject); crossProjectDelete.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { crossProjectErrors.add(1); diff --git a/test/load/src/host-prefix-claim-concurrent.js b/test/load/src/host-prefix-claim-concurrent.js new file mode 100644 index 0000000..01e2fe2 --- /dev/null +++ b/test/load/src/host-prefix-claim-concurrent.js @@ -0,0 +1,221 @@ +// host-prefix-claim-concurrent.js +// +// Measures the throughput and concurrency safety of host-route allocation: +// IPClaim creates with prefixLength: 32 (IPv4 /32) against a dedicated /24 +// pool. Single-address allocation via IPClaim replaced the former +// IPAddressClaim resource. +// +// Approach: +// - setup() creates a dedicated /24 IPPool (10.60.0.0/24, 256 addresses). +// - Each VU iteration creates a /32 IPClaim and deletes it inline so the +// pool stays available for subsequent iterations. +// - All returned status.allocatedCIDR values must be unique; the +// SELECT...FOR UPDATE pool-row lock guarantees this. +// - teardown() removes all claims and the pool. +// +// Thresholds (matches prefix-claim-throughput.js): +// - p95 create latency < 500ms, p99 < 2000ms (success phase) +// - success rate > 0.95 +// - http_req_failed < 5% +// - ipam_host_missing_status == 0 (status.allocatedCIDR must be populated) +// - ipam_host_duplicate == 0 (no two claims may share a CIDR) +// +// Configuration: +// VUS - Concurrent virtual users (default 10) +// DURATION - Test duration (default 2m) +// NAMESPACE_COUNT - Namespace pool size (default 10, must match setup-pools.js) +// POOL_CIDR - Parent CIDR for the dedicated pool (default 10.60.0.0/24) +// IPAM_API_URL - Apiserver URL + +import { check } from 'k6'; +import { Counter, Rate, Trend } from 'k6/metrics'; +import { + createIPPool, + deleteIPPool, + createIPClaimForProject, + deleteIPClaimForProject, + nsFor, + projectIDFor, +} from '../lib/ipam-client.js'; + +const VUS = parseInt(__ENV.VUS || '10'); +const DURATION = __ENV.DURATION || '2m'; +const NAMESPACE_COUNT = parseInt(__ENV.NAMESPACE_COUNT || '10'); +const POOL_CIDR = __ENV.POOL_CIDR || '10.60.0.0/24'; + +const POOL_NAME = 'perf-host-claim-pool'; +const PROJECT = projectIDFor(0); + +// /24 = 256 host addresses. Each VU releases its slot inline so we stay well +// under pool capacity for the full DURATION burst. +const POOL_SIZE = 256; + +const createLatency = new Trend('ipam_host_create_latency_ms', true); +const deleteLatency = new Trend('ipam_host_delete_latency_ms', true); +const successRate = new Rate('ipam_host_success_rate'); +const created = new Counter('ipam_host_created'); +const denied = new Counter('ipam_host_denied'); +const errors = new Counter('ipam_host_errors'); +const missingStatus = new Counter('ipam_host_missing_status'); +const duplicates = new Counter('ipam_host_duplicate'); + +export const options = { + insecureSkipTLSVerify: __ENV.K6_INSECURE_SKIP_TLS_VERIFY !== 'false', + scenarios: { + concurrent_burst: { + executor: 'constant-vus', + vus: VUS, + duration: DURATION, + tags: { scenario: 'concurrent' }, + exec: 'concurrent', + }, + uniqueness_check: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + maxDuration: '5m', + // Run after the burst finishes so the pool is empty. + startTime: DURATION, + tags: { scenario: 'uniqueness' }, + exec: 'uniqueness', + }, + }, + thresholds: { + 'ipam_host_create_latency_ms{phase:success}': ['p(95)<500', 'p(99)<2000'], + 'ipam_host_success_rate': ['rate>0.95'], + 'http_req_failed': ['rate<0.05'], + // Hard guards: missing status or duplicate CIDRs fail the run. + 'ipam_host_missing_status': ['count==0'], + 'ipam_host_duplicate': ['count==0'], + }, +}; + +// setup creates the dedicated /24 IPPool. Idempotent — 409 is OK. +export function setup() { + const poolRes = createIPPool(POOL_NAME, POOL_CIDR, { + ipFamily: 'IPv4', + visibility: 'consumer', + minLen: 32, + maxLen: 32, + strategy: 'FirstFit', + }); + if (poolRes.status !== 201 && poolRes.status !== 409) { + throw new Error(`host pool create failed: ${poolRes.status} ${poolRes.body}`); + } + + console.log( + `setup complete: pool=${POOL_NAME} cidr=${POOL_CIDR} (${POOL_SIZE} host addresses)`, + ); + return { poolName: POOL_NAME }; +} + +function extractAllocatedCIDR(res) { + let body; + try { + body = JSON.parse(res.body); + } catch (_e) { + return null; + } + if (!body || !body.status) return null; + const cidr = body.status.allocatedCIDR; + if (!cidr || cidr === '') return null; + return cidr; +} + +// concurrent is the burst loop: many VUs CREATE a /32 claim then DELETE it +// inline. Each iteration releases its slot so the pool stays unsaturated. +export function concurrent() { + const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); + const claimName = `host-concurrent-${__VU}-${__ITER}`; + + const createRes = createIPClaimForProject(ns, claimName, POOL_NAME, 32, PROJECT); + + if (createRes.status === 201) { + created.add(1); + createLatency.add(createRes.timings.duration, { phase: 'success' }); + successRate.add(1); + + if (extractAllocatedCIDR(createRes) === null) { + missingStatus.add(1); + if (__ITER < 5) { + console.error( + `host claim ${claimName} created without status.allocatedCIDR: ${createRes.body}`, + ); + } + } + + const delRes = deleteIPClaimForProject(ns, claimName, PROJECT); + deleteLatency.add(delRes.timings.duration); + if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { + errors.add(1); + } + } else if (createRes.status === 507) { + denied.add(1); + createLatency.add(createRes.timings.duration, { phase: 'denied' }); + successRate.add(0); + } else { + errors.add(1); + createLatency.add(createRes.timings.duration, { phase: 'error' }); + successRate.add(0); + if (__ITER < 5) { + console.error(`VU ${__VU} iter ${__ITER}: unexpected ${createRes.status}: ${createRes.body}`); + } + } +} + +// uniqueness drains the pool sequentially from a single VU, recording every +// status.allocatedCIDR and reporting any duplicates. Cleans up after itself. +export function uniqueness() { + const ns = nsFor(0); + const seen = {}; + const claims = []; + let dupCount = 0; + + for (let i = 0; i < POOL_SIZE + 16; i++) { + const claimName = `host-unique-${i}`; + const res = createIPClaimForProject(ns, claimName, POOL_NAME, 32, PROJECT); + if (res.status === 507) break; + if (res.status !== 201) { + console.error(`uniqueness create ${i}: status=${res.status} body=${res.body}`); + continue; + } + const cidr = extractAllocatedCIDR(res); + if (cidr === null) { + missingStatus.add(1); + continue; + } + if (seen[cidr]) { + dupCount++; + console.error(`DUPLICATE CIDR ${cidr} returned for both ${seen[cidr]} and ${claimName}`); + } else { + seen[cidr] = claimName; + } + claims.push(claimName); + } + + if (dupCount > 0) { + duplicates.add(dupCount); + } + console.log( + `uniqueness scenario: ${claims.length} claims, ${Object.keys(seen).length} unique /32 CIDRs, ${dupCount} duplicates`, + ); + + // Release all slots so teardown can delete the pool cleanly. + for (const name of claims) { + deleteIPClaimForProject(ns, name, PROJECT); + } +} + +// teardown removes the pool. The burst scenario frees its claims inline; the +// uniqueness scenario drains its own. A leftover claim will block the pool +// delete and surface the leak in the logs. +export function teardown(data) { + if (!data) return; + const poolRes = deleteIPPool(data.poolName); + if (poolRes.status !== 200 && poolRes.status !== 202 && poolRes.status !== 404) { + console.error( + `teardown: pool delete ${data.poolName} status=${poolRes.status} body=${poolRes.body}`, + ); + } + console.log('host-prefix-claim-concurrent teardown complete'); +} diff --git a/test/load/src/ipaddress-claim-concurrent.js b/test/load/src/ipaddress-claim-concurrent.js deleted file mode 100644 index 894c4d2..0000000 --- a/test/load/src/ipaddress-claim-concurrent.js +++ /dev/null @@ -1,240 +0,0 @@ -// ipaddress-claim-concurrent.js -// -// Stress-tests IPAddressClaim concurrency (audit Task #11 gap-fill: parallel -// to concurrent-claims.js, but exercises the IPAddressClaim path which had -// no dedicated concurrency coverage). -// -// Concurrent IPAddressClaim CREATEs from many VUs against a single pool must -// always produce non-overlapping addresses. The SELECT...FOR UPDATE pool-row -// lock guarantees this regardless of parallelism. -// -// Approach: -// - setup() creates a dedicated pool (default /22 = 1024 addresses). -// - Each VU iteration creates an IPAddressClaim, captures status.allocatedIP, -// then immediately deletes it so the pool stays under capacity. -// - A separate uniqueness scenario fills the pool sequentially and asserts -// every status.allocatedIP is unique. -// -// Thresholds (audit spec): -// - p95 create latency < 500ms, p99 < 2000ms (success phase) -// - success rate > 0.95 -// - http_req_failed < 5% -// - ipam_ipaddr_duplicate == 0 (uniqueness assertion) -// - ipam_ipaddr_missing_status == 0 (status.allocatedIP must be populated) -// -// Configuration: -// VUS - Concurrent virtual users (default 50) -// DURATION - Test duration (default 2m) -// NAMESPACE_COUNT - Namespace pool size (default 10, must match setup-pools.js) -// POOL_CIDR - Parent CIDR for the dedicated pool (default 10.250.0.0/22) -// IPAM_API_URL - Apiserver URL - -import { check } from 'k6'; -import { Counter, Rate, Trend } from 'k6/metrics'; -import { - createPrefixClass, - createPrefix, - createIPAddressClaimForProject, - deleteIPAddressClaimForProject, - ipamDelete, - prefixPath, - prefixClassPath, - nsFor, - projectIDFor, -} from '../lib/ipam-client.js'; - -const VUS = parseInt(__ENV.VUS || '50'); -const DURATION = __ENV.DURATION || '2m'; -const NAMESPACE_COUNT = parseInt(__ENV.NAMESPACE_COUNT || '10'); -const POOL_CIDR = __ENV.POOL_CIDR || '10.250.0.0/22'; - -const CLASS_NAME = 'perf-ipaddr-concurrent'; -const POOL_NAME = 'perf-ipaddr-concurrent-pool'; -const PROJECT = projectIDFor(0); - -// /22 = 1024 addresses. Bounded, but well above the per-VU iteration count -// expected in a 2m run at VUS=50 since each iteration releases its slot. -const POOL_SIZE = 1024; - -const createLatency = new Trend('ipam_ipaddr_create_latency_ms', true); -const deleteLatency = new Trend('ipam_ipaddr_delete_latency_ms', true); -const successRate = new Rate('ipam_ipaddr_success_rate'); -const created = new Counter('ipam_ipaddr_created'); -const denied = new Counter('ipam_ipaddr_denied'); -const errors = new Counter('ipam_ipaddr_errors'); -const missingStatus = new Counter('ipam_ipaddr_missing_status'); -const uniqueAllocated = new Counter('ipam_ipaddr_unique_allocated'); -const duplicates = new Counter('ipam_ipaddr_duplicate'); - -export const options = { - insecureSkipTLSVerify: __ENV.K6_INSECURE_SKIP_TLS_VERIFY !== 'false', - scenarios: { - concurrent_burst: { - executor: 'constant-vus', - vus: VUS, - duration: DURATION, - tags: { scenario: 'concurrent' }, - exec: 'concurrent', - }, - uniqueness_check: { - executor: 'shared-iterations', - vus: 1, - iterations: 1, - maxDuration: '5m', - // Run after the burst finishes so the pool is empty. - startTime: DURATION, - tags: { scenario: 'uniqueness' }, - exec: 'uniqueness', - }, - }, - thresholds: { - 'ipam_ipaddr_create_latency_ms{phase:success}': ['p(95)<500', 'p(99)<2000'], - 'ipam_ipaddr_success_rate': ['rate>0.95'], - 'http_req_failed': ['rate<0.05'], - // Hard guards from the audit spec. - 'ipam_ipaddr_missing_status': ['count==0'], - 'ipam_ipaddr_duplicate': ['count==0'], - }, -}; - -// setup creates the dedicated class + pool used by both scenarios. Idempotent -// — if the resources already exist (409), we proceed. -export function setup() { - // Class with single allocation length (effectively /32 for IPAddressClaim, - // but the IPPrefixClass.defaultAllocation must permit /32 carve-outs). - const classRes = createPrefixClass(CLASS_NAME, { - requiresVerification: false, - visibility: 'consumer', - minLen: 22, - maxLen: 32, - strategy: 'FirstFit', - }); - if (classRes.status !== 201 && classRes.status !== 409) { - throw new Error(`prefix class create failed: ${classRes.status} ${classRes.body}`); - } - - const poolRes = createPrefix(POOL_NAME, POOL_CIDR, CLASS_NAME, { - ipFamily: 'IPv4', - minLen: 22, - maxLen: 32, - strategy: 'FirstFit', - }); - if (poolRes.status !== 201 && poolRes.status !== 409) { - throw new Error(`pool create failed: ${poolRes.status} ${poolRes.body}`); - } - - console.log(`setup complete: class=${CLASS_NAME} pool=${POOL_NAME} cidr=${POOL_CIDR} (~${POOL_SIZE} addresses)`); - return { className: CLASS_NAME, poolName: POOL_NAME }; -} - -function extractIP(res) { - let body; - try { - body = JSON.parse(res.body); - } catch (_e) { - return null; - } - if (!body || !body.status) return null; - const ip = body.status.allocatedIP; - if (!ip || ip === '') return null; - return ip; -} - -// concurrent is the burst loop: many VUs CREATE + DELETE in parallel. Each -// iteration releases its slot inline so the pool stays unsaturated. -export function concurrent() { - const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - const claimName = `ipaddr-concurrent-${__VU}-${__ITER}`; - - const createRes = createIPAddressClaimForProject(ns, claimName, POOL_NAME, PROJECT); - - if (createRes.status === 201) { - created.add(1); - createLatency.add(createRes.timings.duration, { phase: 'success' }); - successRate.add(1); - - if (extractIP(createRes) === null) { - missingStatus.add(1); - if (__ITER < 5) { - console.error(`ipaddr claim ${claimName} created without status.allocatedIP: ${createRes.body}`); - } - } - - const delRes = deleteIPAddressClaimForProject(ns, claimName, PROJECT); - deleteLatency.add(delRes.timings.duration); - if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { - errors.add(1); - } - } else if (createRes.status === 507) { - denied.add(1); - createLatency.add(createRes.timings.duration, { phase: 'denied' }); - successRate.add(0); - } else { - errors.add(1); - createLatency.add(createRes.timings.duration, { phase: 'error' }); - successRate.add(0); - if (__ITER < 5) { - console.error(`VU ${__VU} iter ${__ITER}: unexpected ${createRes.status}: ${createRes.body}`); - } - } -} - -// uniqueness drains the pool sequentially with a single VU. Records every -// allocated IP and reports duplicates. Cleans up after itself. -export function uniqueness() { - const ns = nsFor(0); - const seen = {}; - const claims = []; - let dupCount = 0; - - for (let i = 0; i < POOL_SIZE + 16; i++) { - const claimName = `ipaddr-unique-${i}`; - const res = createIPAddressClaimForProject(ns, claimName, POOL_NAME, PROJECT); - if (res.status === 507) break; - if (res.status !== 201) { - console.error(`uniqueness create ${i}: status=${res.status} body=${res.body}`); - continue; - } - const ip = extractIP(res); - if (ip === null) { - missingStatus.add(1); - continue; - } - if (seen[ip]) { - dupCount++; - console.error(`DUPLICATE ip ${ip} returned for both ${seen[ip]} and ${claimName}`); - } else { - seen[ip] = claimName; - uniqueAllocated.add(1); - } - claims.push(claimName); - } - - if (dupCount > 0) { - duplicates.add(dupCount); - } - console.log( - `uniqueness scenario: ${claims.length} claims, ${Object.keys(seen).length} unique IPs, ${dupCount} duplicates`, - ); - - // Drain so the pool delete in teardown succeeds. - for (const name of claims) { - deleteIPAddressClaimForProject(ns, name, PROJECT); - } -} - -// teardown removes the pool and class. The throughput claims free themselves -// inline; the uniqueness scenario drains its own. A leftover claim will block -// the pool delete and surface the leak in the logs. -export function teardown(data) { - if (!data) return; - const poolRes = ipamDelete(prefixPath(data.poolName), 'prefix_delete'); - if (poolRes.status !== 200 && poolRes.status !== 202 && poolRes.status !== 404) { - console.error(`teardown: pool delete ${data.poolName} status=${poolRes.status} body=${poolRes.body}`); - } - const classRes = ipamDelete(prefixClassPath(data.className), 'prefix_class_delete'); - if (classRes.status !== 200 && classRes.status !== 202 && classRes.status !== 404) { - console.error(`teardown: class delete ${data.className} status=${classRes.status} body=${classRes.body}`); - } - console.log('ipaddress-claim-concurrent teardown complete'); -} diff --git a/test/load/src/ipv6-claim-throughput.js b/test/load/src/ipv6-claim-throughput.js index abe2e2e..b82f19e 100644 --- a/test/load/src/ipv6-claim-throughput.js +++ b/test/load/src/ipv6-claim-throughput.js @@ -1,10 +1,10 @@ // ipv6-claim-throughput.js // -// PRIMARY PRIORITY load test for the IPAM platform: IPv6 prefix-claim -// throughput. The platform allocates primarily IPv6 — this script is the -// canonical proof that the hot path holds the same SLO under IPv6 as under -// IPv4, with the additional correctness gate that no two simultaneous -// allocations may overlap. +// PRIMARY PRIORITY load test for the IPAM platform: IPv6 IPClaim throughput. +// The platform allocates primarily IPv6 — this script is the canonical proof +// that the hot path holds the same SLO under IPv6 as under IPv4, with the +// additional correctness gate that no two simultaneous allocations may +// overlap. // // Topology (provisioned by setup-pools.js): // - Per-project IPv6 /32 pool `perf-ipv6-prefix-` (fd:::/32) @@ -56,10 +56,10 @@ import { check } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; import { API_BASE, - ipPrefixClaim, - prefixClaimPath, - crossProjectPrefixClaim, - deletePrefixClaimForProject, + ipClaim, + ipClaimPath, + crossProjectIPClaim, + deleteIPClaimForProject, nsFor, projectIDFor, withProjectTagged, @@ -99,7 +99,7 @@ export const options = { }, }, thresholds: { - // SLO: same envelope as the IPv4 prefix-claim path. + // SLO: same envelope as the IPv4 IPClaim path. 'ipam_ipv6_claim_create_latency_ms{phase:success}': ['p(95)<500', 'p(99)<2000'], 'ipam_ipv6_claim_success_rate': ['rate>0.95'], 'http_req_failed': ['rate<0.05'], @@ -180,11 +180,6 @@ function containsCIDR(parent, child) { return maskAddr(child.addr, parent.prefixLen) === maskAddr(parent.addr, parent.prefixLen); } -// Two CIDRs collide iff one contains the other. -function cidrsOverlap(a, b) { - return containsCIDR(a, b) || containsCIDR(b, a); -} - // Per-pool reference for containment checks. Parsed once at module load. const POOL_CIDR = {}; POOL_CIDR[SHARED_IPV6_POOL] = parseCIDR('fd00:f000::/28'); @@ -200,9 +195,9 @@ for (let n = 0; n < PROJECT_COUNT; n++) { // ---- Duplicate-CIDR detection ---- // // k6 VUs each run in their own goja runtime, so we cannot share a single -// JS Set across VUs. We rely on the server's invariant: an IPPrefixClaim -// CREATE must never return an overlapping CIDR. For an in-script signal we -// keep a per-VU registry; a duplicate within ONE VU would also be a bug. +// JS Set across VUs. We rely on the server's invariant: an IPClaim CREATE +// must never return an overlapping CIDR. For an in-script signal we keep a +// per-VU registry; a duplicate within ONE VU would also be a bug. // Cross-VU duplicates are detectable via the e2e suite and the count of // 201s vs distinct CIDRs in the json-out, both of which are tracked. const seenCIDRs = new Set(); @@ -282,18 +277,18 @@ function recordCreate(res, mode, poolName) { // Direct HTTP wrapper — the lib helpers default to IPv4, so we post our own // IPv6 body with the project tenant header in a single round-trip. -function postIPv6Claim(ns, name, prefixRef, projectID) { - const body = ipPrefixClaim(ns, name, prefixRef, CLAIM_PREFIX_LENGTH, { ipFamily: 'IPv6' }); - const params = withProjectTagged(projectID, 'ipv6_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); +function postIPv6Claim(ns, name, poolName, projectID) { + const body = ipClaim(ns, name, poolName, CLAIM_PREFIX_LENGTH, { ipFamily: 'IPv6' }); + const params = withProjectTagged(projectID, 'ipv6_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } function postCrossProjectIPv6Claim(ns, name, poolName, sourceProjectID, callerProjectID) { - const body = crossProjectPrefixClaim(ns, name, poolName, sourceProjectID, CLAIM_PREFIX_LENGTH, { + const body = crossProjectIPClaim(ns, name, poolName, sourceProjectID, CLAIM_PREFIX_LENGTH, { ipFamily: 'IPv6', }); - const params = withProjectTagged(callerProjectID, 'ipv6_cross_project_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(ns)}`, JSON.stringify(body), params); + const params = withProjectTagged(callerProjectID, 'ipv6_cross_project_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(ns)}`, JSON.stringify(body), params); } export default function () { @@ -323,7 +318,7 @@ export default function () { const ok = recordCreate(res, mode, poolName); if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, callerProject); + const delRes = deleteIPClaimForProject(ns, claimName, callerProject); claimDeleteLatency.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { claimErrors.add(1, { mode, phase: 'delete' }); diff --git a/test/load/src/mixed-load.js b/test/load/src/mixed-load.js index 2e4b1ee..4de2edd 100644 --- a/test/load/src/mixed-load.js +++ b/test/load/src/mixed-load.js @@ -20,11 +20,11 @@ import { check } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; import { - createPrefixClaimForProject, - deletePrefixClaimForProject, - listPrefixesForProject, - listPrefixClaimsForProject, - getPrefixForProject, + createIPClaimForProject, + deleteIPClaimForProject, + listIPPoolsForProject, + listIPClaimsForProject, + getIPPoolForProject, nsFor, projectIDFor, } from '../lib/ipam-client.js'; @@ -41,7 +41,7 @@ const claimsCreated = new Counter('ipam_claims_created'); const claimsDenied = new Counter('ipam_claims_denied'); const claimErrors = new Counter('ipam_claim_errors'); -const prefixListLatency = new Trend('ipam_prefix_list_ms', true); +const poolListLatency = new Trend('ipam_prefix_list_ms', true); const claimGetLatency = new Trend('ipam_claim_get_ms', true); const clusterListLatency = new Trend('ipam_cluster_list_ms', true); const readSuccessRate = new Rate('ipam_read_success_rate'); @@ -140,7 +140,7 @@ function recordCreate(res) { // --- Exported scenario functions --- -// writeScenario: create a /28 prefix claim then delete it. Used by both +// writeScenario: create a /28 IPClaim then delete it. Used by both // write_steady (baseline) and write_burst (spike) scenarios. export function writeScenario() { const projectIdx = pickProjectIdx(); @@ -149,11 +149,11 @@ export function writeScenario() { const poolName = `perf-prefix-${projectIdx}`; const claimName = `mixed-${__VU}-${__ITER}`; - const createRes = createPrefixClaimForProject(ns, claimName, poolName, 28, projectID); + const createRes = createIPClaimForProject(ns, claimName, poolName, 28, projectID); const ok = recordCreate(createRes); if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, projectID); + const delRes = deleteIPClaimForProject(ns, claimName, projectID); claimDeleteLatency.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { claimErrors.add(1); @@ -163,9 +163,9 @@ export function writeScenario() { // readScenario: randomly picks one of three read operations weighted to match // real operator traffic patterns. Used by both read_steady and read_spike. -// 60% — cluster-scoped prefix list (pool utilisation check) -// 20% — namespace-scoped prefix claim list (operator reconcile) -// 20% — single prefix GET (get allocated CIDR for a specific pool) +// 60% — cluster-scoped IPPool list (pool utilisation check) +// 20% — namespace-scoped IPClaim list (operator reconcile) +// 20% — single IPPool GET (read pool state for a specific pool) export function readScenario() { const projectIdx = pickProjectIdx(); const projectID = projectIDFor(projectIdx); @@ -173,14 +173,14 @@ export function readScenario() { let res; if (r < 0.6) { - res = listPrefixesForProject(projectID); + res = listIPPoolsForProject(projectID); clusterListLatency.add(res.timings.duration); } else if (r < 0.8) { const ns = pickNs(); - res = listPrefixClaimsForProject(ns, projectID); - prefixListLatency.add(res.timings.duration); + res = listIPClaimsForProject(ns, projectID); + poolListLatency.add(res.timings.duration); } else { - res = getPrefixForProject(`perf-prefix-${projectIdx}`, projectID); + res = getIPPoolForProject(`perf-prefix-${projectIdx}`, projectID); claimGetLatency.add(res.timings.duration); } diff --git a/test/load/src/pool-exhaustion.js b/test/load/src/pool-exhaustion.js index 9dc496b..c4b65a7 100644 --- a/test/load/src/pool-exhaustion.js +++ b/test/load/src/pool-exhaustion.js @@ -7,13 +7,13 @@ // shared pool, which is also exhausted). // // Setup phase: -// - Create perf-exhaust-class (visibility: shared, /30 only) -// - Create perf-exhaust-pool (192.168.100.0/28) owned by project 0 +// - Create perf-exhaust-pool (192.168.100.0/28, /30 only, visibility=shared) +// owned by project 0 // - Bind perf-exhaust-pool-user role to all other perf projects // - Fill the pool with 4 /30 claims (project 0 identity) // Main phase: hammer additional claim requests from both same-project and // cross-project callers. -// Teardown: delete the 4 fill claims. +// Teardown: delete the 4 fill claims, then the pool. // // Configuration: // VUS - Concurrent virtual users (default 20) @@ -24,14 +24,13 @@ import { check } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; import { - createPrefixClass, - createPrefix, - deletePrefix, + createIPPool, + deleteIPPool, createClusterRole, createClusterRoleBinding, - createPrefixClaimForProject, - deletePrefixClaimForProject, - createCrossProjectPrefixClaim, + createIPClaimForProject, + deleteIPClaimForProject, + createCrossProjectIPClaim, nsFor, projectIDFor, } from '../lib/ipam-client.js'; @@ -41,11 +40,9 @@ const PROJECT_COUNT = parseInt(__ENV.PROJECT_COUNT || '5'); const VUS = parseInt(__ENV.VUS || '20'); const DURATION = __ENV.DURATION || '1m'; const POOL_NAME = 'perf-exhaust-pool'; -const CLASS_NAME = 'perf-exhaust-class'; const EXHAUST_USER_ROLE = 'perf-exhaust-pool-user'; -// Visibility for the cross-project pool. The server accepts any string for -// Visibility (plain string field with no enum validation), so 'shared' is -// accepted today and matches the documented intent. +// Visibility for the cross-project pool. The apiserver enum is +// platform|consumer|shared; 'shared' enables cross-project claiming. const SHARED_VISIBILITY = __ENV.SHARED_VISIBILITY || 'shared'; const FILL_NAMESPACE = nsFor(0); const OWNER_PROJECT = projectIDFor(0); @@ -82,26 +79,23 @@ export const options = { }; export function setup() { - const c = createPrefixClass(CLASS_NAME, { + const p = createIPPool(POOL_NAME, '192.168.100.0/28', { + ipFamily: 'IPv4', visibility: SHARED_VISIBILITY, minLen: 30, maxLen: 30, strategy: 'FirstFit', }); - if (c.status !== 201 && c.status !== 409) { - throw new Error(`class create failed: ${c.status} ${c.body}`); - } - - const p = createPrefix(POOL_NAME, '192.168.100.0/28', CLASS_NAME, { minLen: 30, maxLen: 30 }); if (p.status !== 201 && p.status !== 409) { throw new Error(`pool create failed: ${p.status} ${p.body}`); } // ClusterRole + bindings so cross-project callers can issue use claims. + // CanUsePool targets the ippools resource. const role = createClusterRole(EXHAUST_USER_ROLE, [ { apiGroups: ['ipam.miloapis.com'], - resources: ['ipprefixes'], + resources: ['ippools'], resourceNames: [POOL_NAME], verbs: ['use'], }, @@ -125,7 +119,7 @@ export function setup() { const fillNames = []; for (let i = 0; i < 4; i++) { const name = `exhaust-fill-${i}`; - const r = createPrefixClaimForProject(FILL_NAMESPACE, name, POOL_NAME, 30, OWNER_PROJECT); + const r = createIPClaimForProject(FILL_NAMESPACE, name, POOL_NAME, 30, OWNER_PROJECT); if (r.status === 201) { fillNames.push(name); } else { @@ -147,7 +141,7 @@ function record(res, mode, ns, name, callerProject) { successes.add(1, { mode }); successLatency.add(res.timings.duration, { mode }); denyRate.add(0); - deletePrefixClaimForProject(ns, name, callerProject); + deleteIPClaimForProject(ns, name, callerProject); } else { errors.add(1, { mode }); denyRate.add(0); @@ -163,20 +157,20 @@ export default function () { // Alternate same-project (project 0) and cross-project (project 1) probes. if (__ITER % 2 === 0) { - const r = createPrefixClaimForProject(ns, name, POOL_NAME, 30, OWNER_PROJECT); + const r = createIPClaimForProject(ns, name, POOL_NAME, 30, OWNER_PROJECT); record(r, 'same', ns, name, OWNER_PROJECT); } else { const callerIdx = 1 + (__VU % Math.max(1, PROJECT_COUNT - 1)); const callerProject = projectIDFor(callerIdx); - const r = createCrossProjectPrefixClaim(ns, name, POOL_NAME, OWNER_PROJECT, callerProject, 30); + const r = createCrossProjectIPClaim(ns, name, POOL_NAME, OWNER_PROJECT, callerProject, 30); record(r, 'cross', ns, name, callerProject); } } export function teardown(data) { for (const name of data.fillNames || []) { - deletePrefixClaimForProject(FILL_NAMESPACE, name, OWNER_PROJECT); + deleteIPClaimForProject(FILL_NAMESPACE, name, OWNER_PROJECT); } - deletePrefix(POOL_NAME); + deleteIPPool(POOL_NAME); console.log('teardown complete'); } diff --git a/test/load/src/pool-scale.js b/test/load/src/pool-scale.js index 11f67d5..7c77bb9 100644 --- a/test/load/src/pool-scale.js +++ b/test/load/src/pool-scale.js @@ -5,7 +5,7 @@ // latency. Tags every metric with {depth: N} so we can compare across steps. // // All requests are scoped to project 0 (`ipam-perf-0`) and target project 0's -// per-project pool `perf-prefix-0` (10.0.0.0/16). The /16 size keeps the +// per-project IPPool `perf-prefix-0` (10.0.0.0/16). The /16 size keeps the // sweep bounded while still letting us walk /20 -> /28 densities. // // Asserts (informally, via thresholds) that p95 latency does not increase @@ -25,8 +25,8 @@ import { Counter, Trend } from 'k6/metrics'; import { - createPrefixClaimForProject, - deletePrefixClaimForProject, + createIPClaimForProject, + deleteIPClaimForProject, nsFor, projectIDFor, } from '../lib/ipam-client.js'; @@ -91,7 +91,7 @@ function fillStep(prefixLen) { const samples = latenciesByDepth[prefixLen] || (latenciesByDepth[prefixLen] = []); for (let i = 0; i < target; i++) { const name = `scale-d${prefixLen}-${i}`; - const r = createPrefixClaimForProject(FILL_NS, name, PARENT_PREFIX, prefixLen, PROJECT); + const r = createIPClaimForProject(FILL_NS, name, PARENT_PREFIX, prefixLen, PROJECT); if (r.status === 201) { created.push(name); createLatency.add(r.timings.duration, { depth: String(prefixLen) }); @@ -106,7 +106,7 @@ function fillStep(prefixLen) { // Cleanup so the next step gets fresh capacity for (const name of created) { - deletePrefixClaimForProject(FILL_NS, name, PROJECT); + deleteIPClaimForProject(FILL_NS, name, PROJECT); } return created.length; } diff --git a/test/load/src/prefix-claim-throughput.js b/test/load/src/prefix-claim-throughput.js index 854a806..5b4c818 100644 --- a/test/load/src/prefix-claim-throughput.js +++ b/test/load/src/prefix-claim-throughput.js @@ -1,11 +1,11 @@ // prefix-claim-throughput.js // -// Measures the hot path of the IPAM service: IPPrefixClaim creation throughput -// and latency under sustained load, with a multi-tenant traffic mix. +// Measures the hot path of the IPAM service: IPClaim creation throughput and +// latency under sustained load, with a multi-tenant traffic mix. // // 90% of iterations: same-project claim — VU picks a random project N, sends -// a claim against perf-prefix-N with the project N tenant -// headers (no projectRef in spec). +// an IPClaim against perf-prefix-N with the project N +// tenant headers (no projectRef in spec). // 10% of iterations: cross-project claim — VU picks a random project N != 0 // and claims from project 0's shared pool (perf-shared-prefix) // using its own project identity in headers and projectRef @@ -26,9 +26,9 @@ import { check } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; import { - createPrefixClaimForProject, - deletePrefixClaimForProject, - createCrossProjectPrefixClaim, + createIPClaimForProject, + deleteIPClaimForProject, + createCrossProjectIPClaim, nsFor, projectIDFor, } from '../lib/ipam-client.js'; @@ -105,7 +105,7 @@ export default function () { // Pick any project except project 0 (which owns the shared pool). const callerIdx = 1 + Math.floor(Math.random() * Math.max(1, PROJECT_COUNT - 1)); callerProject = projectIDFor(callerIdx); - createRes = createCrossProjectPrefixClaim( + createRes = createCrossProjectIPClaim( ns, claimName, SHARED_PREFIX, @@ -118,13 +118,13 @@ export default function () { const projectIdx = Math.floor(Math.random() * PROJECT_COUNT); callerProject = projectIDFor(projectIdx); const poolName = `perf-prefix-${projectIdx}`; - createRes = createPrefixClaimForProject(ns, claimName, poolName, 28, callerProject); + createRes = createIPClaimForProject(ns, claimName, poolName, 28, callerProject); } const ok = recordCreate(createRes, mode); if (ok) { - const delRes = deletePrefixClaimForProject(ns, claimName, callerProject); + const delRes = deleteIPClaimForProject(ns, claimName, callerProject); claimDeleteLatency.add(delRes.timings.duration); if (delRes.status !== 200 && delRes.status !== 202 && delRes.status !== 404) { claimErrors.add(1); diff --git a/test/load/src/read-latency.js b/test/load/src/read-latency.js index 06eee56..25a2976 100644 --- a/test/load/src/read-latency.js +++ b/test/load/src/read-latency.js @@ -1,16 +1,16 @@ // read-latency.js // // Measures read-path latency under several workload shapes: -// - steady (10 VUs, 3m): 60% cluster-list IPPrefix, 20% ns list IPPrefixClaims, 20% single GET +// - steady (10 VUs, 3m): 60% cluster-list IPPool, 20% ns list IPClaims, 20% single GET // - ramp (0->20->50->0 VUs over 3m): same workload mix // - spike (0->100->0 VUs over 30s): list-heavy // -// Coverage extension scenarios (audit Task #11): assert read latency for the -// other listable resources matches the IPPrefix list envelope. Each runs in -// parallel with the original three so the operator gets a unified summary. -// - addr_list: constant LIST ipaddresses (namespaced) -// - asnpool_list: constant LIST asnpools (cluster scope) -// - asnclaim_list: constant LIST asnclaims (namespaced) +// Coverage extension scenarios: assert read latency for the other listable +// resources matches the IPPool list envelope. Each runs in parallel with the +// original three so the operator gets a unified summary. +// - alloc_list: namespaced LIST ipallocations +// - asnpool_list: constant LIST asnpools (cluster scope) +// - asnclaim_list: namespaced LIST asnclaims // // Every iteration picks a random perf project and scopes all reads to that // project's tenant context (X-Remote-Extra parent headers). @@ -25,12 +25,10 @@ import { check } from 'k6'; import { Rate, Trend } from 'k6/metrics'; import { - listPrefixesForProject, - listPrefixClaimsForProject, - getPrefixForProject, - listIPAddressesForProject, - listASNPoolsForProject, - listASNClaimsForProject, + listIPPoolsForProject, + listIPClaimsForProject, + getIPPoolForProject, + listIPAllocationsForProject, nsFor, projectIDFor, } from '../lib/ipam-client.js'; @@ -38,15 +36,13 @@ import { const NAMESPACE_COUNT = parseInt(__ENV.NAMESPACE_COUNT || '10'); const PROJECT_COUNT = parseInt(__ENV.PROJECT_COUNT || '5'); -const prefixListLatency = new Trend('ipam_prefix_list_ms', true); +const poolListLatency = new Trend('ipam_prefix_list_ms', true); const claimGetLatency = new Trend('ipam_claim_get_ms', true); const clusterListLatency = new Trend('ipam_cluster_list_ms', true); -// New per-resource list trends for the audit-expansion scenarios. Tagged the -// same way as the existing prefix-list trend so dashboards can plot them +// Per-resource list trends for the audit-expansion scenarios. Tagged the +// same way as the existing pool-list trend so dashboards can plot them // side-by-side. -const ipAddressListLatency = new Trend('ipam_ipaddress_list_ms', true); -const asnPoolListLatency = new Trend('ipam_asnpool_list_ms', true); -const asnClaimListLatency = new Trend('ipam_asnclaim_list_ms', true); +const ipAllocationListLatency = new Trend('ipam_ipallocation_list_ms', true); const readSuccessRate = new Rate('ipam_read_success_rate'); export const options = { @@ -84,36 +80,22 @@ export const options = { // -- Coverage extension: dedicated list-only scenarios for the resources // that previously had no read-latency coverage. Each runs against a // modest VU pool for the full steady duration so we get stable p95s. - addr_list: { + alloc_list: { executor: 'constant-vus', vus: 5, duration: '3m', - tags: { scenario: 'addr_list' }, - exec: 'ipAddressList', - }, - asnpool_list: { - executor: 'constant-vus', - vus: 5, - duration: '3m', - tags: { scenario: 'asnpool_list' }, - exec: 'asnPoolList', - }, - asnclaim_list: { - executor: 'constant-vus', - vus: 5, - duration: '3m', - tags: { scenario: 'asnclaim_list' }, - exec: 'asnClaimList', + tags: { scenario: 'alloc_list' }, + exec: 'ipAllocationList', }, + // NOTE: asnpool_list / asnclaim_list scenarios disabled — ASNPool/ASNClaim + // resources are not yet implemented in this branch (see commit 86aceec). }, thresholds: { 'ipam_prefix_list_ms': ['p(95)<200'], 'ipam_claim_get_ms': ['p(95)<100'], 'ipam_cluster_list_ms': ['p(95)<500'], - // Audit gap-fill thresholds: same envelope as the IPPrefix list path. - 'ipam_ipaddress_list_ms': ['p(95)<200'], - 'ipam_asnpool_list_ms': ['p(95)<200'], - 'ipam_asnclaim_list_ms': ['p(95)<200'], + // Audit gap-fill threshold: same envelope as the IPPool list path. + 'ipam_ipallocation_list_ms': ['p(95)<200'], 'ipam_read_success_rate': ['rate>0.99'], }, }; @@ -140,17 +122,17 @@ function doWork() { let res; switch (w) { case 'cluster_list': - res = listPrefixesForProject(projectID); + res = listIPPoolsForProject(projectID); clusterListLatency.add(res.timings.duration); break; case 'ns_list': { const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - res = listPrefixClaimsForProject(ns, projectID); - prefixListLatency.add(res.timings.duration); + res = listIPClaimsForProject(ns, projectID); + poolListLatency.add(res.timings.duration); break; } case 'single_get': - res = getPrefixForProject(`perf-prefix-${projectIdx}`, projectID); + res = getIPPoolForProject(`perf-prefix-${projectIdx}`, projectID); claimGetLatency.add(res.timings.duration); break; } @@ -166,44 +148,28 @@ export function spike() { const r = Math.random(); let res; if (r < 0.7) { - res = listPrefixesForProject(projectID); + res = listIPPoolsForProject(projectID); clusterListLatency.add(res.timings.duration); } else { const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - res = listPrefixClaimsForProject(ns, projectID); - prefixListLatency.add(res.timings.duration); + res = listIPClaimsForProject(ns, projectID); + poolListLatency.add(res.timings.duration); } const ok = check(res, { 'read ok': (r) => r.status === 200 }); readSuccessRate.add(ok ? 1 : 0); } -// ipAddressList: namespaced LIST against a random perf namespace, scoped to -// a random project's tenant context. -export function ipAddressList() { +// ipAllocationList: namespaced LIST against a random perf namespace, scoped +// to a random project's tenant context. +export function ipAllocationList() { const projectID = pickProject(); const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - const res = listIPAddressesForProject(ns, projectID); - ipAddressListLatency.add(res.timings.duration); - const ok = check(res, { 'ipaddress list ok': (r) => r.status === 200 }); + const res = listIPAllocationsForProject(ns, projectID); + ipAllocationListLatency.add(res.timings.duration); + const ok = check(res, { 'ipallocation list ok': (r) => r.status === 200 }); readSuccessRate.add(ok ? 1 : 0); } -// asnPoolList: cluster-scoped LIST. ASNPools are global; the project headers -// are still applied so the auth path matches production traffic. -export function asnPoolList() { - const projectID = pickProject(); - const res = listASNPoolsForProject(projectID); - asnPoolListLatency.add(res.timings.duration); - const ok = check(res, { 'asnpool list ok': (r) => r.status === 200 }); - readSuccessRate.add(ok ? 1 : 0); -} - -// asnClaimList: namespaced LIST against a random perf namespace. -export function asnClaimList() { - const projectID = pickProject(); - const ns = nsFor(Math.floor(Math.random() * NAMESPACE_COUNT)); - const res = listASNClaimsForProject(ns, projectID); - asnClaimListLatency.add(res.timings.duration); - const ok = check(res, { 'asnclaim list ok': (r) => r.status === 200 }); - readSuccessRate.add(ok ? 1 : 0); -} +// ASN list scenarios removed — ASNPool/ASNClaim resources are not implemented +// on this branch. Restore once `asnpools.ipam.miloapis.com` / `asnclaims.ipam.miloapis.com` +// are served. diff --git a/test/load/src/setup-pools.js b/test/load/src/setup-pools.js index eb0118f..22681a0 100644 --- a/test/load/src/setup-pools.js +++ b/test/load/src/setup-pools.js @@ -4,23 +4,20 @@ // // Layout produced: // Platform-level (kept for backwards compatibility with older tests): -// - IPPrefixClass `perf-private` (visibility: consumer) -// - IPPrefix `perf-prefix` (10.0.0.0/8, /20-/28) -// - ASNPoolClass `perf-asn` -// - ASNPool `perf-asn-pool` (4200000000-4200099999) +// - IPPool `perf-prefix` (10.0.0.0/8, /20-/28, visibility=consumer) +// - ASNPoolClass `perf-asn` +// - ASNPool `perf-asn-pool` (4200000000-4200099999) // // Per-project (one set per perf project, n in [0, PROJECT_COUNT)): -// - IPPrefix `perf-prefix-` covering 10..0.0/16 (/20-/28) -// - IPPrefix `perf-ipv6-prefix-` covering fd:::/32 (/40-/56) -// - ASNPool `perf-asn-pool-` each spanning 20k ASNs +// - IPPool `perf-prefix-` covering 10..0.0/16 (/20-/28) +// - IPPool `perf-ipv6-prefix-` covering fd:::/32 (/40-/56) +// - ASNPool `perf-asn-pool-` each spanning 20k ASNs // // Shared cross-project pool (owned by project 0): -// - IPPrefixClass `perf-shared` (visibility: shared, IPv4) -// - IPPrefix `perf-shared-prefix` (172.16.0.0/12, /24-/28) -// - IPPrefixClass `perf-ipv6-shared-class` (visibility: shared, IPv6) -// - IPPrefix `perf-ipv6-shared` (fd00:ffff::/28, /40-/56) -// - ClusterRole `perf-shared-pool-user` (use on perf-shared-prefix) -// - ClusterRole `perf-ipv6-shared-pool-user` (use on perf-ipv6-shared) +// - IPPool `perf-shared-prefix` (172.16.0.0/12, /24-/28, visibility=shared) +// - IPPool `perf-ipv6-shared` (fd00:f000::/28, /40-/56, visibility=shared) +// - ClusterRole `perf-shared-pool-user` (use on perf-shared-prefix) +// - ClusterRole `perf-ipv6-shared-pool-user` (use on perf-ipv6-shared) // - ClusterRoleBinding per project [1..N) granting use of each shared pool // // Namespaces: `ipam-perf-` for n in [0, NAMESPACE_COUNT) @@ -34,8 +31,7 @@ import { check, sleep } from 'k6'; import { - createPrefixClass, - createPrefix, + createIPPool, createASNPoolClass, createASNPool, createNamespace, @@ -48,17 +44,14 @@ import { const NAMESPACE_COUNT = parseInt(__ENV.NAMESPACE_COUNT || '10'); const PROJECT_COUNT = parseInt(__ENV.PROJECT_COUNT || '5'); const SETUP_VUS = parseInt(__ENV.SETUP_VUS || '1'); -// IPPrefixClass.spec.visibility for the cross-project pool. The server -// accepts any string for Visibility (plain string field with no enum -// validation), so 'shared' is accepted today and matches the documented -// intent. +// IPPool.spec.visibility for the cross-project pool. The apiserver enum is +// platform|consumer|shared; 'shared' is the value cross-project tests use. const SHARED_VISIBILITY = __ENV.SHARED_VISIBILITY || 'shared'; // Each per-project ASN pool spans 20k ASNs starting at this base. const ASN_BASE = 4200000000; const ASN_PER_PROJECT = 20000; -const SHARED_CLASS_NAME = 'perf-shared'; const SHARED_PREFIX_NAME = 'perf-shared-prefix'; const SHARED_POOL_USER_ROLE = 'perf-shared-pool-user'; @@ -73,7 +66,6 @@ const SHARED_POOL_USER_ROLE = 'perf-shared-pool-user'; // // minPrefixLength=40 corresponds to a SMALLER prefix length number (LARGER // block), maxPrefixLength=56 a LARGER number (SMALLER block). -const SHARED_IPV6_CLASS_NAME = 'perf-ipv6-shared-class'; const SHARED_IPV6_PREFIX_NAME = 'perf-ipv6-shared'; const IPV6_POOL_USER_ROLE = 'perf-ipv6-shared-pool-user'; const IPV6_MIN_LEN = 40; @@ -91,28 +83,20 @@ export const options = { }, }; -function okOrConflict(name) { +function okOrConflict() { return (res) => res.status === 201 || res.status === 409; } export default function () { // ---- Platform-level pool (legacy / compatibility) ---- - let r = createPrefixClass('perf-private', { - requiresVerification: false, - visibility: 'consumer', - minLen: 20, - maxLen: 28, - strategy: 'FirstFit', - }); - check(r, { 'perf-private class created or exists': okOrConflict() }); - - r = createPrefix('perf-prefix', '10.0.0.0/8', 'perf-private', { + let r = createIPPool('perf-prefix', '10.0.0.0/8', { ipFamily: 'IPv4', + visibility: 'consumer', minLen: 20, maxLen: 28, strategy: 'FirstFit', }); - check(r, { 'perf-prefix created or exists': okOrConflict() }); + check(r, { 'perf-prefix pool created or exists': okOrConflict() }); r = createASNPoolClass('perf-asn', { requiresVerification: false, visibility: 'consumer' }); check(r, { 'perf-asn class created or exists': okOrConflict() }); @@ -133,45 +117,47 @@ export default function () { const sliceStart = vuIndex * sliceSize; const sliceEnd = Math.min(sliceStart + sliceSize, PROJECT_COUNT); - let projectPrefixes = 0; + let projectPools = 0; let projectASNPools = 0; - let projectIPv6Prefixes = 0; + let projectIPv6Pools = 0; for (let n = sliceStart; n < sliceEnd; n++) { - const prefixName = `perf-prefix-${n}`; + const poolName = `perf-prefix-${n}`; // CIDR: projects 0-255 → 10.0.x.x/16, 256-511 → 10.1.x.x/16, etc. // Uses octets 10-13 (covering 0-1023 projects within RFC1918 space). const cidr = `${10 + Math.floor(n / 256)}.${n % 256}.0.0/16`; - const pres = createPrefix(prefixName, cidr, 'perf-private', { + const pres = createIPPool(poolName, cidr, { ipFamily: 'IPv4', + visibility: 'consumer', minLen: 20, maxLen: 28, strategy: 'FirstFit', }); if (pres.status === 201 || pres.status === 409) { - projectPrefixes++; + projectPools++; } else { - console.error(`per-project prefix ${prefixName} create failed: ${pres.status} ${pres.body}`); + console.error(`per-project pool ${poolName} create failed: ${pres.status} ${pres.body}`); } // Per-project IPv6 pool. fd:::/32 with HH = n>>8, LLLL = n&0xff. // Project 0 → fd00:0000::/32, project 1 → fd00:0001::/32, ... // Up to 65536 perf projects fit in fd00::/16 without collisions. - const v6Prefix = `perf-ipv6-prefix-${n}`; + const v6PoolName = `perf-ipv6-prefix-${n}`; const hi = (n >> 8) & 0xff; const lo = n & 0xff; const v6Cidr = `fd${hi.toString(16).padStart(2, '0')}:` + `${lo.toString(16).padStart(4, '0')}::/32`; - const v6Res = createPrefix(v6Prefix, v6Cidr, 'perf-private', { + const v6Res = createIPPool(v6PoolName, v6Cidr, { ipFamily: 'IPv6', + visibility: 'consumer', minLen: IPV6_MIN_LEN, maxLen: IPV6_MAX_LEN, strategy: 'FirstFit', }); if (v6Res.status === 201 || v6Res.status === 409) { - projectIPv6Prefixes++; + projectIPv6Pools++; } else { - console.error(`per-project IPv6 prefix ${v6Prefix} create failed: ${v6Res.status} ${v6Res.body}`); + console.error(`per-project IPv6 pool ${v6PoolName} create failed: ${v6Res.status} ${v6Res.body}`); } const asnPoolName = `perf-asn-pool-${n}`; @@ -184,33 +170,26 @@ export default function () { console.error(`per-project ASN pool ${asnPoolName} create failed: ${ares.status} ${ares.body}`); } } - check(projectPrefixes, { 'per-vu prefixes created': (n) => n === sliceEnd - sliceStart }); - check(projectIPv6Prefixes, { 'per-vu IPv6 prefixes created': (n) => n === sliceEnd - sliceStart }); + check(projectPools, { 'per-vu pools created': (n) => n === sliceEnd - sliceStart }); + check(projectIPv6Pools, { 'per-vu IPv6 pools created': (n) => n === sliceEnd - sliceStart }); check(projectASNPools, { 'per-vu ASN pools created': (n) => n === sliceEnd - sliceStart }); // ---- Shared cross-project pool (owned by project 0) ---- - r = createPrefixClass(SHARED_CLASS_NAME, { - requiresVerification: false, - visibility: SHARED_VISIBILITY, - minLen: 24, - maxLen: 28, - strategy: 'FirstFit', - }); - check(r, { 'perf-shared class created or exists': okOrConflict() }); - - r = createPrefix(SHARED_PREFIX_NAME, '172.16.0.0/12', SHARED_CLASS_NAME, { + r = createIPPool(SHARED_PREFIX_NAME, '172.16.0.0/12', { ipFamily: 'IPv4', + visibility: SHARED_VISIBILITY, minLen: 24, maxLen: 28, strategy: 'FirstFit', }); - check(r, { 'perf-shared-prefix created or exists': okOrConflict() }); + check(r, { 'perf-shared-prefix pool created or exists': okOrConflict() }); - // ClusterRole granting the `use` verb on the shared pool + // ClusterRole granting the `use` verb on the shared pool. The CanUsePool + // check targets the `ippools` resource. r = createClusterRole(SHARED_POOL_USER_ROLE, [ { apiGroups: ['ipam.miloapis.com'], - resources: ['ipprefixes'], + resources: ['ippools'], resourceNames: [SHARED_PREFIX_NAME], verbs: ['use'], }, @@ -245,27 +224,19 @@ export default function () { // fd00:f000::/28 sits above the per-project /32s (which use lo bytes 0..ff // in the second 16-bit group), so it can never overlap with a per-project // pool no matter how PROJECT_COUNT grows. - r = createPrefixClass(SHARED_IPV6_CLASS_NAME, { - requiresVerification: false, - visibility: SHARED_VISIBILITY, - minLen: IPV6_MIN_LEN, - maxLen: IPV6_MAX_LEN, - strategy: 'FirstFit', - }); - check(r, { 'perf-ipv6-shared-class created or exists': okOrConflict() }); - - r = createPrefix(SHARED_IPV6_PREFIX_NAME, 'fd00:f000::/28', SHARED_IPV6_CLASS_NAME, { + r = createIPPool(SHARED_IPV6_PREFIX_NAME, 'fd00:f000::/28', { ipFamily: 'IPv6', + visibility: SHARED_VISIBILITY, minLen: IPV6_MIN_LEN, maxLen: IPV6_MAX_LEN, strategy: 'FirstFit', }); - check(r, { 'perf-ipv6-shared created or exists': okOrConflict() }); + check(r, { 'perf-ipv6-shared pool created or exists': okOrConflict() }); r = createClusterRole(IPV6_POOL_USER_ROLE, [ { apiGroups: ['ipam.miloapis.com'], - resources: ['ipprefixes'], + resources: ['ippools'], resourceNames: [SHARED_IPV6_PREFIX_NAME], verbs: ['use'], }, @@ -306,8 +277,8 @@ export default function () { sleep(2); console.log( - `setup complete: platform pool perf-prefix(/8), ${projectPrefixes}/${PROJECT_COUNT} per-project /16 prefixes, ` + - `${projectIPv6Prefixes}/${PROJECT_COUNT} per-project IPv6 /32 prefixes, ` + + `setup complete: platform pool perf-prefix(/8), ${projectPools}/${PROJECT_COUNT} per-project /16 IPv4 pools, ` + + `${projectIPv6Pools}/${PROJECT_COUNT} per-project IPv6 /32 pools, ` + `${projectASNPools}/${PROJECT_COUNT} per-project ASN pools, shared pool perf-shared-prefix(/12), ` + `shared IPv6 pool ${SHARED_IPV6_PREFIX_NAME}(/28), ` + `${bindings}/${PROJECT_COUNT - 1} v4 bindings, ${v6Bindings}/${PROJECT_COUNT - 1} v6 bindings, ` + diff --git a/test/load/src/watch-latency.js b/test/load/src/watch-latency.js index d6231fc..b173bea 100644 --- a/test/load/src/watch-latency.js +++ b/test/load/src/watch-latency.js @@ -1,8 +1,8 @@ // watch-latency.js // -// SLO probe for the IPPrefixClaim watch pipeline (LISTEN ipam_changelog + -// polling cursor): how long after a CREATE commits does the server start -// streaming the ADDED event to a watcher? +// SLO probe for the IPClaim watch pipeline (LISTEN ipam_changelog + polling +// cursor): how long after a CREATE commits does the server start streaming +// the ADDED event to a watcher? // // Implementation note: k6's HTTP client buffers the entire response body — // there is no true streaming. So we cannot timestamp individual events as @@ -15,8 +15,8 @@ // // Scenario: // - Two interleaved single-VU loops via shared-iterations: -// - listAndCreate: lists current RV, creates one IPPrefixClaim with -// a `created-at-ms` label, deletes it, sleeps, repeats. +// - listAndCreate: lists current RV, creates one IPClaim with a +// `created-at-ms` label, deletes it, sleeps, repeats. // - watch: in lockstep, opens a watch with resourceVersion= // and timeoutSeconds=W. Computes lag = TTFB-anchored arrival time of // the first ADDED event minus the createdAt label value. @@ -40,9 +40,9 @@ import { sleep } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; import { API_BASE, - deletePrefixClaimForProject, + deleteIPClaimForProject, nsFor, - prefixClaimPath, + ipClaimPath, projectIDFor, withProjectTagged, } from '../lib/ipam-client.js'; @@ -86,11 +86,11 @@ export const options = { }, }; -// Issue a GET against the IPPrefixClaim list to obtain the current +// Issue a GET against the IPClaim list to obtain the current // resourceVersion. Returned as a string (k8s RVs are opaque). function currentResourceVersion() { - const params = withProjectTagged(PROJECT, 'list_prefix_claims_rv'); - const res = http.get(`${API_BASE}${prefixClaimPath(NS)}?limit=1`, params); + const params = withProjectTagged(PROJECT, 'list_ipclaims_rv'); + const res = http.get(`${API_BASE}${ipClaimPath(NS)}?limit=1`, params); if (res.status !== 200) { return ''; } @@ -108,13 +108,13 @@ function currentResourceVersion() { // pinpoints when the server started emitting events for our resourceVersion // cursor — which is when our committed CREATE became visible to the watch. function watchOnce(rv, expectedCreatedAtMs) { - const params = withProjectTagged(PROJECT, 'watch_prefix_claims'); + const params = withProjectTagged(PROJECT, 'watch_ipclaims'); // Buffer the connection generously so the server can drive timeoutSeconds // without us cutting it off early. params.timeout = `${WATCH_TIMEOUT_S + 30}s`; const url = - `${API_BASE}${prefixClaimPath(NS)}?watch=true` + + `${API_BASE}${ipClaimPath(NS)}?watch=true` + `&resourceVersion=${encodeURIComponent(rv)}` + `&timeoutSeconds=${WATCH_TIMEOUT_S}` + `&allowWatchBookmarks=true`; @@ -188,17 +188,17 @@ function createClaim(name, createdAtMs) { labels[CREATED_AT_LABEL] = String(createdAtMs); const body = { apiVersion: 'ipam.miloapis.com/v1alpha1', - kind: 'IPPrefixClaim', + kind: 'IPClaim', metadata: { name, namespace: NS, labels }, spec: { ipFamily: 'IPv4', prefixLength: 28, - prefixRef: { name: POOL_NAME }, + poolRef: { name: POOL_NAME }, reclaimPolicy: 'Delete', }, }; - const params = withProjectTagged(PROJECT, 'watch_prefix_claim_create'); - return http.post(`${API_BASE}${prefixClaimPath(NS)}`, JSON.stringify(body), params); + const params = withProjectTagged(PROJECT, 'watch_ipclaim_create'); + return http.post(`${API_BASE}${ipClaimPath(NS)}`, JSON.stringify(body), params); } export function probe() { @@ -225,7 +225,7 @@ export function probe() { // ADDED event as the first byte. watchOnce(rv, createdAtMs); // 4. Cleanup so the next iteration starts from a known state. - deletePrefixClaimForProject(NS, name, PROJECT); + deleteIPClaimForProject(NS, name, PROJECT); // Small spacing so consecutive probes don't pile up on the changelog. sleep(0.25); }