Skip to content

Developer Guide

This guide provides comprehensive information for developers who want to understand, modify, or extend the Subnetter codebase.

Table of Contents

Project Structure

Subnetter is organized as a monorepo with multiple packages:

subnetter/
├── packages/
│ ├── core/ # Core CIDR allocation engine
│ │ ├── src/
│ │ │ ├── allocator/ # CIDR allocation logic
│ │ │ ├── config/ # Configuration loading and validation
│ │ │ ├── models/ # TypeScript interfaces
│ │ │ ├── output/ # Output generation (CSV)
│ │ │ └── index.ts # Public API exports
│ │ ├── tests/ # Unit tests for core
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── cli/ # Command-line interface
│ │ ├── src/
│ │ │ ├── commands/ # CLI command implementations
│ │ │ └── index.ts # CLI entry point
│ │ ├── tests/ # Unit tests for CLI
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── cidr-utils/ # Low-level CIDR utilities
│ │ ├── src/
│ │ │ ├── calculator.ts # CIDR calculations
│ │ │ ├── ip.ts # IP address manipulation
│ │ │ ├── parser.ts # CIDR parsing
│ │ │ ├── subnet.ts # Subnet allocation
│ │ │ ├── types.ts # TypeScript interfaces
│ │ │ ├── validator.ts # CIDR validation
│ │ │ └── index.ts # Public API exports
│ │ ├── tests/ # Unit tests for utilities
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── docs/ # Documentation site
│ ├── src/
│ │ ├── content/ # Documentation content
│ │ ├── components/ # Astro components
│ │ └── styles/ # CSS stylesheets
│ ├── public/ # Static assets
│ ├── package.json
│ └── astro.config.mjs # Astro configuration
├── __tests__/ # End-to-end tests
│ └── e2e/ # End-to-end test files
├── .github/ # GitHub Actions workflows
├── config/ # Build and configuration files
├── scripts/ # Utility scripts
├── examples/ # Example configurations
├── tsconfig.json # Root TypeScript configuration
├── package.json # Root package.json with workspaces
└── README.md # Project documentation

Development Setup

Prerequisites

  • Node.js (^18.18.0 || ^20.9.0 || >=21.1.0)
  • Yarn (the project uses Yarn Berry 4.7.0 via the yarn releases file)

Installing and Configuring Yarn

This project uses Yarn with the zero-install configuration (yarn files are checked into the repository). We also use Yarn Workspaces to manage the monorepo structure. This section provides instructions for installing and configuring Yarn on various operating systems.

macOS

  1. Install Node.js using Homebrew:

    Terminal window
    brew install node@20
  2. No need to install Yarn globally as the project uses Yarn from the .yarn/releases directory.

  3. Verify you can use Yarn in the project:

    Terminal window
    # After cloning the repository
    cd subnetter
    ./yarn --version

Linux (Ubuntu/Debian)

  1. Install Node.js:

    Terminal window
    # Add NodeSource repository
    curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
    # Install Node.js
    sudo apt-get install -y nodejs
  2. No need to install Yarn globally as the project uses Yarn from the .yarn/releases directory.

Linux (Fedora/RHEL)

  1. Install Node.js:

    Terminal window
    # Enable NodeSource repository
    sudo dnf module enable nodejs:20
    # Install Node.js
    sudo dnf install -y nodejs
  2. No need to install Yarn globally as the project uses Yarn from the .yarn/releases directory.

NixOS

  1. Add Node.js to your configuration:

    In your configuration.nix:

    environment.systemPackages = with pkgs; [
    nodejs_20
    ];

    Or if using home-manager, in your home.nix:

    home.packages = with pkgs; [
    nodejs_20
    ];
  2. No need to install Yarn globally as the project uses Yarn from the .yarn/releases directory.

Windows

  1. Install Node.js:

    • Download the Node.js installer from the official website
    • Run the installer and follow the installation wizard
    • Make sure to check the box for “Automatically install the necessary tools”
  2. No need to install Yarn globally as the project uses Yarn from the .yarn/releases directory.

WSL2 (Windows Subsystem for Linux)

  1. Install WSL2 if you haven’t already:

    Terminal window
    # In PowerShell (Admin)
    wsl --install
  2. Install your preferred Linux distribution from the Microsoft Store

  3. Follow the Linux installation instructions above for your specific distribution

