Compare commits

...

6 Commits

Author SHA1 Message Date
eric
99658b27dc feat: rework to modular 2026-04-10 17:25:08 +02:00
eric
28dad81816 fix: remove py 2026-04-02 23:03:19 +02:00
eric
1de34c1869 feat: move things around 2026-03-18 17:41:10 +01:00
eric
f558ab4ba9 feat: add spinner 2026-03-18 13:28:51 +01:00
eric
f150afec0a fix: probe correct value 2026-03-18 03:21:10 +01:00
eric
9e0eb5b583 fix: correct field from bao 2026-03-18 03:15:11 +01:00
53 changed files with 703 additions and 2506 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
bootstrap/

467
README.md
View File

@@ -1,413 +1,132 @@
# nix-nodeiwest # nix-nodeiwest
NixOS flake for NodeiWest VPS provisioning and ongoing deployment. Composable company Nix for NodeiWest workstations and project shells.
This repo currently provisions NixOS hosts with: This repo is now structured as a shared SDK:
- the `nodeiwest` employee helper CLI for safe provisioning - `modules/` holds focused Home Manager building blocks
- shared base config in `modules/nixos/common.nix` - `profiles/` bundles those modules into opinionated employee entrypoints
- Tailscale bootstrap via OpenBao AppRole in `modules/nixos/tailscale-init.nix` - `shells/` exposes reusable flake dev shells for project repos
- Home Manager profile in `modules/home.nix` - `systems/` adapts the shared modules into Darwin or standalone Linux Home Manager configs
- disk partitioning via `disko` - `lib/` holds the small helpers that keep composition consistent
- deployment via `colmena` - `templates/` bootstraps downstream user flakes
## Current Model It does not define users or machines directly. Downstream flakes decide who uses which profile.
- Employees should use `nodeiwest` as the supported provisioning interface ## Layout
- New machines are installed with `nixos-anywhere`
- Ongoing changes are deployed with `colmena`
- Hosts authenticate to OpenBao as clients
- Tailscale auth keys are fetched from OpenBao namespace `it`, path `tailscale`, field `auth_key`
- Public SSH must work independently of Tailscale for first access and recovery
## Repo Layout
```text ```text
flake.nix .
hosts/ ├── flake.nix
vps[X]/ ├── lib/
configuration.nix ├── modules/
disko.nix ├── base/
hardware-configuration.nix ├── dev/
modules/ │ ├── optional/
home.nix ├── roles/
helpers/ ├── secrets/
home.nix └── services/
nixos/ ├── profiles/
common.nix ├── shells/
tailscale-init.nix ├── systems/
pkgs/ └── templates/
helpers/
cli.py
templates/
``` ```
## Recommended Workflow ## Flake Interface
The supported employee path is the `nodeiwest` CLI. Primary outputs:
It is exported from the root flake as `.#nodeiwest-helper` and installed by the shared Home Manager profile. You can also run it ad hoc with: - `homeManagerModules.base.*`: low-level base modules
- `homeManagerModules.dev.*`: language and workflow modules
- `homeManagerModules.roles.*`: reusable role bundles
- `homeManagerModules.profiles.*`: ready-made employee profiles
- `homeManagerModules.default`: compatibility shim for the old default home module
- `lib.mkSystem`: chooses the Darwin or Linux adapter for a downstream flake
- `lib.shells.*`: shell factories for repo-local dev environments
- `devShells.<system>.*`: ready-to-use company shells
- `templates.user-flake`: starter personal flake
```bash ## Workstation Consumption
nix run .#nodeiwest-helper -- --help
```
Recommended sequence for a new VPS: Downstream user flakes own the actual machine definitions. They consume profiles from this repo:
### 1. Probe The Live Host
```bash
nodeiwest host probe --ip <ip>
```
This validates SSH reachability and derives the boot mode, root device, primary disk candidate, and swap facts from the live machine.
### 2. Scaffold The Host Files
Dry-run first:
```bash
nodeiwest host init --name <name> --ip <ip>
```
Write after reviewing the plan:
```bash
nodeiwest host init --name <name> --ip <ip> --apply
```
This command:
- probes the host unless you override disk or boot mode
- creates or updates `hosts/<name>/configuration.nix`
- creates or updates `hosts/<name>/disko.nix`
- creates `hosts/<name>/hardware-configuration.nix` as a placeholder if needed
- prints the exact `flake.nix` snippets still required for `nixosConfigurations` and `colmena`
### 3. Create The OpenBao Bootstrap Material
Dry-run first:
```bash
nodeiwest openbao init-host --name <name>
```
Apply after reviewing the policy and AppRole plan:
```bash
nodeiwest openbao init-host --name <name> --apply
```
This verifies your existing `bao` login, creates the host policy and AppRole, and writes:
- `bootstrap/var/lib/nodeiwest/openbao-approle-role-id`
- `bootstrap/var/lib/nodeiwest/openbao-approle-secret-id`
### 4. Plan Or Run The Install
```bash
nodeiwest install plan --name <name>
nodeiwest install run --name <name> --apply
```
`install plan` validates the generated host files and bootstrap files, then prints the exact `nixos-anywhere` command. `install run` re-validates, asks for confirmation, and executes that command.
### 5. Verify First Boot And Colmena Readiness
```bash
nodeiwest verify host --name <name> --ip <ip>
nodeiwest colmena plan --name <name>
```
`verify host` summarizes the first-boot OpenBao and Tailscale services over SSH. `colmena plan` confirms the deploy target or prints the exact missing host stanza.
## Manual Flow (Fallback / Advanced)
This is the underlying sequence that `nodeiwest` automates. Keep it as the fallback path for unsupported host layouts or when you intentionally want to run the raw commands yourself.
### 1. Prepare The Host Entry
Create a new directory under `hosts/<name>/` with:
- `configuration.nix`
- `disko.nix`
- `hardware-configuration.nix`
`configuration.nix` should import both `disko.nix` and `hardware-configuration.nix`.
Example:
```nix ```nix
{ lib, ... }:
{ {
imports = [ inputs.company.url = "git+ssh://git@git.dgren.dev/employees/company-nix.git";
./disko.nix
./hardware-configuration.nix
];
networking.hostName = "vps1"; outputs = { company, ... }: {
networking.useDHCP = lib.mkDefault true; darwinConfigurations.eric = company.lib.mkSystem {
target = "darwin";
time.timeZone = "UTC"; system = "aarch64-darwin";
username = "eric";
boot.loader.efi.canTouchEfiVariables = true; homeDirectory = "/Users/eric";
boot.loader.grub = { modules = [
enable = true; company.homeManagerModules.profiles.frontend
efiSupport = true; ];
device = "nodev"; };
}; };
nodeiwest.ssh.userCAPublicKeys = [
"ssh-ed25519 AAAA... openbao-user-ca"
];
nodeiwest.tailscale.openbao.enable = true;
system.stateVersion = "25.05";
} }
``` ```
### 2. Add The Host To `flake.nix` For Linux Home Manager:
Add the host to: ```nix
{
inputs.company.url = "git+ssh://git@git.dgren.dev/employees/company-nix.git";
- `nixosConfigurations` outputs = { company, ... }: {
- `colmena` homeConfigurations."eric@work" = company.lib.mkSystem {
target = "linux";
For `colmena`, set: system = "x86_64-linux";
username = "eric";
- `deployment.targetHost` homeDirectory = "/home/eric";
- `deployment.targetUser = "root"` modules = [
- tags as needed company.homeManagerModules.profiles.backend
];
## Discover Disk And Boot Facts };
};
Before writing `disko.nix`, inspect the current VPS over SSH:
```bash
ssh root@<ip> 'lsblk -o NAME,SIZE,TYPE,MODEL,FSTYPE,PTTYPE,MOUNTPOINTS'
ssh root@<ip> 'test -d /sys/firmware/efi && echo UEFI || echo BIOS'
ssh root@<ip> 'findmnt -no SOURCE /'
ssh root@<ip> 'cat /proc/swaps'
```
Use that output to decide:
- disk device name: `/dev/sda`, `/dev/vda`, `/dev/nvme0n1`, etc.
- boot mode: UEFI or BIOS
- partition layout you want `disko` to create
`hosts/vps1/disko.nix` currently assumes:
- GPT
- `/dev/sda`
- UEFI
- ext4 root
- swap partition
Do not install blindly if those assumptions are wrong.
## Generate `hardware-configuration.nix`
`hardware-configuration.nix` is generated during install with `nixos-anywhere`.
The repo path is passed directly to the install command:
```bash
--generate-hardware-config nixos-generate-config ./hosts/<name>/hardware-configuration.nix
```
That generated file should remain tracked in Git after install.
## OpenBao Setup For Tailscale
Each host gets its own AppRole.
The host uses:
- OpenBao address: `https://secrets.api.nodeiwest.se`
- namespace: `it`
- auth mount: `auth/approle`
- secret path: `tailscale`
- field: `auth_key`
The host stores:
- `/var/lib/nodeiwest/openbao-approle-role-id`
- `/var/lib/nodeiwest/openbao-approle-secret-id`
The rendered Tailscale auth key lives at:
- `/run/nodeiwest/tailscale-auth-key`
### Create A Policy
Create a minimal read-only policy for the Tailscale secret.
If the secret is accessible as:
```bash
BAO_NAMESPACE=it bao kv get tailscale
```
then create the matching read policy for that mount.
Example shape for a KV v2 mount named `kv`:
```hcl
path "kv/data/tailscale" {
capabilities = ["read"]
} }
``` ```
Write it from your machine: ## Project Shell Consumption
```bash Project repos should keep their own flake and compose shells from this repo instead of outsourcing project ownership here.
export BAO_ADDR=https://secrets.api.nodeiwest.se
export BAO_NAMESPACE=it
bao policy write tailscale-vps1 ./tailscale-vps1-policy.hcl Use the ready-made shell directly:
```nix
{
inputs.company.url = "git+ssh://git@git.dgren.dev/employees/company-nix.git";
outputs = { nixpkgs, company, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in
{
devShells.${system}.default = company.lib.shells.node {
inherit pkgs;
extraPackages = [ pkgs.ffmpeg ];
};
};
}
``` ```
Adjust the path to match your actual OpenBao KV mount. Or extend the published company shell in place:
### Create The AppRole ```nix
devShells.${system}.default = pkgs.mkShell {
Create one AppRole per host. inputsFrom = [ company.devShells.${system}.node ];
packages = [ pkgs.ffmpeg ];
Example for `vps1`: };
```bash
bao write auth/approle/role/tailscale-vps1 \
token_policies=tailscale-vps1 \
token_ttl=1h \
token_max_ttl=24h \
token_num_uses=0 \
secret_id_num_uses=0
``` ```
### Generate Bootstrap Credentials ## Template
Create a temporary bootstrap directory on your machine: Bootstrap a personal flake with:
```bash ```bash
mkdir -p bootstrap/var/lib/nodeiwest nix flake init -t .#user-flake
``` ```
Write the AppRole credentials into it: That template is intentionally small. Add machine-specific modules in the personal repo, not here.
```bash
bao read -field=role_id auth/approle/role/tailscale-vps1/role-id \
> bootstrap/var/lib/nodeiwest/openbao-approle-role-id
bao write -f -field=secret_id auth/approle/role/tailscale-vps1/secret-id \
> bootstrap/var/lib/nodeiwest/openbao-approle-secret-id
chmod 0400 bootstrap/var/lib/nodeiwest/openbao-approle-role-id
chmod 0400 bootstrap/var/lib/nodeiwest/openbao-approle-secret-id
```
These files are install-time bootstrap material. They are not stored in Git.
## Install With `nixos-anywhere`
Install from your machine:
```bash
nix run github:nix-community/nixos-anywhere -- \
--extra-files ./bootstrap \
--copy-host-keys \
--generate-hardware-config nixos-generate-config ./hosts/vps1/hardware-configuration.nix \
--flake .#vps1 \
root@100.101.167.118
```
What this does:
- wipes the target disk according to `hosts/vps1/disko.nix`
- installs NixOS with `.#vps1`
- copies the AppRole bootstrap files into `/var/lib/nodeiwest`
- generates `hosts/vps1/hardware-configuration.nix`
Important:
- this destroys the existing OS on the target
- take provider snapshots and application backups first
- the target SSH host keys may change after install
## First Boot Behavior
On first boot:
1. `vault-agent-tailscale.service` starts using `pkgs.openbao`
2. it authenticates to OpenBao with AppRole
3. it renders `auth_key` from `it/tailscale` to `/run/nodeiwest/tailscale-auth-key`
4. `nodeiwest-tailscale-authkey-ready.service` waits until that file exists
5. `tailscaled-autoconnect.service` uses that file and runs `tailscale up --ssh`
Public SSH remains the recovery path if OpenBao or Tailscale bootstrap fails.
## Verify After Install
SSH to the host over the public IP first.
Check:
```bash
systemctl status vault-agent-tailscale
systemctl status nodeiwest-tailscale-authkey-ready
systemctl status tailscaled-autoconnect
ls -l /var/lib/nodeiwest
ls -l /run/nodeiwest/tailscale-auth-key
tailscale status
```
If Tailscale bootstrap fails, inspect logs:
```bash
journalctl -u vault-agent-tailscale -b
journalctl -u nodeiwest-tailscale-authkey-ready -b
journalctl -u tailscaled-autoconnect -b
```
Typical causes:
- wrong AppRole credentials
- wrong OpenBao policy
- wrong secret path
- wrong KV mount path
- `auth_key` field missing in the secret
## Deploy Changes After Install
Once the host is installed and reachable, use Colmena:
```bash
nix run .#colmena -- apply --on vps1
```
## Rotating The AppRole SecretID
To rotate the machine credential:
1. generate a new `secret_id` from your machine
2. replace `/var/lib/nodeiwest/openbao-approle-secret-id` on the host
3. restart the agent
Example:
```bash
bao write -f -field=secret_id auth/approle/role/tailscale-vps1/secret-id > new-secret-id
scp new-secret-id root@100.101.167.118:/var/lib/nodeiwest/openbao-approle-secret-id
ssh root@100.101.167.118 'chmod 0400 /var/lib/nodeiwest/openbao-approle-secret-id && systemctl restart vault-agent-tailscale tailscaled-autoconnect'
rm -f new-secret-id
```
## Recovery Notes
- Tailscale is additive. It should not be your only access path.
- Public SSH on port `22` must remain available for first access and recovery.
- OpenBao SSH CA auth is separate from Tailscale bootstrap.
- If a machine fails to join the tailnet, recover via public SSH or provider console.

