Commit Graph

729 Commits

Author SHA1 Message Date
tomnewton-mulga 72503cd7ae fix: prevented systemd blocking 2026-04-16 17:58:54 +10:00
Ben Duncan d8924a2ecc ISO tweaks and doc template 2026-04-16 15:38:56 +10:00
Ben Duncan 402a04e1b9 Merge branch 'feat/iso-installer' of github.com:mulgadc/spinifex into feat/iso-installer 2026-04-16 09:29:15 +10:00
Ben Duncan 25d02707d0 ISO tweaks 2026-04-16 09:29:00 +10:00
tomnewton-mulga 0e51a6b06a fix: enable AWS CLI install in firstboot (remove SKIP_AWS flag) 2026-04-15 16:21:56 +10:00
tomnewton-mulga d3e21d3360 fix: added remove usb prompt 2026-04-15 16:21:56 +10:00
Julian Sommer 32effaa16e Merge branch 'main' into feat/iso-installer 2026-04-15 15:50:37 +10:00
tomnewton-mulga 054485e930 refactor: move systemd unit files into installer/systemd package
Extract all inline systemd unit and drop-in content from install.go and
firstboot.go into a dedicated cmd/installer/systemd package for editability.
Bash scripts remain inline in firstboot.go.

Also copies the GRUB splash image from the live ISO into the installed
system's /boot/grub/ and sets GRUB_BACKGROUND so the branded background
appears in the post-install GRUB, not just the installer GRUB.
2026-04-15 15:34:40 +10:00
Ben Duncan c8429f98ae Update ALB doc 2026-04-15 15:25:48 +10:00
Julian Sommer 68ae17dcf1 chore: rename app-id to client-id 2026-04-15 15:03:16 +10:00
Julian Sommer 5b39efb078 fix: make e2e use env vars 2026-04-15 13:40:22 +10:00
tomnewton-mulga 24429ee8b5 fix: simplify firstboot — delegate setup to setup.sh with embedded tarball
Remove the duplicated chown/ownership/symlink block from the firstboot
script. setup.sh (called with INSTALL_SPINIFEX_TARBALL) already handles
service users, directory ownership, sudoers, systemd unit installation,
and enabling spinifex.target — the same steps as the online curl|bash
install.

Firstboot now follows the documented install flow directly:
  1. setup.sh --tarball (air-gapped equivalent of curl|bash)
  2. setup-ovn.sh --management
  3. spx admin init
  4. systemctl start spinifex.target