Understanding the Yarn Zero-Install Configuration

This project uses Yarn with “Zero-Install” configuration, meaning:

  1. The .yarn/releases/yarn-sources.cjs file is checked into the repository

  2. The .yarnrc.yml file configures Yarn with these key settings:

    compressionLevel: mixed
    enableGlobalCache: false
    nodeLinker: node-modules
    yarnPath: .yarn/releases/yarn-sources.cjs
    • compressionLevel: mixed optimizes the compression of dependencies
    • nodeLinker: node-modules uses the classic node_modules resolution strategy
    • enableGlobalCache: false ensures dependencies are stored project-locally
    • yarnPath specifies the location of the Yarn binary included in the repo
  3. The package.json configures workspaces:

    "workspaces": [
    "packages/*"
    ]

This configuration provides several benefits:

  • No global Yarn installation needed: The project uses the version from .yarn/releases
  • Deterministic builds: Identical dependency installation across all environments
  • Simplified onboarding: New contributors don’t need to install Yarn separately
  • Workspace management: Packages can reference each other and share dependencies

Getting Started

Follow these steps to set up your development environment:

Terminal window
# Clone the repository
git clone https://github.com/gangster/subnetter.git
cd subnetter
# Install dependencies
./yarn install
# Build all packages
./yarn build
# Run tests to ensure everything works
./yarn test

Build Process

Subnetter uses TypeScript’s project references for a structured, efficient build process.

Understanding TypeScript Project References

Project references allow dependencies between TypeScript projects:

  1. The root tsconfig.json configures shared settings and references other packages
  2. Each package has its own tsconfig.json that extends the root configuration
  3. When you build a package, TypeScript automatically builds its dependencies first

This provides faster builds, proper encapsulation, and better organization.

Building the Project

Terminal window
# Build all packages
./yarn build
# Build individual packages
./yarn build:cidr-utils
./yarn build:core
./yarn build:cli
./yarn build:docs
# Clean build artifacts
./yarn clean

Package-Specific Build Scripts

Each package has its own build scripts defined in its package.json:

  • Core Package: Simple TypeScript compilation

    "scripts": {
    "build": "tsc -b",
    "clean": "rm -rf dist tsconfig.tsbuildinfo"
    }
  • CLI Package: TypeScript compilation plus executable script generation

    "scripts": {
    "build": "tsc -b && yarn build:bin",
    "build:bin": "node scripts/create-bin.js"
    }
  • Docs Package: TypeScript compilation plus static site generation

    "scripts": {
    "build": "yarn generate-api-docs && astro build && node postbuild.js"
    }

Build Process Flow

  1. When you run yarn build, it builds packages in the correct dependency order:

    • First the cidr-utils package is built (@subnetter/cidr-utils)
    • Then the core package is built (@subnetter/core), which depends on cidr-utils
    • Then the CLI package (@subnetter/cli), which depends on core
    • Documentation can be built separately
  2. The TypeScript compiler follows project references to ensure dependent projects are built first.

  3. Each package’s build process:

    • Compiles TypeScript to JavaScript
    • Generates type declaration files (.d.ts)
    • Creates source maps for debugging

CLI Binary Generation

The CLI package includes a special step to create an executable binary:

  1. The build:bin script runs create-bin.js which:
    • Creates a bin directory
    • Generates a subnetter executable file with a Node.js shebang
    • Sets executable permissions (chmod 755)
packages/cli/scripts/create-bin.js
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
// Define paths
const binDir = path.resolve(__dirname, "../bin");
const binFile = path.resolve(binDir, "subnetter");
// Create bin directory if it doesn't exist
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
// Create the bin file with the appropriate shebang
const binContent = `#!/usr/bin/env node
require("../dist/index.js");
`;
// Write the bin file
fs.writeFileSync(binFile, binContent);
// Make the bin file executable
fs.chmodSync(binFile, "755");
  1. The CLI package.json specifies this binary in the bin field:
"bin": {
"subnetter": "./bin/subnetter.js"
}
  1. When the package is installed globally, npm/yarn creates a symlink to this binary in the user’s PATH.

