pipeline: add testing improvements for branch-based workflows
Some checks failed
Run nix flake check / flake-check (push) Has been cancelled
Some checks failed
Run nix flake check / flake-check (push) Has been cancelled
Implement dual improvements to enable efficient testing of pipeline changes without polluting master branch: 1. Add --force flag to create-host script - Skip hostname/IP uniqueness validation - Overwrite existing host configurations - Update entries in flake.nix and terraform/vms.tf (no duplicates) - Useful for iterating on configurations during testing 2. Add branch support to bootstrap mechanism - Bootstrap service reads NIXOS_FLAKE_BRANCH environment variable - Defaults to master if not set - Uses branch in git URL via ?ref= parameter - Service loads environment from /etc/environment 3. Add cloud-init disk support for branch configuration - VMs can specify flake_branch field in terraform/vms.tf - Automatically generates cloud-init snippet setting NIXOS_FLAKE_BRANCH - Uploads snippet to Proxmox via SSH - Production VMs omit flake_branch and use master 4. Update documentation - Document --force flag usage in create-host README - Add branch testing examples in terraform README - Update TODO.md with testing workflow - Add .generated/ to gitignore Testing workflow: Create feature branch, set flake_branch in VM definition, deploy with terraform, iterate with --force flag, clean up before merging. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,23 @@ python -m scripts.create_host.create_host create \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
### Force Mode (Regenerate Existing Configuration)
|
||||
|
||||
Overwrite an existing host configuration (useful for testing):
|
||||
|
||||
```bash
|
||||
python -m scripts.create_host.create_host create \
|
||||
--hostname test01 \
|
||||
--ip 10.69.13.50/24 \
|
||||
--force
|
||||
```
|
||||
|
||||
This mode:
|
||||
- Skips hostname and IP uniqueness validation
|
||||
- Overwrites files in `hosts/<hostname>/`
|
||||
- Updates existing entries in `flake.nix` and `terraform/vms.tf` (doesn't duplicate)
|
||||
- Useful for iterating on configuration templates during testing
|
||||
|
||||
### Options
|
||||
|
||||
- `--hostname` (required): Hostname for the new host
|
||||
@@ -73,6 +90,10 @@ python -m scripts.create_host.create_host create \
|
||||
|
||||
- `--dry-run` (flag): Preview changes without creating files
|
||||
|
||||
- `--force` (flag): Overwrite existing host configuration
|
||||
- Skips uniqueness validation
|
||||
- Updates existing entries instead of creating duplicates
|
||||
|
||||
## What It Does
|
||||
|
||||
The tool performs the following actions:
|
||||
|
||||
@@ -45,6 +45,7 @@ def main(
|
||||
memory: int = typer.Option(2048, "--memory", help="Memory in MB"),
|
||||
disk: str = typer.Option("20G", "--disk", help="Disk size (e.g., 20G, 50G, 100G)"),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes without creating files"),
|
||||
force: bool = typer.Option(False, "--force", help="Overwrite existing host configuration"),
|
||||
) -> None:
|
||||
"""
|
||||
Create a new NixOS host configuration.
|
||||
@@ -75,11 +76,20 @@ def main(
|
||||
|
||||
config.validate()
|
||||
validate_hostname_format(hostname)
|
||||
validate_hostname_unique(hostname, repo_root)
|
||||
|
||||
# Skip uniqueness checks in force mode
|
||||
if not force:
|
||||
validate_hostname_unique(hostname, repo_root)
|
||||
if ip:
|
||||
validate_ip_unique(ip, repo_root)
|
||||
else:
|
||||
# Check if we're actually overwriting something
|
||||
host_dir = repo_root / "hosts" / hostname
|
||||
if host_dir.exists():
|
||||
console.print(f"[yellow]⚠[/yellow] Updating existing host configuration for {hostname}")
|
||||
|
||||
if ip:
|
||||
validate_ip_subnet(ip)
|
||||
validate_ip_unique(ip, repo_root)
|
||||
|
||||
console.print("[green]✓[/green] All validations passed\n")
|
||||
|
||||
@@ -96,13 +106,14 @@ def main(
|
||||
console.print("\n[bold blue]Generating host configuration...[/bold blue]")
|
||||
|
||||
generate_host_files(config, repo_root)
|
||||
console.print(f"[green]✓[/green] Created hosts/{hostname}/default.nix")
|
||||
console.print(f"[green]✓[/green] Created hosts/{hostname}/configuration.nix")
|
||||
action = "Updated" if force else "Created"
|
||||
console.print(f"[green]✓[/green] {action} hosts/{hostname}/default.nix")
|
||||
console.print(f"[green]✓[/green] {action} hosts/{hostname}/configuration.nix")
|
||||
|
||||
update_flake_nix(config, repo_root)
|
||||
update_flake_nix(config, repo_root, force=force)
|
||||
console.print("[green]✓[/green] Updated flake.nix")
|
||||
|
||||
update_terraform_vms(config, repo_root)
|
||||
update_terraform_vms(config, repo_root, force=force)
|
||||
console.print("[green]✓[/green] Updated terraform/vms.tf")
|
||||
|
||||
# Success message
|
||||
|
||||
@@ -6,21 +6,18 @@ from pathlib import Path
|
||||
from models import HostConfig
|
||||
|
||||
|
||||
def update_flake_nix(config: HostConfig, repo_root: Path) -> None:
|
||||
def update_flake_nix(config: HostConfig, repo_root: Path, force: bool = False) -> None:
|
||||
"""
|
||||
Add new host entry to flake.nix nixosConfigurations.
|
||||
Add or update host entry in flake.nix nixosConfigurations.
|
||||
|
||||
Args:
|
||||
config: Host configuration
|
||||
repo_root: Path to repository root
|
||||
force: If True, replace existing entry; if False, insert new entry
|
||||
"""
|
||||
flake_path = repo_root / "flake.nix"
|
||||
content = flake_path.read_text()
|
||||
|
||||
# Find the closing of nixosConfigurations block
|
||||
# Pattern: " };\n packages ="
|
||||
pattern = r"( \};)\n( packages =)"
|
||||
|
||||
# Create new entry
|
||||
new_entry = f""" {config.hostname} = nixpkgs.lib.nixosSystem {{
|
||||
inherit system;
|
||||
@@ -40,35 +37,47 @@ def update_flake_nix(config: HostConfig, repo_root: Path) -> None:
|
||||
}};
|
||||
"""
|
||||
|
||||
# Insert new entry before closing brace
|
||||
replacement = rf"\g<1>\n{new_entry}\g<2>"
|
||||
# Check if hostname already exists
|
||||
hostname_pattern = rf"^ {re.escape(config.hostname)} = nixpkgs\.lib\.nixosSystem"
|
||||
existing_match = re.search(hostname_pattern, content, re.MULTILINE)
|
||||
|
||||
new_content, count = re.subn(pattern, replacement, content)
|
||||
if existing_match and force:
|
||||
# Replace existing entry
|
||||
# Match the entire block from "hostname = " to "};"
|
||||
replace_pattern = rf"^ {re.escape(config.hostname)} = nixpkgs\.lib\.nixosSystem \{{.*?^ \}};\n"
|
||||
new_content, count = re.subn(replace_pattern, new_entry, content, flags=re.MULTILINE | re.DOTALL)
|
||||
|
||||
if count == 0:
|
||||
raise ValueError(
|
||||
"Could not find insertion point in flake.nix. "
|
||||
"Looking for pattern: ' };\\n devShells ='"
|
||||
)
|
||||
if count == 0:
|
||||
raise ValueError(f"Could not find existing entry for {config.hostname} in flake.nix")
|
||||
else:
|
||||
# Insert new entry before closing brace
|
||||
# Pattern: " };\n packages ="
|
||||
pattern = r"( \};)\n( packages =)"
|
||||
replacement = rf"\g<1>\n{new_entry}\g<2>"
|
||||
|
||||
new_content, count = re.subn(pattern, replacement, content)
|
||||
|
||||
if count == 0:
|
||||
raise ValueError(
|
||||
"Could not find insertion point in flake.nix. "
|
||||
"Looking for pattern: ' };\\n packages ='"
|
||||
)
|
||||
|
||||
flake_path.write_text(new_content)
|
||||
|
||||
|
||||
def update_terraform_vms(config: HostConfig, repo_root: Path) -> None:
|
||||
def update_terraform_vms(config: HostConfig, repo_root: Path, force: bool = False) -> None:
|
||||
"""
|
||||
Add new VM entry to terraform/vms.tf locals.vms map.
|
||||
Add or update VM entry in terraform/vms.tf locals.vms map.
|
||||
|
||||
Args:
|
||||
config: Host configuration
|
||||
repo_root: Path to repository root
|
||||
force: If True, replace existing entry; if False, insert new entry
|
||||
"""
|
||||
terraform_path = repo_root / "terraform" / "vms.tf"
|
||||
content = terraform_path.read_text()
|
||||
|
||||
# Find the closing of locals.vms block
|
||||
# Pattern: " }\n\n # Compute VM configurations"
|
||||
pattern = r"( \})\n\n( # Compute VM configurations)"
|
||||
|
||||
# Create new entry based on whether we have static IP or DHCP
|
||||
if config.is_static_ip:
|
||||
new_entry = f''' "{config.hostname}" = {{
|
||||
@@ -86,15 +95,30 @@ def update_terraform_vms(config: HostConfig, repo_root: Path) -> None:
|
||||
}}
|
||||
'''
|
||||
|
||||
# Insert new entry before closing brace
|
||||
replacement = rf"{new_entry}\g<1>\n\n\g<2>"
|
||||
# Check if hostname already exists
|
||||
hostname_pattern = rf'^\s+"{re.escape(config.hostname)}" = \{{'
|
||||
existing_match = re.search(hostname_pattern, content, re.MULTILINE)
|
||||
|
||||
new_content, count = re.subn(pattern, replacement, content)
|
||||
if existing_match and force:
|
||||
# Replace existing entry
|
||||
# Match the entire block from "hostname" = { to }
|
||||
replace_pattern = rf'^\s+"{re.escape(config.hostname)}" = \{{.*?^\s+\}}\n'
|
||||
new_content, count = re.subn(replace_pattern, new_entry, content, flags=re.MULTILINE | re.DOTALL)
|
||||
|
||||
if count == 0:
|
||||
raise ValueError(
|
||||
"Could not find insertion point in terraform/vms.tf. "
|
||||
"Looking for pattern: ' }\\n\\n # Compute VM configurations'"
|
||||
)
|
||||
if count == 0:
|
||||
raise ValueError(f"Could not find existing entry for {config.hostname} in terraform/vms.tf")
|
||||
else:
|
||||
# Insert new entry before closing brace
|
||||
# Pattern: " }\n\n # Compute VM configurations"
|
||||
pattern = r"( \})\n\n( # Compute VM configurations)"
|
||||
replacement = rf"{new_entry}\g<1>\n\n\g<2>"
|
||||
|
||||
new_content, count = re.subn(pattern, replacement, content)
|
||||
|
||||
if count == 0:
|
||||
raise ValueError(
|
||||
"Could not find insertion point in terraform/vms.tf. "
|
||||
"Looking for pattern: ' }\\n\\n # Compute VM configurations'"
|
||||
)
|
||||
|
||||
terraform_path.write_text(new_content)
|
||||
|
||||
Reference in New Issue
Block a user