Also reduce wait-for-nats.sh timeout from 90s to 30s per reviewer feedback.
2026-04-15 13:36:07 +10:00
Julian Sommer 7d6e0780b5 feat: action rate limiting (#140) 2026-04-15 12:19:30 +10:00
tomnewton-mulga 83f1766081 fix: fixed TUI breaking at the end 2026-04-15 11:54:57 +10:00
tomnewton-mulga 811a0230fc fix: fall back to tty1 when requested TTY unavailable instead of aborting 2026-04-15 11:17:58 +10:00
tomnewton-mulga 69c8b20b78 fix: harden ISO installer error handling and safety checks
- partUUID: error on empty blkid output instead of writing UUID= to fstab
- installBootloader: fail if both EFI and BIOS targets fail; track efiErr
- machine-id fallback: write at mode 0600 so systemd can commit on first boot
- Run: defer unmount so partitions are always cleaned up on failure
- copyRootfs: verify critical paths exist after rsync before continuing
- resolveNIC: skip virtual/bridge interfaces (docker0, virbr0, ovs-system)
- EjectAndReboot: use syscall.Reboot instead of exec(reboot) which needs D-Bus
- ui.Run: return error on TTY open failure instead of silent fallback
- firstboot script: trap EXIT to always self-disable, preventing retry loops
- enableUnit/enableBannerUnit: deduplicate into enableSystemdUnit; check Remove
- runAdminBanner: promote service restart failure to slog.Error
- parseNodeConf: log non-ENOENT read failures
2026-04-15 10:59:51 +10:00
Julian Sommer f0bb064237 Merge branch 'main' into feat/iso-installer 2026-04-15 09:57:15 +10:00
Julian Sommer 1ad03bb518 feat(gateway): per-IP auth failure rate limiting (#138) 2026-04-15 09:56:04 +10:00
tomnewton-mulga c75f4a0160 fix: eject iso boot device 2026-04-15 09:46:53 +10:00
tomnewton-mulga 01cd3b9ab1 Merge commit 'b7b639084b3c2a40b97c2f543b88ad498901b77d' into feat/iso-installer
# Conflicts:
#	Makefile
#	cmd/spinifex/cmd/admin.go
#	go.sum
2026-04-14 16:11:49 +10:00
Julian Sommer b7b639084b chore: use ts config files v1.1.0 2026-04-14 14:28:00 +10:00
Julian Sommer 7dcad2ba1d chore: remove unused e2e 2026-04-14 13:59:29 +10:00
Julian Sommer 7cf0783122 fix: set dev tags as prerelease 2026-04-14 13:29:05 +10:00
Josh Siviour 13eae4d40b fix(elbv2): tag-based LB AMI lookup with hard error on miss (#137)
Replace fragile image-name substring match (-lb-/-alb-) with a
DescribeImages tag filter on spinifex:managed-by=elbv2. Remove the
silent fallback that picks the first image in the store when no LB
image exists — CreateLoadBalancer now returns ServerInternal with a
remediation hint instead of persisting a record in StateFailed.

- daemon: tag filter + error return from the resolver closure
- service_impl: resolver signature is func() (string, error); caches
  only successful results so the next call retries after an error
- CreateLoadBalancer gate now requires launcher + networking; AMI is
  resolved inside the gate and failures short-circuit before persist
- admin images import: add --name+--file combo (catalog metadata with
  a local file source) and a --tag key=value flag for explicit tagging
  of file-only imports
- build-system-image.sh + lb.conf: propagate SYSTEM_TAG via --tag so
  locally built system AMIs get the right tag even when the catalog
  is bypassed

Closes mulga-969
2026-04-14 12:52:28 +10:00
Julian Sommer b32111102d fix: README.md 2026-04-14 12:06:33 +10:00
Ben Duncan 71d0f31997 Merge branch 'main' of github.com:mulgadc/spinifex 2026-04-14 11:45:46 +10:00
Ben Duncan f99eb42ca7 Doc tweaks and README for release 2026-04-14 11:45:20 +10:00
Julian Sommer 56cdc158a6 chore: bump viperblock 2026-04-14 11:17:48 +10:00
Julian Sommer a4ae813e38 fix: recursive chmod on shared data dirs so existing image files are group-readable
chmod 0770 without -R only affected the top directory; pre-existing files
(e.g. images imported while owned by another user at mode 0600) stayed
unreadable to the spinifex group, breaking image import on upgrade paths.
2026-04-14 11:05:01 +10:00
Julian Sommer e0118975d1 fix: setup.sh 2026-04-14 10:17:01 +10:00
Julian Sommer 36e3481ca3 fix: enable spinifex.target so services autostart on boot 2026-04-14 10:14:09 +10:00
Julian Sommer 515c313e07 fix: setup.sh newgrp 2026-04-14 10:07:59 +10:00
Julian Sommer 0547bdd1fe feat: loadbalancer frontend (#136) 2026-04-14 09:24:14 +10:00
Julian Sommer e75953f5e2 chore: build ui with latest deps 2026-04-14 09:07:59 +10:00
dependabot[bot] e5a6f3ea60 chore: bump the npm-dependencies group (#134) 2026-04-14 08:59:32 +10:00
dependabot[bot] 36da673ecd chore: bump the github-actions group with 2 updates (#133) 2026-04-14 08:59:01 +10:00
tomnewton-mulga ca63011f25 feat: GRUB env var headless install, disk selection modes, banner
- autoinstall: replace file scanner with env var reader (SPINIFEX_AUTO,
  SPINIFEX_HOSTNAME, etc.) set by spinifex-init.sh from kernel cmdline
- autoinstall: disk selection modes — auto (single or fail), largest,
  smallest, nvme, or exact path; clear error listing available disks
  when auto/nvme finds multiple candidates
- admin banner: add Login row showing spinifex account name
- admin banner: write to /etc/issue (pre-login console) as well as
  /etc/motd (post-login SSH); fix SSH line root@ → spinifex@
2026-04-13 15:37:16 +10:00
Ben Duncan 96b5ad5740 Minor doc tweaks and permissions 2026-04-13 15:23:18 +10:00
tomnewton-mulga e8cb102819 feat: headless autoinstall mode via autoinstall.toml on boot media
Scans the USB EFI partition at boot for autoinstall.toml. When
enabled = true, the installer runs fully unattended using values from
the config file then ejects the USB and reboots — no user interaction
needed.

- New autoinstall package: TOML config struct, boot-media scanner,
  install.Config converter, EjectAndReboot
- Scanner skips iso9660 mounts so the bundled reference copy of the
  template never accidentally triggers headless mode
- "auto" disk target selects the largest non-removable disk
- "auto" NIC target picks the first physical interface
- Validates all required fields before touching any disk
- main.go: check for autoinstall config before launching TUI
2026-04-13 13:19:05 +10:00
Ben Duncan ebd0a64fe5 chore: bump predastore to v1.1.5 (#135)
Picks up multipart upload wedge fixes from predastore v1.1.5:
- 76fdbf1 fix: pool cleanup must not reap connections with in-flight streams (Bug A)
- 7df9645 fix: add mutex to bufferedWALFile to prevent bufio race (Bug B)
- 9dcd3dd fix: drain shard body before taking wal.mu in handlePUTShard (Bug C)
- 43e20f6 fix: widen QUIC flow-control windows to mitigate multipart upload deadlock

plus regression tests for all three bugs (predastore PR #8).
2026-04-13 13:00:25 +10:00
tomnewton-mulga 02aba39942 feat: spinifex login user, credential handoff, networking hardening (ISO)
- Add spinifex login user password via chpasswd in install.go (same
  password as root, collected once in TUI)
- Copy /root/.aws/ to /home/spinifex/.aws/ in firstboot so the operator
  can use the AWS CLI without sudo after first boot
- Fix getty drop-ins to be instance-specific (tty1/ttyS0 only), leaving
  tty2+ immediately available as rescue terminals
- Remove autologin — node now requires password at login prompt
- Pre-start OVS/OVN in firstboot and poll NB DB up to 120s to eliminate
  setup-ovn.sh timeout races on slow physical hardware
- Tighten per-service ownership in firstboot (each service user owns only
  its own /var/lib/spinifex subdirectory)
- Add LAN bridge as a non-critical systemd unit so a missing LAN cable
  can never stall network-online.target or firstboot
- Write dhcpcd.conf with short br-lan timeout; add networking.service
  drop-in to treat exit 1 as success for optional interface failures
- Disable IPv6 on management bridges to silence dhcpcd journal noise
- Fix WAN/LAN enter-key and ESC navigation logic in TUI
- Fix LAN NIC cursor to skip WAN NIC correctly
2026-04-13 12:53:07 +10:00
Josh Siviour db9e93351e fix: nginx-alb + route table NAT GW SNAT event bugs
Bundle of ALB and NAT GW fixes surfaced by the nginx-alb terraform example.

- fix: expose ALB ENI private IPs in DescribeLoadBalancers (mulga-928)
- fix: attach every ENI to multi-subnet ALB VMs (mulga-929)
- fix: nginx-alb example workers reach apt via NAT Gateway (mulga-930)
- fix: accept every ALB attribute key terraform sends (mulga-931)
- fix: publish NAT GW SNAT events on route table (dis)association (mulga-932)
- fix: publish NAT GW SNAT events on route table replace (mulga-933)
- refactor: extract multi-ENI NIC setup into testable helpers

E2E: https://github.com/mulgadc/spinifex/actions/runs/24318441048 (all green)
2026-04-13 09:29:47 +10:00
Josh Siviour c8ca746bf0 feat: ELBv2 attributes API + DescribeTags (#128)
* feat: add attributes map to LB and TG records with defaults

Replace CrossZoneEnabled bool with Attributes map[string]string on
LoadBalancerRecord and TargetGroupRecord. Add DefaultLoadBalancerAttributes()
and DefaultTargetGroupAttributes() functions. ALBs seed cross-zone enabled
at create time.

* feat: add 4 attributes methods to ELBv2Service interface

* feat: implement ELBv2 attributes service methods and NATS wrappers

Add ModifyTargetGroupAttributes, DescribeTargetGroupAttributes,
ModifyLoadBalancerAttributes, and DescribeLoadBalancerAttributes
to service_impl.go with proper ARN validation and default merging.

Add corresponding NATS request wrappers in service_nats.go.

* feat: add gateway handlers for ELBv2 attributes API endpoints

Add ModifyTargetGroupAttributes, DescribeTargetGroupAttributes,
ModifyLoadBalancerAttributes, and DescribeLoadBalancerAttributes
gateway handlers with input validation and action map registration.

* feat: add daemon handlers & NATS subscriptions for elbv2 attributes

Add 4 handler functions in daemon_handlers_elbv2.go and 4 NATS
subscriptions in daemon.go for ModifyTargetGroupAttributes,
DescribeTargetGroupAttributes, ModifyLoadBalancerAttributes, and
DescribeLoadBalancerAttributes.

* fix: add missing accountID authorization check to ELBv2 attribute handlers

* fix: guard nil attributes and log skips in ELBv2 Modify*Attributes

ModifyTargetGroupAttributes and ModifyLoadBalancerAttributes silently
dropped attributes with nil Key or Value, and would panic on a nil
slice element when dereferencing attr.Key. Add an explicit nil-element
guard and log a warning whenever an attribute is skipped, so callers
and operators can see divergence between the submitted payload and the
persisted state.

* fix: sort ELBv2 attribute responses by key for deterministic output

DescribeTargetGroupAttributes and DescribeLoadBalancerAttributes iterated
a map and appended to a slice, producing random attribute order on each
call. Sort keys before building the response so Terraform plans stay
stable and snapshot tests can rely on consistent ordering.

Closes mulga-910

* feat: add ELBv2 DescribeTags handler (mulga-919)

Required by the Terraform AWS provider's post-create refresh on
aws_lb, aws_lb_target_group, and aws_lb_listener. Without it, the
gateway returns InvalidAction and apply fails after the resources
are already created.

Dispatches by ARN resource segment (loadbalancer/targetgroup/listener)
to the existing record stores. Enforces account scoping via per-type
not-found errors to avoid cross-account existence leaks. Listeners
return an empty Tags slice since ListenerRecord doesn't store tags yet
— that's the exact case the provider hits on every untagged listener
refresh. Output is sorted by key for deterministic responses.

* refactor: parameterize DefaultLoadBalancerAttributes by lbType (mulga-915)

Cross-zone default was split across three places: CreateLoadBalancer seeded
{load_balancing.cross_zone.enabled: true} for ALBs, DefaultLoadBalancerAttributes
returned false unconditionally, and tests asserted both sides. Make
DefaultLoadBalancerAttributes(lbType) the single source of truth: ALBs get
"true", NLBs (and any other type) get "false". CreateLoadBalancer no longer
seeds attributes — DescribeLoadBalancerAttributes derives per-type defaults
from lb.Type on read. Updated TestCreateLoadBalancer_CrossZoneAttributes to
assert via the Describe handler instead of poking stored record state.

* perf: direct KV lookup for ELBv2 *ByArn finders (mulga-913)

GetLoadBalancerByArn and GetTargetGroupByArn used to call ListLoadBalancers /
ListTargetGroups and linearly scan, doing one NATS round-trip per record in
the bucket. Terraform hits Describe*Attributes on every plan/refresh — a
20-ALB / 40-TG workspace was doing ~60 calls × ~60 records = ~3600 KV gets
per refresh where ~60 would suffice. The LB/TG ARN's final path segment IS
the KV key used by GetLoadBalancer(lbID)/GetTargetGroup(tgID), so parse the
short ID from the ARN and do an O(1) lookup, verifying the record's stored
ARN matches as defence-in-depth against any stale-key weirdness.

* fix: reject ELBv2 ModifyAttributes calls where every entry is invalid (mulga-911)

When every submitted attribute was rejected by the nil-guard (nil element,
nil Key, or nil Value), Modify{TargetGroup,LoadBalancer}Attributes still
called PutTargetGroup/PutLoadBalancer with an unchanged record and returned
a successful ModifyOutput with empty Attributes. Only a slog.Warn marked
the skip — the caller saw 200 OK and moved on. Return
ErrorInvalidParameterValue when len(input.Attributes) > 0 and every entry
was rejected, so the caller learns that nothing landed.

* fix: reject unknown ELBv2 ModifyAttributes keys with ValidationError (mulga-912)

ModifyTargetGroupAttributes and ModifyLoadBalancerAttributes had no whitelist
check — any caller-supplied key (typo like 'stickness.enabled', deprecated
keys, or cross-product keys sent to the wrong resource) was persisted verbatim
and merrily echoed back by Describe. AWS rejects unknown keys with
ValidationError; match that so Terraform surfaces the typo at plan time.
Validate each key against DefaultTargetGroupAttributes() /
DefaultLoadBalancerAttributes(lb.Type) and return ErrorValidationError on the
first unknown key, logging both the arn and the offending key name.

* perf: skip no-op Put in ELBv2 ModifyAttributes (mulga-914)

The unconditional PutTargetGroup/PutLoadBalancer fired on every Modify call,
even when the caller sent an empty attribute list or when every submitted
value already matched the stored value. Terraform re-applies the same
attribute set on every drift check, so the steady-state path was doing a
pointless NATS/KV round-trip per call and unnecessarily expanding the
read-modify-write race window.

Track a dirty flag per Modify call: compare each submitted key's value
against tg.Attributes/lb.Attributes and only write to the map (and allocate
the map lazily) when it actually differs. Skip the Put entirely if nothing
changed. Added regression tests that capture the KV entry revision before
and after a no-op modify to prove the write was skipped.

* test: assert exact ModifyAttributes echoed key/value pairs (mulga-917)

Both round-trip tests used assert.Len(t, modOut.Attributes, 2) and never
checked that the returned Attributes actually contained the submitted
keys/values. A regression that pointed at the wrong source, dropped Value,
or returned a copy of defaults would have passed the length check
unchallenged. Build a modMap from the echoed response and assert each
expected Key/Value pair explicitly.

* test: cover sequential ModifyAttributes merge and same-key overwrite (mulga-916)

All existing round-trip tests did one Modify then Describe. No test covered:
Modify {A}, Modify {B}, Describe, assert BOTH keys present; or Modify
overwriting the same key on a second call. A future refactor that replaced
tg.Attributes instead of mutating in place would pass every existing test
but silently wipe previous attributes on each Modify — the most likely
real-world ALB/TG bug. Add sequential-merge tests for both TG and LB
handlers that exercise multi-key merge, same-key overwrite, and
cross-survival of unrelated keys.
2026-04-10 21:31:43 +10:00
Josh Siviour 67bdc9d4af fix: nginx-alb terraform example uses private IPs for instances (#131)
The ALB reaches backends over the private VPC network, so the Nginx
instances no longer need public IPs. Drop associate_public_ip_address
and map_public_ip_on_launch, and swap the instance public-IP outputs
for private-IP ones.

The *.elb.spinifex.local DNS name does not resolve from the operator's
host, so document an AWS CLI one-liner (describe-load-balancers with
an AvailabilityZones[].LoadBalancerAddresses[].IpAddress query) that
retrieves the ALB's public IP for curl/browser testing.
2026-04-10 18:54:12 +10:00
Josh Siviour 8d2830481d update alb terraform example (#130) 2026-04-10 16:52:52 +10:00
Josh Siviour 6238988a8f feat: add Terraform nginx-alb example (#129)
Deploys two Nginx EC2 instances behind an Application Load Balancer
on Spinifex, exercising VPC, public subnets, IGW, security group,
key pair + cloud-init, ALB, target group, and HTTP listener end-to-end.

Also gitignore generated *.pem files from tofu local_file outputs.
2026-04-10 15:56:50 +10:00
Julian Sommer 4365fb0958 feat: update guide 2026-04-10 15:05:46 +10:00
Julian Sommer 48c90b795a feat: migration framework (#127) 2026-04-10 14:34:15 +10:00
Julian Sommer 18684d5c8b fix: remove InsecureSkipVerify and normalize 0.0.0.0 connect addresses (#125) 2026-04-10 13:30:13 +10:00
Julian Sommer 708bb1d340 feat: enforce nats authentication (again) (#126) 2026-04-10 12:18:46 +10:00