Development Mode

For active development, use the watch mode:

Terminal window
./yarn dev

This starts TypeScript in watch mode, rebuilding files as you make changes.

Development Workflow

Branch Strategy

We follow the GitHub Flow branching strategy:

  1. Create a feature branch from main
  2. Make your changes
  3. Open a pull request to main
  4. After review, merge the pull request

Commit Messages

We use Conventional Commits for commit messages. This helps generate accurate changelogs and determine version bumps.

Examples:

  • feat: add new subnet type validation
  • fix: correct CIDR overlapping detection
  • docs: update installation instructions
  • test: add test for edge case in CIDR allocation
  • refactor: simplify allocation algorithm

The commit message format is enforced by commitlint, which runs as a pre-commit hook.

Pull Request Process

  1. Ensure your code passes all tests and linting
  2. Update documentation if necessary
  3. Open a pull request with a clear description of the changes
  4. Address review comments
  5. Once approved, the pull request will be merged

Testing

Subnetter has a comprehensive testing strategy that includes multiple levels of testing.

Unit Tests

Unit tests focus on individual components and are located in the tests directory of each package.

To run unit tests:

Terminal window
yarn test # Run all tests
yarn test:coverage # Run tests with coverage reporting

To run tests for a specific package:

Terminal window
yarn workspace @subnetter/core test
yarn workspace @subnetter/cli test

End-to-End Tests

End-to-end tests verify the complete workflow and are located in the __tests__/e2e directory.

To run end-to-end tests:

Terminal window
yarn test:e2e

Test Structure

Tests are organized by package and follow a consistent structure:

  • Unit Tests: Test individual functions and classes
    • Located in packages/<package>/tests/
    • Named *.test.ts
  • End-to-End Tests: Test complete workflows
    • Located in __tests__/e2e/
    • Named *.test.ts

Testing Guidelines

  • Every feature should have corresponding tests
  • Aim for high test coverage (>90% for core functionality)
  • Use descriptive test names that indicate what is being tested
  • Use mocks sparingly and only when necessary
  • End-to-end tests should use realistic configurations

Code Style and Conventions

TypeScript and ESLint

We use TypeScript for type safety and ESLint for static code analysis. Our configuration is based on recommended settings with custom rules specified in eslint.config.js.

To lint your code:

Terminal window
yarn lint # Check for linting issues
yarn lint:fix # Automatically fix linting issues

We use TypeScript 5.8.x for the core package and 5.7.x for other packages.

Code Formatting

We use Prettier for code formatting. The configuration is in .prettierrc.json.

Naming Conventions

  • Files: Use kebab-case for filenames (e.g., cidr-allocator.ts)
  • Classes: Use PascalCase for class names (e.g., CidrAllocator)
  • Functions and Variables: Use camelCase for functions and variables (e.g., processRegions)
  • Interfaces: Use PascalCase for interfaces (e.g., Allocation)
  • Enums: Use PascalCase for enums and enum members

Documentation

  • Use JSDoc comments for all public APIs
  • Include examples in documentation when helpful
  • Keep documentation up to date with the code

Contribution Guidelines

We welcome contributions from the community! Here’s how you can contribute:

Finding Issues to Work On

  • Check the GitHub Issues for tasks
  • Look for issues labeled “good first issue” for a good starting point

Submitting Changes

  1. Fork the repository
  2. Create a feature branch from main
  3. Make your changes
  4. Ensure your code passes tests and linting
  5. Open a pull request
  6. Address review comments

Code Review Process

All code changes go through a review process:

  1. Automated checks (CI/CD pipeline)
  2. Code review by at least one maintainer
  3. Address feedback
  4. Final approval and merge

Release Process

We use semantic versioning (SemVer) for releases.

Version Bumping

Versions are bumped automatically based on commit messages:

  • fix: Patch version bump
  • feat: Minor version bump
  • feat! or fix!: Major version bump (breaking changes)

Release Workflow

  1. Merge pull requests to main
  2. The CI/CD pipeline runs tests on multiple platforms
  3. If all tests pass, the release job creates releases automatically
  4. Packages are published to GitHub Packages
  5. Release notes are generated based on commit messages

Package Publishing

