Execution host — run
hostname -sbefore deciding whether to use SSH. Treat bothcasibbald-MS-02-Ultraandms02as the ms02 execution host. An agent already on ms02 runs Git, builds, tests, Docker, Kubernetes, and filesystem commands directly under~/Workspace/microscaler/DCops; it must not SSH back into ms02. Agents starting elsewhere may usessh ms02to reach that same checkout. Tunnels are inbound access for other machines, not a reason for an ms02-local agent to connect outward.The active development cluster and registry are owned by the sibling
../shared-gitops-k8s-clusterrepository.cylon-local-infrais deprecated and must not be used as a topology or workflow dependency.
DCops deploys to the shared k3s management cluster, not a dedicated local cluster.
| Item | Value |
|---|---|
| Cluster | context shared-k8s |
| Registry | 10.177.76.220:5000 |
| Prerequisite repo | ../shared-gitops-k8s-cluster |
| DCops namespaces | netbox, dcops-system |
| Tilt UI port | 10354 (tilt-dcops.service) |
| NetBox UI (Tilt forward) | http://localhost:8011 |
| Kea Control Agent (Tilt forward) | http://localhost:8010 |
| PXE HTTP (Tilt forward) | http://localhost:8088 |
Workflow: just verify-shared-k8s → just dev-up (or just tilt-up).
just dev-down stops Tilt only — it does not delete the shared cluster.
Override checkout layout with
SHARED_K8S_CLUSTER_ROOT=/path/to/shared-gitops-k8s-cluster.
This document provides specific guidance for AI agents working on the DCops codebase, with explicit modularization requirements from the start.
MANDATORY: Before creating ANY new function, method, or helper, you MUST:
- Search the codebase for existing helpers, traits, or utilities that already do what you need
- Check if existing code can be extended rather than duplicated
- Use existing patterns - don't reinvent the wheel
- Refactor to use helpers - if helpers exist but aren't being used, fix the code to use them
Why: The whole point of creating a client/library is to have DRY (Don't Repeat Yourself) code, not to move calls out of reconcilers into a WET (Write Everything Twice) mess.
ALWAYS ask yourself:
- Does a helper function already exist that does this?
- Is there a trait that can be extended?
- Can I refactor existing code to use a helper instead of duplicating?
- Have I searched the codebase for similar patterns?
- Am I following the DRY principle?
❌ BAD: Duplicating slug generation in 12 methods
// BAD: Duplicated in every create method
let slug_value = if let Some(slug_str) = slug {
slug_str.to_string()
} else {
name.to_lowercase().replace(' ', "-")
};✅ GOOD: Using a helper function
// GOOD: Single helper used everywhere
let slug_value = Self::generate_slug(name, slug);Before writing new code:
-
Grep for similar patterns:
grep -r "to_lowercase().replace" crates/ grep -r "if let Some.*body\[" crates/
-
Check for existing helpers:
grep -r "fn.*helper\|fn.*add_\|fn.*generate" crates/ -
Look for traits:
grep -r "trait.*Trait" crates/ -
Check documentation:
- Read
docs/NETBOX_CLIENT_AUDIT.mdfor known helpers - Check
rust-guidelines.txtfor patterns - Review existing code for established patterns
- Read
If you find existing code that duplicates patterns:
- Extract to helper - Create a helper function
- Refactor all uses - Update all methods to use the helper
- Verify compilation - Ensure everything still compiles
- Run tests - Verify functionality is preserved
- Update documentation - Document the helper
Never:
- ❌ Leave duplicate code "for now"
- ❌ Create a new helper but not use it everywhere
- ❌ Claim code is done when duplication exists
MANDATORY: When creating new code, always create proper module structure from the beginning. Never write monolithic files that will need to be refactored later.
Why: Refactoring large files into modules is:
- Expensive: Takes days of work
- Risky: High chance of introducing bugs
- Unnecessary: Can be avoided by starting with modules
When implementing a new feature or crate, always:
- ✅ Create module files first - Before writing any implementation
- ✅ Define module boundaries - What goes in which module?
- ✅ Add module documentation -
//!docs for each module - ✅ Keep modules small - Target 200-300 lines, max 500 lines
- ✅ One responsibility per module - Clear, single purpose
Always use this structure for crates/*:
// lib.rs - Re-exports only (< 50 lines)
//! Brief description of the crate.
//!
//! Extended documentation explaining the crate's purpose,
//! when to use it, and key concepts.
pub mod error;
pub mod client; // or service, controller, etc.
pub mod models; // or types, domain, etc.
#[doc(inline)]
pub use error::*;
#[doc(inline)]
pub use client::*;
#[doc(inline)]
pub use models::*;Module breakdown:
error.rs- All error types for this crateclient.rs- Main client/service implementationmodels.rs- Data structures and types
Example: Creating pxe-client
// Step 1: Create lib.rs with module structure
//! PXE Boot Service Client
//!
//! Client for interacting with PXE boot services.
pub mod error;
pub mod pixiecore;
pub use error::PxeError;
pub use pixiecore::PixiecoreClient;
// Step 2: Create error.rs
//! PXE client errors
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PxeError {
// ...
}
// Step 3: Create pixiecore.rs
//! Pixiecore API client
use crate::error::PxeError;
pub struct PixiecoreClient {
// ...
}Always use this structure for controllers/*:
// main.rs - Entry point only (< 100 lines)
//! Controller name and purpose
//!
//! Extended description.
mod controller;
mod reconciler;
mod watcher;
mod error;
mod config; // Only if config is > 100 lines
use controller::Controller;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let config = config::load()?;
let controller = Controller::new(config).await?;
controller.run().await?;
Ok(())
}Module breakdown:
controller.rs- Main controller struct, lifecycle, initializationreconciler.rs- Reconciliation logic for CRDswatcher.rs- Kubernetes resource watcherserror.rs- Controller-specific errorsconfig.rs- Configuration types (only if needed and > 100 lines)
Example: Creating pxe-intent-controller
// Step 1: Create main.rs with module structure
mod controller;
mod reconciler;
mod watcher;
mod error;
use controller::Controller;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// ...
}
// Step 2: Create each module file immediately
// controller.rs, reconciler.rs, watcher.rs, error.rsFor crates/crds, use one file per CRD:
// lib.rs
pub mod boot_profile;
pub mod boot_intent;
pub mod ip_pool;
pub mod ip_claim;
pub use boot_profile::*;
pub use boot_intent::*;
pub use ip_pool::*;
pub use ip_claim::*;Each CRD in its own file:
boot_profile.rs- BootProfile CRD definitionboot_intent.rs- BootIntent CRD definition- etc.
- Maximum: 500 lines per module (excluding tests)
- Warning threshold: 400 lines - split immediately
- Target: 200-300 lines per module
Split a module when:
- It exceeds 400 lines
- It has multiple distinct responsibilities
- It's hard to understand at a glance
- Tests are becoming hard to organize
- Identify responsibilities - What distinct concerns exist?
- Create new modules - One per responsibility
- Move code - Keep related code together
- Update imports - Fix all references
- Update tests - Move tests to appropriate modules
//! Brief description (< 15 words).
//!
//! Extended documentation explaining:
//! - What this module contains
//! - When to use it
//! - Key concepts or patterns
//! - Examples if helpful
//! NetBox REST API client implementation.
//!
//! This module provides the `NetBoxClient` type for interacting with
//! the NetBox API. It handles authentication, request building, and
//! response parsing.
//!
//! # Examples
//!
//! ```no_run
//! use netbox_client::NetBoxClient;
//!
//! let client = NetBoxClient::new("https://netbox.example.com", "token")?;
//! let prefix = client.get_prefix(1).await?;
//! ```
Every crate with errors should have an error.rs module:
//! Error types for this crate.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CrateError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("API error: {0}")]
Api(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}- Create crate directory and Cargo.toml
- Create
src/directory - Create
lib.rswith module declarations (empty modules are fine) - Create each module file with
todo!()placeholders - Add module documentation to each file
- Implement modules one at a time
- Write tests as you implement - Don't wait until the end
- Verify coverage - Run
just test-coverageregularly - Verify functionality - Not just compilation, actually test it works
- Write tests as you implement - Don't wait until the end
- Verify coverage - Run
just test-coverageregularly - Verify functionality - Not just compilation, actually test it works
# 1. Create crate
mkdir -p crates/my-crate/src
# 2. Create Cargo.toml (with dependencies)
# 3. Create lib.rs with structure
cat > crates/my-crate/src/lib.rs << 'EOF'
//! My crate description
pub mod error;
pub mod client;
pub mod models;
pub use error::*;
pub use client::*;
pub use models::*;
EOF
# 4. Create module files
touch crates/my-crate/src/{error,client,models}.rs
# 5. Add documentation and placeholders to each// BAD: Everything in one file
// lib.rs (2000 lines)
pub struct Error { }
pub struct Client { }
pub struct Model1 { }
pub struct Model2 { }
// ... 2000 lines// BAD: Generic, unclear purpose
pub mod util;
pub mod common;
pub mod helper;
pub mod misc;// BAD: "I'll split this later"
// lib.rs - 800 lines, "I'll refactor it next week"// BAD: No module docs
pub mod client;
// GOOD: Has module docs
//! Client implementation for API interactions.
pub mod client;When reviewing agent-generated code, check:
- Module structure exists - Not everything in one file
- Module size - No module > 500 lines
- Module documentation - Every module has
//!docs - Clear responsibilities - Each module has one purpose
- Error module - Errors in dedicated
error.rsif needed - Re-exports -
lib.rsproperly re-exports public items - No generic names - No
util,common,helpermodules - Tests exist - All public APIs have tests
- Test coverage - Minimum 65%, target 80% (run
just test-coverage) - Functionality verified - Not just compilation, actually works
- Controller verification - For controllers, CRs verified to reconcile
- Database verification - For NetBox resources, verified in database
crates/netbox-client/
├── Cargo.toml
└── src/
├── lib.rs # Re-exports
├── error.rs # Error types
├── client.rs # Client implementation
└── models.rs # Data structures
crates/crds/
├── Cargo.toml
└── src/
├── lib.rs # Re-exports
├── boot_profile.rs # One CRD per file
├── boot_intent.rs
├── ip_pool.rs
└── ip_claim.rs
controllers/pxe-intent/
├── Cargo.toml
└── src/
├── main.rs # Entry point
├── controller.rs
├── reconciler.rs
├── watcher.rs
└── error.rs
Agents must:
- Always create module structure first - Before any implementation
- Never create files > 500 lines - Split immediately
- Always add module documentation -
//!docs required - Follow standard patterns - Use established structures
- Never claim code works just because it compiles - Must verify functionality
- Write tests with adequate coverage - Minimum 65%, target 80%
- Verify controller reconciliation - Use verification scripts for NetBox CRs
MANDATORY: Code that compiles successfully is NOT considered working. You MUST verify functionality before claiming completion.
Before claiming code is complete, verify:
- ✅ Compilation - Use
python3 scripts/host_aware_build.py --release -p netbox-controllerfor comprehensive error checking (DO NOT USEcargo check, which may miss errors) - ✅ Tests pass -
cargo testpasses - ✅ Test coverage - Minimum 65% coverage, target 80%
- ✅ Integration works - For controllers, CRs actually reconcile
- ✅ Database verification - For NetBox resources, they exist in the database
python3 scripts/host_aware_build.py --release -p netbox-controller) to check for compilation errors. cargo check may be incomplete and miss some errors that only appear during a full release build.
After implementing reconciliation logic:
# 1. Verify CRD exists
kubectl get crd netboxprefixes.dcops.microscaler.io
# 2. Verify CR has status
kubectl get netboxprefix default/control-plane-prefix -o jsonpath='{.status}'
# 3. Verify resource in NetBox database
python3 scripts/verify_netbox_crs.py --crd netboxprefixes --name control-plane-prefix
# Or verify all CRs
just verify-netbox-crsNever claim reconciliation works just because:
- ❌ Code compiles
- ❌ Controller starts without errors
- ❌ No obvious errors in logs
Always verify:
- ✅ CR status has
netboxIdpopulated - ✅ Resource exists in NetBox database
- ✅ Status state is
Created - ✅ No errors in status
- Minimum: 65% coverage
- Target: 80% coverage
- Tool:
cargo-llvm-cov(LLVM-based coverage) - Command:
just test-coverage
Coverage must be verified before:
- Marking a feature as complete
- Submitting code for review
- Claiming code is working
If unsure about module organization:
- Check existing crates (
netbox-client,crds) for patterns - Follow the standard patterns in this document
- When in doubt, create more modules, not fewer
Remember: Starting with proper modules is free. Refactoring later is expensive.
Remember: Compilation is the first step, not the last. Always verify functionality.
When implementing or modifying NetBox CRDs and their corresponding CRs, agents must verify the complete reconciliation flow:
For every CRD and its corresponding CR, verify:
- ✅ CRD exists in Kubernetes
- ✅ CR has been created and reconciled
- ✅ CR has status populated
- ✅ Resource exists in NetBox database
Quick commands:
# List all NetBox CRDs
kubectl get crd | grep netbox
# Check specific CRD
kubectl get crd netboxprefixes.dcops.microscaler.io -o yamlOr use the quick verification script:
./scripts/verify_netbox_crs_quick.shExpected: CRD should exist with proper schema and status subresource enabled.
# List all CRs of a type
kubectl get netboxprefixes -A
# Get specific CR with status
kubectl get netboxprefix default/control-plane-prefix -o yaml
# Check status specifically
kubectl get netboxprefix default/control-plane-prefix -o jsonpath='{.status}'Expected:
- CR should exist
status.netboxIdshould be populated (non-null)status.stateshould beCreatedstatus.netboxUrlshould be populatedstatus.lastReconciledshould have a timestampstatus.errorshould be null/empty if successful
Use the PostgreSQL database query pattern to verify the resource actually exists in NetBox:
# Get PostgreSQL pod
POSTGRES_POD=$(kubectl get pod -n netbox -l app=postgres -o jsonpath='{.items[0].metadata.name}')
# Query for a prefix (example)
kubectl exec -n netbox $POSTGRES_POD -- psql -U netbox -d netbox -c \
"SELECT id, prefix, status, description FROM ipam_prefix WHERE prefix = '192.168.1.0/24';"
# Query for a tenant (example)
kubectl exec -n netbox $POSTGRES_POD -- psql -U netbox -d netbox -c \
"SELECT id, name, slug FROM tenancy_tenant WHERE name = 'Data Center Operations';"
# Query for a site (example)
kubectl exec -n netbox $POSTGRES_POD -- psql -U netbox -d netbox -c \
"SELECT id, name, slug, status FROM dcim_site WHERE name = 'datacenter-1';"Expected: Database query should return a row with matching data.
Common NetBox tables for verification:
| CRD Type | NetBox Table | Key Fields |
|---|---|---|
NetBoxPrefix |
ipam_prefix |
id, prefix, status, description |
NetBoxTenant |
tenancy_tenant |
id, name, slug, description |
NetBoxSite |
dcim_site |
id, name, slug, status, description |
NetBoxRole |
ipam_role |
id, name, slug, description |
NetBoxTag |
extras_tag |
id, name, slug, color |
NetBoxAggregate |
ipam_aggregate |
id, prefix, rir_id, description |
NetBoxVLAN |
ipam_vlan |
id, vid, name, site_id, status |
NetBoxDeviceRole |
dcim_devicerole |
id, name, slug, color |
NetBoxManufacturer |
dcim_manufacturer |
id, name, slug, description |
NetBoxPlatform |
dcim_platform |
id, name, slug, manufacturer_id |
NetBoxDeviceType |
dcim_devicetype |
id, manufacturer_id, model, slug |
NetBoxRegion |
dcim_region |
id, name, slug, parent_id |
NetBoxSiteGroup |
dcim_sitegroup |
id, name, slug, parent_id |
NetBoxLocation |
dcim_location |
id, name, slug, site_id, parent_id |
Create a verification script for each CRD type:
A simple example script demonstrating basic verification is available at:
scripts/verify_netbox_prefix_simple.py- Simple example for verifying a single NetBoxPrefix CR
This example script demonstrates:
- How to verify CRD exists
- How to check CR status
- How to query NetBox database
Usage:
# Verify default control-plane-prefix
python3 scripts/verify_netbox_prefix_simple.py
# Verify specific CR
python3 scripts/verify_netbox_prefix_simple.py my-prefix defaultNote: For comprehensive verification of all CRs, use scripts/verify_netbox_crs.py instead.
Agents must verify after:
- Creating a new CRD
- Modifying reconciliation logic
- Adding a new CR type
- Fixing reconciliation bugs
- Before marking a feature as complete
Issue: CR exists but no status
- Check: Controller is running and has RBAC permissions
- Fix: Ensure RBAC includes the CRD and status subresource
Issue: Status exists but netboxId is null
- Check: Controller logs for reconciliation errors
- Fix: Check NetBox API connectivity and token validity
Issue: Status has netboxId but not in database
- Check: NetBox API returned success but resource wasn't created
- Fix: Check NetBox API logs and verify create operation succeeded
Issue: Resource in database but CR status is wrong
- Check: Startup reconciliation logic
- Fix: Ensure startup reconciliation maps existing resources correctly
Quick bash commands for manual verification are available in:
scripts/verify_netbox_crs_quick.sh- Quick verification script with kubectl commands
Usage:
# Run quick verification
./scripts/verify_netbox_crs_quick.shOr run the commands manually:
# Verify all NetBox CRDs exist
kubectl get crd | grep netbox
# Verify all CRs have status
for crd in netboxprefixes netboxtenants netboxsites netboxroles netboxtags netboxaggregates netboxvlans; do
echo "Checking $crd..."
kubectl get $crd -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}: {.status.netboxId}{"\n"}{end}'
done
# Verify specific resource in database
POSTGRES_POD=$(kubectl get pod -n netbox -l app=postgres -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n netbox $POSTGRES_POD -- psql -U netbox -d netbox -c "SELECT id, name FROM tenancy_tenant;"Comprehensive Verification:
scripts/verify_netbox_crs.py- Full-featured verification script for all NetBox CRs
Usage:
# Verify all NetBox CRDs and CRs
python3 scripts/verify_netbox_crs.py --all
# Verify specific CRD type
python3 scripts/verify_netbox_crs.py --crd netboxprefixes
# Verify specific CR
python3 scripts/verify_netbox_crs.py --crd netboxprefixes --name control-plane-prefix
# Verify with custom namespaces
python3 scripts/verify_netbox_crs.py --all --namespace default --netbox-namespace netbox
# Or use the justfile command
just verify-netbox-crsAvailable Verification Scripts:
scripts/verify_netbox_crs.py- Comprehensive verification (recommended)scripts/verify_netbox_prefix_simple.py- Simple example for single CRscripts/verify_netbox_crs_quick.sh- Quick bash commands
The comprehensive script automatically:
- ✅ Verifies CRD exists
- ✅ Checks CR has status with netboxId
- ✅ Queries NetBox database to confirm resource exists
- ✅ Reports comprehensive status for all resources
Example output:
============================================================
Verifying All NetBox CRDs
============================================================
ℹ️ Using PostgreSQL pod: postgres-7d8f9c4b5-abc123
============================================================
Verifying netboxprefixes
============================================================
✅ CRD netboxprefixes.dcops.microscaler.io exists
Checking default/control-plane-prefix...
✅ CR default/control-plane-prefix has status (netboxId: 1, state: Created)
✅ Resource exists in NetBox database (ID: 1, prefix: 192.168.1.0/24)
============================================================
✅ All verifications passed!
When developing with Tilt, verification should happen automatically:
- Tilt applies CRDs via
generate-crdsresource - Tilt applies example CRs
- Controller reconciles CRs
- Agent verifies all CRs have status and exist in NetBox
Add verification as a Tilt local_resource if needed for continuous validation.
IMPORTANT: CRDs in config/crd/all-crds.yaml are ephemeral and automatically generated from Rust code. They should never be manually edited.
- Source of Truth: CRD definitions are in
crates/crds/src/(Rust code) - Generation: CRDs are generated by running
cargo run -p crds --bin crdgen - Output: Generated YAML is written to
config/crd/all-crds.yaml - Tilt Integration: Tilt automatically regenerates CRDs when:
- CRD code in
crates/crds/src/changes crates/crds/Cargo.tomlchangesscripts/generate_crds.pychanges
- CRD code in
✅ DO:
- Edit CRD definitions in
crates/crds/src/(Rust code) - Run
python3 scripts/generate_crds.pyto regenerate CRDs - Let Tilt automatically regenerate CRDs during development
- Commit changes to
crates/crds/src/(the source code)
❌ DON'T:
- Manually edit
config/crd/all-crds.yaml(it will be overwritten) - Commit manual changes to
config/crd/all-crds.yaml - Assume CRD YAML files are the source of truth
- Try to fix CRD issues by editing YAML directly
When modifying CRDs:
-
Edit Rust code in
crates/crds/src/:// crates/crds/src/dcim/netbox_device.rs #[derive(CustomResource, ...)] pub struct NetBoxDeviceSpec { // Your changes here }
-
Regenerate CRDs:
# Manual generation python3 scripts/generate_crds.py # Or via cargo directly cargo run -p crds --bin crdgen > config/crd/all-crds.yaml
-
Tilt will automatically regenerate when you run
tilt up:- Tilt watches
crates/crds/src/for changes - Automatically runs
generate-crdsresource - Applies updated CRDs to the cluster
- Tilt watches
- Single Source of Truth: Rust code is the authoritative definition
- Type Safety: Rust types ensure consistency between code and CRDs
- Automatic Updates: Tilt ensures CRDs stay in sync with code
- No Drift: Prevents manual YAML edits from diverging from code
If CRDs aren't working as expected:
- Check the source code in
crates/crds/src/- this is what matters - Regenerate CRDs manually:
python3 scripts/generate_crds.py - Check Tilt logs for
generate-crdsresource errors - Verify CRD code compiles:
cargo check -p crds - Never edit YAML directly - fix the Rust code instead