139
flake.lock generated
View File

@@ -1,78 +1,5 @@
{ {
"nodes": { "nodes": {
"colmena": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": "nixpkgs",
"stable": "stable"
},
"locked": {
"lastModified": 1762034856,
"narHash": "sha256-QVey3iP3UEoiFVXgypyjTvCrsIlA4ecx6Acaz5C8/PQ=",
"owner": "zhaofengli",
"repo": "colmena",
"rev": "349b035a5027f23d88eeb3bc41085d7ee29f18ed",
"type": "github"
},
"original": {
"owner": "zhaofengli",
"repo": "colmena",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773506317,
"narHash": "sha256-qWKbLUJpavIpvOdX1fhHYm0WGerytFHRoh9lVck6Bh0=",
"owner": "nix-community",
"repo": "disko",
"rev": "878ec37d6a8f52c6c801d0e2a2ad554c75b9353c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -80,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1773681856, "lastModified": 1775781825,
"narHash": "sha256-+bRqxoFCJFO9ZTFhcCkzNXbDT3b8AEk88fyjB7Is6eo=", "narHash": "sha256-L5yKTpR+alrZU2XYYvIxCeCP4LBHU5jhwSj7H1VAavg=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "57d5560ee92a424fb71fde800acd6ed2c725dfce", "rev": "e35c39fca04fee829cecdf839a50eb9b54d8a701",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -93,50 +20,33 @@
"type": "github" "type": "github"
} }
}, },
"nix-github-actions": { "nix-darwin": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"colmena",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1729742964, "lastModified": 1775037210,
"narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=", "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=",
"owner": "nix-community", "owner": "LnL7",
"repo": "nix-github-actions", "repo": "nix-darwin",
"rev": "e04df33f62cdcf93d73e9a04142464753a16db67", "rev": "06648f4902343228ce2de79f291dd5a58ee12146",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "LnL7",
"repo": "nix-github-actions", "repo": "nix-darwin",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1750134718, "lastModified": 1775763530,
"narHash": "sha256-v263g4GbxXv87hMXMCpjkIxd/viIF7p3JpJrwgKdNiI=", "narHash": "sha256-BuTK9z1QEwWPOIakQ1gCN4pa4VwVJpfptYCviy2uOGc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9e83b64f727c88a7711a2c463a7b16eedb69a84c", "rev": "b0188973b4b2a5b6bdba8b65381d6cd09a533da0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1773628058,
"narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -148,26 +58,9 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"colmena": "colmena",
"disko": "disko",
"home-manager": "home-manager", "home-manager": "home-manager",
"nixpkgs": "nixpkgs_2" "nix-darwin": "nix-darwin",
} "nixpkgs": "nixpkgs"
},
"stable": {
"locked": {
"lastModified": 1750133334,
"narHash": "sha256-urV51uWH7fVnhIvsZIELIYalMYsyr2FCalvlRTzqWRw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "36ab78dab7da2e4e27911007033713bab534187b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
} }
} }
}, },