Our packages are published to GitHub Packages. The package configuration includes:

"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "public"
}

Manual Release

If necessary, manual releases can be triggered:

Terminal window
npm run release

Common Development Tasks

Adding a New Feature

  1. Understand the requirements and design the feature
  2. Create a feature branch from main
  3. Implement the feature with tests
  4. Update documentation
  5. Open a pull request

Debugging

For debugging, you can use:

Terminal window
# Running with verbose output
yarn workspace @subnetter/cli develop -- generate -c config.json -o output.csv -v
# Node.js inspector
node --inspect-brk packages/cli/dist/index.js generate -c config.json

Adding a New Command

To add a new CLI command:

  1. Create a new file in packages/cli/src/commands/
  2. Implement the command using the Commander.js API
  3. Register the command in packages/cli/src/index.ts
  4. Add tests for the command
  5. Update the documentation

Extending the Core Functionality

To extend the core functionality:

  1. Identify where your feature fits in the architecture
  2. Add new interfaces or types in packages/core/src/models/
  3. Implement the functionality in the appropriate directory
  4. Export the new functionality in packages/core/src/index.ts
  5. Add tests for the new functionality
  6. Update the documentation

Advanced Topics

Performance Optimization

When optimizing for performance:

  • Use profiling tools to identify bottlenecks
  • Consider memoization for expensive calculations
  • Optimize CIDR subdivision for large allocations
  • Use efficient data structures for lookup operations

Adding Support for New Cloud Providers

To add a new cloud provider:

  1. Update the CloudProvider interface
  2. Extend the provider detection logic in getProviderForRegion()
  3. Add provider-specific AZ naming in generateAzNames()
  4. Add tests for the new provider
  5. Update documentation

Extending Output Formats

To add a new output format:

  1. Create a new file in packages/core/src/output/
  2. Implement the output generation function
  3. Export the function in packages/core/src/index.ts
  4. Add options to the CLI in packages/cli/src/index.ts
  5. Add tests for the new format

Troubleshooting

Common Build Issues

  • TypeScript Errors: Check if your TypeScript version matches the project requirements
  • Missing Dependencies: Run yarn install to ensure all dependencies are installed
  • Out-of-Memory: Increase Node.js memory limit with NODE_OPTIONS=--max_old_space_size=4096

Testing Issues

  • Failed Tests: Check the error message for details
  • Timeout Issues: Consider increasing the test timeout value
  • Coverage Issues: Ensure you have written tests for all code paths

CI/CD Pipeline

Subnetter uses GitHub Actions for continuous integration and deployment:

Workflow Overview

The main CI/CD pipeline is defined in .github/workflows/ci-cd.yml and includes:

  1. Validating Pull Requests:

    • Checks for package-lock.json files (using yarn instead)
    • Validates commit messages follow conventional commits
  2. Testing:

    • Linting
    • Building all packages
    • Running unit tests
    • Running end-to-end tests
  3. Releasing:

    • Automatic version bumping based on commit types
    • Publishing to GitHub Packages
  4. Documentation:

    • Building and deploying to GitHub Pages

Environment Configuration

The CI/CD pipeline runs on GitHub-hosted runners with the following configuration:

  • Node.js: Version 20.x
  • Operating System: Ubuntu Latest
  • Package Manager: Yarn Berry

Workflow Triggers

The workflow is triggered by:

  • Pull Requests to the main branch
  • Pushes to the main branch

Workflow Jobs

  1. validate-pr:

    • Validates PR structure and commits
    • Checks that package-lock.json is not present
    • Ensures commit messages follow conventional commit format
  2. test-pr:

    • Runs on pull requests after validation
    • Sets up Node.js 20.x and Yarn
    • Runs linting
    • Builds all packages
    • Executes unit tests and E2E tests
  3. test-main:

    • Similar to test-pr but runs on pushes to main
    • Required for the release process
  4. check-release:

    • Determines if a release is needed based on commit messages
    • Runs after tests pass on the main branch
  5. release:

    • Creates a new release if required
    • Automatically bumps version based on commit types
    • Publishes packages to GitHub Packages
  6. docs:

    • Builds and deploys documentation to GitHub Pages
    • Triggered by changes to documentation files on main

Example Workflow Configuration

name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate-pr:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# Additional steps...
test-pr:
needs: validate-pr
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# Testing steps...
# Additional jobs...

Debugging CI/CD Issues

If you encounter CI/CD pipeline failures:

  1. Check the GitHub Actions logs for detailed error information
  2. Ensure your local environment matches the CI environment (Node.js 20.x)
  3. Run tests locally with yarn test to reproduce issues
  4. For release failures, check commit message formats and versioning

Troubleshooting

Common Issues and Solutions

Build Errors

Problem: TypeScript build errors in dependencies. Solution:

Terminal window
# Clean build artifacts and rebuild
./yarn clean
./yarn build

Problem: Unable to resolve dependencies between packages. Solution: Check package.json references and ensure the dependency tree is correct.

Test Failures

Problem: Tests pass locally but fail in CI. Solution: Ensure you’re using Node.js 20.x locally to match the CI environment.

Problem: Flaky tests that sometimes pass, sometimes fail. Solution: Investigate race conditions or timing issues in the tests.

Linting Issues

Problem: ESLint or Prettier errors. Solution:

Terminal window
# Fix linting issues automatically where possible
./yarn lint:fix

Release Issues

Problem: Release not triggered despite merged PR. Solution: Ensure at least one commit follows the conventional commit format that triggers a version bump.

Additional Resources

Working with the CIDR Utilities Package

The @subnetter/cidr-utils package provides low-level utilities for IP address and CIDR manipulation. This package can be used both internally (by the core package) or as a standalone utility library for other projects.

Architecture and Design

The CIDR utils package was designed with the following principles:

  1. Zero dependencies: Pure JavaScript/TypeScript implementation without external dependencies
  2. TypeScript-first: Comprehensive type definitions for all functions and data structures
  3. Comprehensive API: Complete set of utilities for all common CIDR operations
  4. Modular design: Each file has a single responsibility and a clean API
  5. Thorough testing: Extensive unit test coverage, including edge cases

Key Components

  • IP Address Manipulation (ip.ts): Functions to convert between string, numeric, and octet representations

    • ipv4ToNumber: Convert an IPv4 address string to its numeric representation
    • numberToIpv4: Convert a numeric representation to an IPv4 address string
    • createIpAddress: Create an IP address object with various formats
  • CIDR Parsing (parser.ts): Functions to parse and normalize CIDR notation

    • parseIpv4Cidr: Parse a CIDR string into its components (address and prefix length)
    • normalizeCidr: Normalize a CIDR string to its canonical form
  • CIDR Validation (validator.ts): Functions to validate CIDR notation

    • isValidIpv4Cidr: Check if a string is a valid CIDR
    • validateIpv4Cidr: Validate a CIDR with detailed error reporting
  • Subnet Calculations (calculator.ts): Functions for subnet calculations

    • calculateNetworkAddress: Calculate the network address of a CIDR
    • calculateBroadcastAddress: Calculate the broadcast address of a CIDR
    • calculateUsableIps: Calculate the number of usable IPs in a CIDR
    • isIpInCidr: Check if an IP address is within a CIDR range
    • doCidrsOverlap: Check if two CIDRs overlap
  • Subnet Allocation (subnet.ts): Functions for subnet allocation

    • subdivideIpv4Cidr: Divide a CIDR into smaller subnets
    • findNextAvailableCidr: Find the next available CIDR block in a range

Using the CIDR Utils Package

You can use the CIDR utils package directly in your code:

// Import specific functions
import {
isValidIpv4Cidr,
calculateUsableIps,
ipv4ToNumber
} from '@subnetter/cidr-utils';
// Validate a CIDR
const validCidr = isValidIpv4Cidr('10.0.0.0/24'); // true
// Calculate usable IPs
const usableIps = calculateUsableIps('10.0.0.0/24'); // 254
// Convert IP to number
const ipNumber = ipv4ToNumber('10.0.0.1'); // 167772161

Extending CIDR Utils

When adding new functionality to the CIDR utils package:

  1. Identify the appropriate file based on the type of functionality
  2. Add well-documented, strongly-typed functions
  3. Write comprehensive tests, including edge cases
  4. Export the new functions in the index.ts file
  5. Update dependencies (if needed) in the core and CLI packages