189
flake.nix
View File

@@ -1,125 +1,134 @@
{ {
description = "NodeiWest company flake"; description = "NodeiWest company Nix SDK";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
colmena.url = "github:zhaofengli/colmena";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = { home-manager = {
url = "github:nix-community/home-manager"; url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
nix-darwin = {
url = "github:LnL7/nix-darwin";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = outputs =
inputs@{ inputs@{
self, self,
nixpkgs, nixpkgs,
colmena,
disko,
home-manager,
... ...
}: }:
let let
lib = nixpkgs.lib; lib = nixpkgs.lib;
supportedSystems = [ defaults = import ./lib/defaults.nix { inherit lib; };
"aarch64-darwin" inherit (defaults) forAllSystems supportedSystems;
"x86_64-darwin"
"x86_64-linux"
];
forAllSystems = lib.genAttrs supportedSystems;
mkPkgs = shellFactories = {
system: default = args: import ./shells/default.nix args;
import nixpkgs { node = args: import ./shells/node.nix (args // { inherit lib; });
inherit system; go = args: import ./shells/go.nix args;
}; rust = args: import ./shells/rust.nix (args // { inherit lib; });
};
mkHost = profileModules = {
name: backend = import ./profiles/backend.nix;
nixpkgs.lib.nixosSystem { frontend = import ./profiles/frontend.nix;
system = "x86_64-linux"; minimal = import ./profiles/minimal.nix;
specialArgs = { };
inherit inputs self;
}; darwinSystem = args: import ./systems/darwin/default.nix ({ inherit inputs; } // args);
modules = [ linuxHome = args: import ./systems/linux/default.nix ({ inherit inputs; } // args);
disko.nixosModules.disko
home-manager.nixosModules.home-manager
self.nixosModules.common
./hosts/${name}/configuration.nix
];
};
in in
{ {
homeManagerModules.default = ./modules/home.nix; lib = {
homeManagerModules.helpers = ./modules/helpers/home.nix; inherit (defaults) companySessionVariables forAllSystems supportedSystems;
nixosModules.common = ./modules/nixos/common.nix; mkProfile = import ./lib/mkProfile.nix;
mkSystem = import ./lib/mkSystem.nix {
systems = {
darwin = darwinSystem;
linux = linuxHome;
};
};
shells = shellFactories;
systems = {
darwin = darwinSystem;
linux = linuxHome;
};
};
packages = forAllSystems ( homeManagerModules = {
base = {
nix = import ./modules/base/nix.nix;
core = import ./modules/base/core.nix;
shell = import ./modules/base/shell.nix;
fonts = import ./modules/base/fonts.nix;
};
dev = {
node = import ./modules/dev/node.nix;
go = import ./modules/dev/go.nix;
rust = import ./modules/dev/rust.nix;
docker = import ./modules/dev/docker.nix;
};
roles = {
backend = import ./modules/roles/backend.nix;
frontend = import ./modules/roles/frontend.nix;
infra = import ./modules/roles/infra.nix;
minimal = import ./modules/roles/minimal.nix;
};
services = {
tailscale = import ./modules/services/tailscale.nix;
ssh = import ./modules/services/ssh.nix;
gpg = import ./modules/services/gpg.nix;
};
secrets = {
env = import ./modules/secrets/env.nix;
openbao = import ./modules/secrets/openbao.nix;
};
optional = {
homebrew = import ./modules/optional/homebrew.nix;
gui = import ./modules/optional/gui.nix;
devtools = import ./modules/optional/devtools.nix;
};
profiles = profileModules;
default = import ./modules/home.nix;
};
devShells = forAllSystems (
system: system:
let let
pkgs = mkPkgs system; pkgs = import nixpkgs { inherit system; };
nodeiwestHelper = pkgs.callPackage ./pkgs/helpers { };
in in
{ {
colmena = colmena.packages.${system}.colmena; default = shellFactories.default { inherit pkgs; };
nodeiwest-helper = nodeiwestHelper; node = shellFactories.node { inherit pkgs; };
default = colmena.packages.${system}.colmena; go = shellFactories.go { inherit pkgs; };
rust = shellFactories.rust { inherit pkgs; };
} }
); );
apps = forAllSystems (system: { formatter = forAllSystems (
colmena = { system:
type = "app"; let
program = "${colmena.packages.${system}.colmena}/bin/colmena"; pkgs = import nixpkgs { inherit system; };
}; in
nodeiwest-helper = { pkgs.nixfmt
type = "app"; );
program = "${self.packages.${system}.nodeiwest-helper}/bin/nodeiwest";
};
default = self.apps.${system}.colmena;
});
nixosConfigurations = { templates = {
vps1 = mkHost "vps1"; user-flake = {
}; path = ./templates/user-flake;
description = "Starter personal flake wired to NodeiWest profiles.";
colmena = {
meta = {
nixpkgs = mkPkgs "x86_64-linux";
specialArgs = {
inherit inputs self;
};
};
defaults =
{ name, ... }:
{
networking.hostName = name;
imports = [
disko.nixosModules.disko
home-manager.nixosModules.home-manager
self.nixosModules.common
];
};
vps1 = {
deployment = {
targetHost = "100.101.167.118";
targetUser = "root";
tags = [
"company"
"edge"
];
};
imports = [ ./hosts/vps1/configuration.nix ];
}; };
}; };
colmenaHive = colmena.lib.makeHive self.outputs.colmena;
}; };
} }

View File

@@ -1,28 +0,0 @@
{ lib, ... }:
{
imports = [
./disko.nix
./hardware-configuration.nix
];
networking.hostName = "vps1";
networking.useDHCP = lib.mkDefault true;
time.timeZone = "UTC";
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.grub = {
enable = true;
efiSupport = true;
device = "nodev";
};
nodeiwest.ssh.userCAPublicKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE6c2oMkM7lLg9qWHVgbrFaFBDrrFyynFlPviiydQdFi openbao-user-ca"
];
nodeiwest.tailscale.openbao = {
enable = true;
};
system.stateVersion = "25.05";
}

View File

@@ -1,46 +0,0 @@
{
lib,
...
}:
{
# Replace /dev/sda if the VPS exposes a different disk, e.g. /dev/vda or /dev/nvme0n1.
disko.devices = {
disk.main = {
type = "disk";
device = lib.mkDefault "/dev/sda";
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
start = "1MiB";
end = "512MiB";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
swap = {
size = "4GiB";
content = {
type = "swap";
resumeDevice = true;
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
}

View File

@@ -1,10 +0,0 @@
{ lib, ... }:
{
# Replace this file with the generated hardware config from the target host.
fileSystems."/" = lib.mkDefault {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
swapDevices = [ ];
}

20
lib/defaults.nix Normal file
View File

@@ -0,0 +1,20 @@
{ lib }:
let
supportedSystems = [
"aarch64-darwin"
"x86_64-darwin"
"x86_64-linux"
];
in
{
inherit supportedSystems;
forAllSystems = f: lib.genAttrs supportedSystems f;
companySessionVariables = {
BAO_ADDR = "https://secrets.api.nodeiwest.se";
SOME_REGISTRY = "git.dgren.dev";
};
stateVersion = "24.11";
}

7
lib/mkProfile.nix Normal file
View File

@@ -0,0 +1,7 @@
{
modules ? [ ],
extraModules ? [ ],
}:
{
imports = modules ++ extraModules;
}

15
lib/mkSystem.nix Normal file
View File

@@ -0,0 +1,15 @@
{ systems }:
{
target,
...
}@args:
let
adapter =
if target == "darwin" then
systems.darwin
else if target == "linux" then
systems.linux
else
throw "Unsupported target `${target}`. Expected `darwin` or `linux`.";
in
adapter (builtins.removeAttrs args [ "target" ])

19
modules/base/core.nix Normal file
View File

@@ -0,0 +1,19 @@
{ lib, pkgs, ... }:
{
home.packages = with pkgs; [
age
curl
fd
git
jq
just
ripgrep
sops
];
programs.git = {
enable = true;
lfs.enable = true;
signing.format = lib.mkDefault "openpgp";
};
}

11
modules/base/fonts.nix Normal file
View File

@@ -0,0 +1,11 @@
{ lib, pkgs, ... }:
let
jetbrainsMono = lib.attrByPath [ "nerd-fonts" "jetbrains-mono" ] null pkgs;
in
{
fonts.fontconfig.enable = pkgs.stdenv.isLinux;
home.packages = builtins.filter (pkg: pkg != null) [
jetbrainsMono
];
}

14
modules/base/nix.nix Normal file
View File

@@ -0,0 +1,14 @@
{ lib, pkgs, ... }:
{
programs.home-manager.enable = true;
nix.package = lib.mkDefault pkgs.nix;
nix.settings = {
experimental-features = [
"nix-command"
"flakes"
];
warn-dirty = false;
};
}

20
modules/base/shell.nix Normal file
View File

@@ -0,0 +1,20 @@
{ ... }:
{
programs.bash.enable = true;
programs.zsh = {
enable = true;
autocd = true;
enableCompletion = true;
shellAliases = {
l = "ls -CF";
la = "ls -A";
ll = "ls -alF";
};
};
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
}

16
modules/dev/docker.nix Normal file
View File

@@ -0,0 +1,16 @@
{ lib, pkgs, ... }:
let
optionalPackage = path: lib.attrByPath path null pkgs;
in
{
home.packages = builtins.filter (pkg: pkg != null) (
[
(optionalPackage [ "docker-client" ])
(optionalPackage [ "docker-compose" ])
(optionalPackage [ "lazydocker" ])
]
++ lib.optionals pkgs.stdenv.isDarwin [
(optionalPackage [ "colima" ])
]
);
}

7
modules/dev/go.nix Normal file
View File

@@ -0,0 +1,7 @@
{ pkgs, ... }:
{
home.packages = with pkgs; [
go
gopls
];
}

10
modules/dev/node.nix Normal file
View File

@@ -0,0 +1,10 @@
{ lib, pkgs, ... }:
let
nodejs = lib.attrByPath [ "nodejs_20" ] pkgs.nodejs pkgs;
in
{
home.packages = [
nodejs
pkgs.pnpm
];
}

13
modules/dev/rust.nix Normal file
View File

@@ -0,0 +1,13 @@
{ lib, pkgs, ... }:
let
optionalPackage = path: lib.attrByPath path null pkgs;
in
{
home.packages = builtins.filter (pkg: pkg != null) [
pkgs.cargo
pkgs.rustc
(optionalPackage [ "rust-analyzer" ])
(optionalPackage [ "rustfmt" ])
(optionalPackage [ "clippy" ])
];
}

View File

@@ -1,10 +0,0 @@
{ pkgs, ... }:
let
nodeiwestHelper = pkgs.callPackage ../../pkgs/helpers { };
in
{
home.packages = [
pkgs.python3
nodeiwestHelper
];
}

View File

@@ -1,18 +1,9 @@
{ pkgs, lib, ... }: { pkgs, ... }:
{ {
imports = [ ./helpers/home.nix ]; imports = [
../profiles/minimal.nix
# Company env vars — available in all shells ./secrets/openbao.nix
home.sessionVariables = {
BAO_ADDR = "https://secrets.api.nodeiwest.se";
SOME_REGISTRY = "git.dgren.dev";
# etc.
};
home.packages = with pkgs; [
# Tools every dev needs
openbao
colmena
# etc.
]; ];
home.packages = [ pkgs.colmena ];
} }

View File

@@ -1,101 +0,0 @@
{
config,
lib,
self,
...
}:
let
cfg = config.nodeiwest;
trustedUserCAKeysPath = "/etc/ssh/trusted-user-ca-keys.pem";
in
{
imports = [ ./tailscale-init.nix ];
options.nodeiwest = {
openbao.address = lib.mkOption {
type = lib.types.str;
default = "https://secrets.api.nodeiwest.se";
description = "Remote OpenBao address that hosts should use as clients.";
example = "https://secrets.api.nodeiwest.se";
};
homeManagerUsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"root"
"deploy"
];
description = "Users that should receive the shared Home Manager company profile.";
example = [
"root"
"deploy"
];
};
ssh.userCAPublicKeys = lib.mkOption {
type = lib.types.listOf lib.types.singleLineStr;
default = [ ];
description = "OpenBao SSH user CA public keys trusted by sshd for user certificate authentication.";
example = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBExampleOpenBaoUserCA openbao-user-ca"
];
};
};
config = {
networking.firewall.allowedTCPPorts = [
22
80
443
];
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PubkeyAuthentication = true;
PermitRootLogin = "prohibit-password";
}
// lib.optionalAttrs (cfg.ssh.userCAPublicKeys != [ ]) {
TrustedUserCAKeys = trustedUserCAKeysPath;
};
};
users.groups.deploy = { };
users.users.deploy = {
isNormalUser = true;
group = "deploy";
createHome = true;
extraGroups = [ "wheel" ];
};
services.traefik = {
enable = true;
staticConfigOptions = {
api.dashboard = true;
entryPoints.web.address = ":80";
entryPoints.websecure.address = ":443";
ping = { };
};
dynamicConfigOptions = lib.mkMerge [ ];
};
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users = lib.genAttrs cfg.homeManagerUsers (_: {
imports = [ self.homeManagerModules.default ];
home.stateVersion = config.system.stateVersion;
});
};
environment.etc = lib.mkIf (cfg.ssh.userCAPublicKeys != [ ]) {
"ssh/trusted-user-ca-keys.pem".text = lib.concatStringsSep "\n" cfg.ssh.userCAPublicKeys + "\n";
};
environment.variables = {
BAO_ADDR = cfg.openbao.address;
};
};
}

View File

@@ -1,158 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.nodeiwest;
tailscaleOpenbaoCfg = cfg.tailscale.openbao;
in
{
options.nodeiwest.tailscale.openbao = {
enable = lib.mkEnableOption "fetching the Tailscale auth key from OpenBao";
namespace = lib.mkOption {
type = lib.types.str;
default = "it";
description = "OpenBao namespace used when fetching the Tailscale auth key.";
};
authPath = lib.mkOption {
type = lib.types.str;
default = "auth/approle";
description = "OpenBao auth mount path used by the AppRole login.";
};
secretPath = lib.mkOption {
type = lib.types.str;
default = "tailscale";
description = "OpenBao secret path containing the Tailscale auth key.";
};
field = lib.mkOption {
type = lib.types.str;
default = "auth_key";
description = "Field in the OpenBao secret that contains the Tailscale auth key.";
};
renderedAuthKeyFile = lib.mkOption {
type = lib.types.str;
default = "/run/nodeiwest/tailscale-auth-key";
description = "Runtime file rendered by OpenBao Agent and consumed by Tailscale autoconnect.";
};
approle = {
roleIdFile = lib.mkOption {
type = lib.types.str;
default = "/var/lib/nodeiwest/openbao-approle-role-id";
description = "Root-only file containing the OpenBao AppRole role_id.";
};
secretIdFile = lib.mkOption {
type = lib.types.str;
default = "/var/lib/nodeiwest/openbao-approle-secret-id";
description = "Root-only file containing the OpenBao AppRole secret_id.";
};
};
};
config = {
systemd.tmpfiles.rules = [
"d /var/lib/nodeiwest 0700 root root - -"
"d /run/nodeiwest 0700 root root - -"
];
services.tailscale = {
enable = true;
openFirewall = true;
extraUpFlags = lib.optionals tailscaleOpenbaoCfg.enable [ "--ssh" ];
authKeyFile = if tailscaleOpenbaoCfg.enable then tailscaleOpenbaoCfg.renderedAuthKeyFile else null;
};
services.vault-agent.instances.tailscale = lib.mkIf tailscaleOpenbaoCfg.enable {
package = pkgs.openbao;
settings = {
vault.address = cfg.openbao.address;
auto_auth = {
method = [
{
type = "approle";
mount_path = tailscaleOpenbaoCfg.authPath;
namespace = tailscaleOpenbaoCfg.namespace;
config = {
role_id_file_path = tailscaleOpenbaoCfg.approle.roleIdFile;
secret_id_file_path = tailscaleOpenbaoCfg.approle.secretIdFile;
remove_secret_id_file_after_reading = false;
};
}
];
};
template = [
{
contents = ''{{- with secret "${tailscaleOpenbaoCfg.secretPath}" -}}{{- if .Data.data -}}{{ index .Data.data "${tailscaleOpenbaoCfg.field}" }}{{- else -}}{{ index .Data "${tailscaleOpenbaoCfg.field}" }}{{- end -}}{{- end -}}'';
destination = tailscaleOpenbaoCfg.renderedAuthKeyFile;
perms = "0400";
}
];
};
};
systemd.services.vault-agent-tailscale = lib.mkIf tailscaleOpenbaoCfg.enable {
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig.Environment = [ "BAO_NAMESPACE=${tailscaleOpenbaoCfg.namespace}" ];
};
systemd.services.nodeiwest-tailscale-authkey-ready = lib.mkIf tailscaleOpenbaoCfg.enable {
description = "Wait for the Tailscale auth key rendered by OpenBao Agent";
after = [ "vault-agent-tailscale.service" ];
requires = [ "vault-agent-tailscale.service" ];
before = [ "tailscaled-autoconnect.service" ];
requiredBy = [ "tailscaled-autoconnect.service" ];
path = [ pkgs.coreutils ];
serviceConfig = {
Type = "oneshot";
};
script = ''
set -euo pipefail
for _ in $(seq 1 60); do
if [ -s ${lib.escapeShellArg tailscaleOpenbaoCfg.renderedAuthKeyFile} ]; then
exit 0
fi
sleep 1
done
echo "Timed out waiting for rendered Tailscale auth key at ${tailscaleOpenbaoCfg.renderedAuthKeyFile}" >&2
exit 1
'';
};
systemd.services.tailscaled-autoconnect = lib.mkIf tailscaleOpenbaoCfg.enable {
after = [
"vault-agent-tailscale.service"
"nodeiwest-tailscale-authkey-ready.service"
];
requires = [
"vault-agent-tailscale.service"
"nodeiwest-tailscale-authkey-ready.service"
];
serviceConfig.ExecStartPre = [
"${lib.getExe' pkgs.coreutils "test"} -s ${tailscaleOpenbaoCfg.renderedAuthKeyFile}"
];
};
assertions = [
{
assertion =
(!tailscaleOpenbaoCfg.enable)
|| (
tailscaleOpenbaoCfg.approle.roleIdFile != ""
&& tailscaleOpenbaoCfg.approle.secretIdFile != ""
);
message = "AppRole roleIdFile and secretIdFile must be set when OpenBao-backed Tailscale enrollment is enabled.";
}
];
};
}

View File

@@ -0,0 +1,11 @@
{ lib, pkgs, ... }:
let
optionalPackage = path: lib.attrByPath path null pkgs;
in
{
home.packages = builtins.filter (pkg: pkg != null) [
(optionalPackage [ "nil" ])
(optionalPackage [ "nixd" ])
(optionalPackage [ "nixfmt" ])
];
}

9
modules/optional/gui.nix Normal file
View File

@@ -0,0 +1,9 @@
{ lib, pkgs, ... }:
let
wezterm = lib.attrByPath [ "wezterm" ] null pkgs;
in
{
home.packages = builtins.filter (pkg: pkg != null) [
wezterm
];
}

View File

@@ -0,0 +1,6 @@
{ ... }:
{
# Intentionally empty. This is the seam downstream workstations can use to
# compose nix-homebrew or Homebrew-specific activation without coupling it
# into the shared base roles.
}

13
modules/roles/backend.nix Normal file
View File

@@ -0,0 +1,13 @@
{ ... }:
{
imports = [
./minimal.nix
../dev/node.nix
../dev/go.nix
../dev/docker.nix
../services/ssh.nix
../services/gpg.nix
../secrets/openbao.nix
../optional/devtools.nix
];
}

View File

@@ -0,0 +1,11 @@
{ ... }:
{
imports = [
./minimal.nix
../base/fonts.nix
../dev/node.nix
../services/gpg.nix
../optional/devtools.nix
../optional/gui.nix
];
}

15
modules/roles/infra.nix Normal file
View File

@@ -0,0 +1,15 @@
{ pkgs, ... }:
{
imports = [
./minimal.nix
../dev/go.nix
../dev/docker.nix
../services/ssh.nix
../services/tailscale.nix
../services/gpg.nix
../secrets/openbao.nix
../optional/devtools.nix
];
home.packages = [ pkgs.colmena ];
}

View File

@@ -0,0 +1,9 @@
{ ... }:
{
imports = [
../base/nix.nix
../base/core.nix
../base/shell.nix
../secrets/env.nix
];
}

7
modules/secrets/env.nix Normal file
View File

@@ -0,0 +1,7 @@
{ lib, ... }:
let
defaults = import ../../lib/defaults.nix { inherit lib; };
in
{
home.sessionVariables = defaults.companySessionVariables;
}

View File

@@ -0,0 +1,6 @@
{ pkgs, ... }:
{
imports = [ ./env.nix ];
home.packages = [ pkgs.openbao ];
}

11
modules/services/gpg.nix Normal file
View File

@@ -0,0 +1,11 @@
{ pkgs, ... }:
{
programs.gpg.enable = true;
services.gpg-agent = {
enable = true;
enableBashIntegration = true;
enableZshIntegration = true;
pinentry.package = pkgs.pinentry-curses;
};
}

22
modules/services/ssh.nix Normal file
View File

@@ -0,0 +1,22 @@
{ lib, ... }:
{
programs.ssh = {
enable = true;
enableDefaultConfig = false;
matchBlocks."*" = {
addKeysToAgent = lib.mkDefault "yes";
compression = lib.mkDefault false;
controlMaster = lib.mkDefault "no";
controlPath = lib.mkDefault "~/.ssh/master-%r@%n:%p";
controlPersist = lib.mkDefault "no";
forwardAgent = lib.mkDefault true;
hashKnownHosts = lib.mkDefault false;
serverAliveCountMax = lib.mkDefault 3;
serverAliveInterval = lib.mkDefault 0;
userKnownHostsFile = lib.mkDefault "~/.ssh/known_hosts";
};
};
services.ssh-agent.enable = true;
}

View File

@@ -0,0 +1,9 @@
{ lib, pkgs, ... }:
let
tailscale = lib.attrByPath [ "tailscale" ] null pkgs;
in
{
home.packages = builtins.filter (pkg: pkg != null) [
tailscale
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
{
lib,
writeShellApplication,
python3,
openbao,
openssh,
gitMinimal,
nix,
}:
writeShellApplication {
name = "nodeiwest";
runtimeInputs = [
python3
openbao
openssh
gitMinimal
nix
];
text = ''
export NODEIWEST_HELPER_TEMPLATES=${./templates}
exec ${python3}/bin/python ${./cli.py} "$@"
'';
meta = with lib; {
description = "Safe VPS provisioning helper for the NodeiWest NixOS flake";
license = licenses.mit;
mainProgram = "nodeiwest";
platforms = platforms.unix;
};
}

View File

@@ -1,23 +0,0 @@
{ lib, ... }:
{
# Generated by nodeiwest host init.
imports = [
./disko.nix
./hardware-configuration.nix
];
networking.hostName = "@@HOST_NAME@@";
networking.useDHCP = lib.mkDefault true;
time.timeZone = "@@TIMEZONE@@";
@@BOOT_LOADER_BLOCK@@
nodeiwest.ssh.userCAPublicKeys = @@SSH_CA_KEYS@@;
nodeiwest.tailscale.openbao = {
enable = @@TAILSCALE_OPENBAO_ENABLE@@;
};
system.stateVersion = "@@STATE_VERSION@@";
}

View File

@@ -1,41 +0,0 @@
{
lib,
...
}:
{
# Generated by nodeiwest host init.
# Replace the disk only if the provider exposes a different primary device.
disko.devices = {
disk.main = {
type = "disk";
device = lib.mkDefault "@@DISK_DEVICE@@";
content = {
type = "gpt";
partitions = {
BIOS = {
priority = 1;
name = "BIOS";
start = "1MiB";
end = "2MiB";
type = "EF02";
};
swap = {
size = "@@SWAP_SIZE@@";
content = {
type = "swap";
resumeDevice = true;
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
}

View File

@@ -1,47 +0,0 @@
{
lib,
...
}:
{
# Generated by nodeiwest host init.
# Replace the disk only if the provider exposes a different primary device.
disko.devices = {
disk.main = {
type = "disk";
device = lib.mkDefault "@@DISK_DEVICE@@";
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
start = "1MiB";
end = "512MiB";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
swap = {
size = "@@SWAP_SIZE@@";
content = {
type = "swap";
resumeDevice = true;
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
}

View File

@@ -1,5 +0,0 @@
{ ... }:
{
# Placeholder generated by nodeiwest host init.
# nixos-anywhere will replace this with the generated hardware config.
}

View File

@@ -1,3 +0,0 @@
path "@@POLICY_PATH@@" {
capabilities = ["read"]
}

View File

@@ -1,50 +0,0 @@
from __future__ import annotations
import importlib.util
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
CLI_PATH = REPO_ROOT / "pkgs" / "helpers" / "cli.py"
spec = importlib.util.spec_from_file_location("nodeiwest_cli", CLI_PATH)
cli = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = cli
spec.loader.exec_module(cli)
class HelperCliTests(unittest.TestCase):
def test_disk_from_device_supports_sd_and_nvme(self) -> None:
self.assertEqual(cli.disk_from_device("/dev/sda2"), "/dev/sda")
self.assertEqual(cli.disk_from_device("/dev/nvme0n1p2"), "/dev/nvme0n1")
def test_lookup_colmena_target_host_reads_existing_inventory(self) -> None:
flake_text = (REPO_ROOT / "flake.nix").read_text()
self.assertEqual(cli.lookup_colmena_target_host(flake_text, "vps1"), "100.101.167.118")
def test_parse_existing_vps1_configuration(self) -> None:
configuration = cli.parse_existing_configuration(REPO_ROOT / "hosts" / "vps1" / "configuration.nix")
self.assertEqual(configuration.host_name, "vps1")
self.assertEqual(configuration.boot_mode, "uefi")
self.assertTrue(configuration.tailscale_openbao)
self.assertEqual(configuration.state_version, "25.05")
self.assertTrue(configuration.user_ca_public_keys)
def test_parse_existing_vps1_disko(self) -> None:
disko = cli.parse_existing_disko(REPO_ROOT / "hosts" / "vps1" / "disko.nix")
self.assertEqual(disko.disk_device, "/dev/sda")
self.assertEqual(disko.boot_mode, "uefi")
self.assertEqual(disko.swap_size, "4GiB")
def test_render_bios_disko_uses_bios_partition(self) -> None:
rendered = cli.render_disko(boot_mode="bios", disk_device="/dev/vda", swap_size="8GiB")
self.assertIn('type = "EF02";', rendered)
self.assertIn('device = lib.mkDefault "/dev/vda";', rendered)
self.assertIn('size = "8GiB";', rendered)
if __name__ == "__main__":
unittest.main()

8
profiles/backend.nix Normal file
View File

@@ -0,0 +1,8 @@
let
mkProfile = import ../lib/mkProfile.nix;
in
mkProfile {
modules = [
../modules/roles/backend.nix
];
}

8
profiles/frontend.nix Normal file
View File

@@ -0,0 +1,8 @@
let
mkProfile = import ../lib/mkProfile.nix;
in
mkProfile {
modules = [
../modules/roles/frontend.nix
];
}

10
profiles/minimal.nix Normal file
View File

@@ -0,0 +1,10 @@
let
mkProfile = import ../lib/mkProfile.nix;
in
mkProfile {
modules = [
../modules/roles/minimal.nix
../modules/services/ssh.nix
../modules/optional/devtools.nix
];
}

20
shells/default.nix Normal file
View File

@@ -0,0 +1,20 @@
{
pkgs,
extraPackages ? [ ],
inputsFrom ? [ ],
shellHook ? "",
}:
pkgs.mkShell {
inherit inputsFrom shellHook;
packages =
(with pkgs; [
git
jq
just
nil
nixd
nixfmt
])
++ extraPackages;
}

16
shells/go.nix Normal file
View File

@@ -0,0 +1,16 @@
{
pkgs,
extraPackages ? [ ],
inputsFrom ? [ ],
shellHook ? "",
}:
pkgs.mkShell {
inherit inputsFrom shellHook;
packages =
(with pkgs; [
go
gopls
])
++ extraPackages;
}

19
shells/node.nix Normal file
View File

@@ -0,0 +1,19 @@
{
lib,
pkgs,
extraPackages ? [ ],
inputsFrom ? [ ],
shellHook ? "",
}:
let
nodejs = lib.attrByPath [ "nodejs_20" ] pkgs.nodejs pkgs;
in
pkgs.mkShell {
inherit inputsFrom shellHook;
packages = [
nodejs
pkgs.pnpm
]
++ extraPackages;
}

24
shells/rust.nix Normal file
View File

@@ -0,0 +1,24 @@
{
lib,
pkgs,
extraPackages ? [ ],
inputsFrom ? [ ],
shellHook ? "",
}:
let
optionalPackage = path: lib.attrByPath path null pkgs;
in
pkgs.mkShell {
inherit inputsFrom shellHook;
packages = builtins.filter (pkg: pkg != null) (
[
pkgs.cargo
pkgs.rustc
(optionalPackage [ "rust-analyzer" ])
(optionalPackage [ "rustfmt" ])
(optionalPackage [ "clippy" ])
]
++ extraPackages
);
}

View File

@@ -0,0 +1,42 @@
{
inputs,
system,
username,
homeDirectory,
modules ? [ ],
darwinStateVersion ? 6,
stateVersion ? "24.11",
extraSpecialArgs ? { },
}:
inputs.nix-darwin.lib.darwinSystem {
inherit system;
specialArgs = extraSpecialArgs // {
inherit inputs;
};
modules = [
inputs.home-manager.darwinModules.home-manager
{
system.stateVersion = darwinStateVersion;
users.users = {
${username}.home = homeDirectory;
};
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = extraSpecialArgs // {
inherit inputs;
};
home-manager.users = {
${username} = {
imports = modules;
home = {
inherit stateVersion username;
};
};
};
}
];
}

24
systems/linux/default.nix Normal file
View File

@@ -0,0 +1,24 @@
{
inputs,
system,
username,
homeDirectory,
modules ? [ ],
stateVersion ? "24.11",
extraSpecialArgs ? { },
}:
inputs.home-manager.lib.homeManagerConfiguration {
pkgs = import inputs.nixpkgs { inherit system; };
extraSpecialArgs = extraSpecialArgs // {
inherit inputs;
};
modules = [
{
home = {
inherit homeDirectory stateVersion username;
};
}
]
++ modules;
}

View File

@@ -0,0 +1,36 @@
{
description = "Starter personal flake for NodeiWest employees";
inputs = {
# Adjust the repo path if you publish this under a different name.
company.url = "git+ssh://git@git.dgren.dev/employees/company-nix.git";
};
outputs =
{ company, ... }:
let
username = "your-name";
in
{
# Remove whichever target you do not need.
homeConfigurations."${username}@linux" = company.lib.mkSystem {
target = "linux";
system = "x86_64-linux";
inherit username;
homeDirectory = "/home/${username}";
modules = [
company.homeManagerModules.profiles.backend
];
};
darwinConfigurations."${username}-mac" = company.lib.mkSystem {
target = "darwin";
system = "aarch64-darwin";
inherit username;
homeDirectory = "/Users/${username}";
modules = [
company.homeManagerModules.profiles.frontend
];
};
};
}