Build Considerations

The cidr-utils package sits at the bottom of the dependency tree, so it needs to be built first:

Terminal window
# Build cidr-utils first
./yarn build:cidr-utils
# Then build dependent packages
./yarn build:core
./yarn build:cli

The CI/CD pipeline is configured to build packages in the correct order automatically.

Core Allocation System

The heart of Subnetter is its CIDR allocation system, which is built around two main classes: HierarchicalAllocator and ContiguousAllocator. These classes work together to provide a deterministic, hierarchical allocation of CIDR blocks.

ContiguousAllocator

The ContiguousAllocator is responsible for allocating CIDR blocks sequentially from a base CIDR block. It ensures that all allocations are contiguous and do not overlap.

Design Principles

  1. Deterministic Allocation: The allocator always produces the same output for the same input.
  2. Sequential Allocation: CIDR blocks are allocated in sequence, without gaps.
  3. Validation: The allocator validates that requested allocations fit within the available space.

Implementation Details

export class ContiguousAllocator {
private baseCidr: string;
private currentIp: bigint;
private endIp: bigint;
private allocated: string[] = [];
constructor(baseCidr: string) {
validateCidr(baseCidr);
this.baseCidr = baseCidr;
// Calculate the start and end IP addresses for the base CIDR
const [baseIp, prefixLength] = this.baseCidr.split("/");
const ipInt = ipToInt(baseIp);
const maskLength = parseInt(prefixLength, 10);
this.currentIp = ipInt;
this.endIp = ipInt + (1n << BigInt(32 - maskLength)) - 1n;
}
allocate(prefixLength: number): string {
// Calculate required size
const size = 1n << BigInt(32 - prefixLength);
// Align the current IP to the required boundary
const alignedIp = this.currentIp + ((size - (this.currentIp % size)) % size);
// Check if there's enough space
if (alignedIp + size - 1n > this.endIp) {
throw new Error(`Not enough space left for allocation with prefix /${prefixLength}`);
}
// Create the CIDR
const cidr = `${intToIp(alignedIp)}/${prefixLength}`;
// Update the current IP
this.currentIp = alignedIp + size;
// Record the allocation
this.allocated.push(cidr);
return cidr;
}
// ... other methods (reset, getAvailableSpace, getAllocated) ...
}

Key Methods

  • allocate(prefixLength): Allocates a CIDR block with the specified prefix length
  • reset(): Resets the allocator to its initial state
  • getAvailableSpace(): Returns the amount of space still available
  • getAllocated(): Returns all allocated CIDR blocks

HierarchicalAllocator

The HierarchicalAllocator builds on the ContiguousAllocator to provide a hierarchical allocation strategy that follows the account > region > availability zone > subnet hierarchy.

Design Principles

  1. Hierarchical Structure: Allocations follow a strict hierarchy
  2. Override Support: Allows overriding allocations at any level
  3. Configuration-Driven: Allocations are driven by a configuration object

Implementation Details

export class HierarchicalAllocator {
private config: Config;
private rootAllocator: ContiguousAllocator;
private allocations: Allocation[] = [];
constructor(config: Config) {
this.config = config;
validateCidr(config.baseCidr);
this.rootAllocator = new ContiguousAllocator(config.baseCidr);
}
generateAllocations(): Allocation[] {
// Reset state
this.allocations = [];
this.rootAllocator.reset();
// Process each account
for (const account of this.config.accounts) {
this.processAccount(account);
}
return this.allocations;
}
private processAccount(account: Account): void {
// Check if the account has a base CIDR override
let accountAllocator: ContiguousAllocator;
if (account.baseCidr) {
// Use the override
accountAllocator = new ContiguousAllocator(account.baseCidr);
} else {
// Allocate from the root
const accountCidr = this.rootAllocator.allocate(this.config.prefixLengths.account);
accountAllocator = new ContiguousAllocator(accountCidr);
}
// Process each cloud provider for this account
for (const [cloudKey, cloud] of Object.entries(account.clouds)) {
this.processCloud(account, cloudKey, cloud, accountAllocator);
}
}
private processCloud(account: Account, cloudKey: string, cloud: CloudConfig, accountAllocator: ContiguousAllocator): void {
// Similar logic for cloud, region, AZ, and subnet levels...
// ... see actual implementation for full details
}
}

Key Methods

  • generateAllocations(): Generates all allocations based on the configuration
  • processAccount(account): Processes an account and its children
  • processCloud(account, cloudKey, cloud, allocator): Processes a cloud provider
  • processRegion(account, cloudKey, cloud, region, allocator): Processes a region
  • processAZ(account, cloudKey, cloud, region, az, allocator): Processes an availability zone
  • addAllocation(allocation): Adds an allocation to the results

Integration and Usage

The allocation system is integrated into the Subnetter CLI and can be used programmatically:

import { HierarchicalAllocator } from '@subnetter/core';
// Create a configuration
const config = {
baseCidr: '10.0.0.0/8',
prefixLengths: {
account: 16,
region: 20,
az: 24
},
accounts: [
// ... account configuration
],
subnetTypes: {
// ... subnet types
}
};
// Create the allocator
const allocator = new HierarchicalAllocator(config);
// Generate allocations
const allocations = allocator.generateAllocations();
// Use the allocations
console.log(allocations);

Extending the Allocation System

The allocation system is designed to be extensible. Here are some ways to extend it:

Custom Allocation Strategies

You can create custom allocation strategies by implementing the allocator interface:

interface IAllocator {
allocate(prefixLength: number): string;
reset(): void;
getAvailableSpace(): bigint;
getAllocated(): string[];
}
// Example: An allocator that allocates from the end of the CIDR block
export class ReverseAllocator implements IAllocator {
private baseCidr: string;
private currentIp: bigint;
private startIp: bigint;
private allocated: string[] = [];
constructor(baseCidr: string) {
validateCidr(baseCidr);
this.baseCidr = baseCidr;
const [baseIp, prefixLength] = this.baseCidr.split("/");
const ipInt = ipToInt(baseIp);
const maskLength = parseInt(prefixLength, 10);
this.startIp = ipInt;
this.currentIp = ipInt + (1n << BigInt(32 - maskLength)) - 1n;
}
allocate(prefixLength: number): string {
// Allocate from the end of the CIDR block
// ... implementation details
}
// ... other methods
}

Custom Hierarchy Levels

The hierarchical allocator supports the default hierarchy (account > cloud > region > AZ > subnet), but you can create a custom hierarchy by implementing a new allocator:

export class CustomHierarchicalAllocator {
// ... similar to HierarchicalAllocator, but with custom hierarchy levels
generateAllocations(): Allocation[] {
// Process custom hierarchy levels
// ...
}
}

Integration with Other Systems

The allocation system can be integrated with other systems, such as Terraform or CloudFormation:

export class TerraformGenerator {
private allocations: Allocation[];
constructor(allocations: Allocation[]) {
this.allocations = allocations;
}
generateTerraform(): string {
// Generate Terraform code from allocations
// ...
}
}

Testing Allocation Algorithms

The allocation system is extensively tested using Jest. Here’s an example of testing the ContiguousAllocator:

describe('ContiguousAllocator', () => {
it('should allocate CIDR blocks sequentially', () => {
const allocator = new ContiguousAllocator('10.0.0.0/16');
expect(allocator.allocate(24)).toBe('10.0.0.0/24');
expect(allocator.allocate(24)).toBe('10.0.1.0/24');
expect(allocator.allocate(24)).toBe('10.0.2.0/24');
});
it('should throw an error when out of space', () => {
const allocator = new ContiguousAllocator('10.0.0.0/24');
expect(allocator.allocate(25)).toBe('10.0.0.0/25');
expect(allocator.allocate(25)).toBe('10.0.0.128/25');
expect(() => allocator.allocate(25)).toThrow();
});
});

Performance Considerations

The allocation system is designed to be efficient and performant. Here are some considerations:

  1. Memory Usage: The allocator stores only the base CIDR, current position, and allocated CIDRs
  2. Computational Complexity: Allocation operations are O(1) in time complexity
  3. Scaling: The system can handle thousands of allocations efficiently

For very large configurations, consider breaking them down into smaller configurations or using a streaming approach to process allocations in batches.