23 Commits

Author SHA1 Message Date
benvin 3b229deb1b Merge pull request 'Add kea-operator Gitea repository' (#53) from benvin/kea-operator-repo into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #53
2026-08-02 17:22:40 +10:00
unkinben 8fc5105d10 Add kea-operator Gitea repository
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Provision the repo for the new Kea DHCP Kubernetes operator that replaces
the ISC dhcpd PXE-boot VM.

- add config/git.unkin.net/unkin/repository/kea-operator.yaml
Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
2026-08-02 16:56:38 +10:00
benvin c920451844 Merge pull request 'argocd-apps: require the vector-test PR check' (#52) from benvin/argocd-vector-test-required into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #52
2026-07-30 21:10:33 +10:00
unkinben 493c33483b argocd-apps: require the vector-test PR check
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-30 21:09:39 +10:00
benvin 58dafb7ac2 Merge pull request 'Add bootapi-images repository' (#51) from benvin/bootapi-images-repo into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #51
2026-07-30 21:03:18 +10:00
unkinben 68584acffb Add bootapi-images repository
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
bootapi-images builds the AlmaLinux node rootfs tarballs that bootapi's
image-based provisioning (liveimg) unpacks, and on a v* tag publishes
almalinux9-node-<ver>.tar.zst to the artifactapi rootfs-images local repo. Split
out of bootapi-templates so that repo stays templates-only.

Modeled on the bootapi-templates entry: public, squash, delete-branch-after-merge,
main protection requiring the pre-commit + lint Woodpecker checks and benvin
approval.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-30 20:55:45 +10:00
benvin bd66d55c99 Merge pull request 'Add bootapi-templates repository' (#50) from benvin/bootapi-templates-repo into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #50
2026-07-28 23:08:51 +10:00
unkinben fe662001c5 Add bootapi-templates repository
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
bootapi-templates holds the live kickstart/iPXE templates and the distro catalog
that bootapi pulls via git-sync (every ~3m, like argocd), so template/OS changes
ship without rebuilding bootapi. Its CI validates that every template parses and
renders for every catalog distro (via 'bootapi validate') plus shellcheck on the
%post snippets.

Modeled on the bootapi repo entry: public, squash, delete-branch-after-merge,
main protection requiring the pre-commit + validate Woodpecker checks and benvin
approval.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-28 22:35:46 +10:00
benvin c0fc234a1c Merge pull request 'Add bootapi repository' (#49) from benvin/bootapi-repo into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #49
2026-07-28 18:41:49 +10:00
unkinben d53b6841e1 Add bootapi repository
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
bootapi is a Go service replacing Cobbler's PXE/kickstart side: it renders
kickstart files and iPXE boot scripts from NetBox data and serves them to
PXE-booting hosts. The ENC half already moved to encapi; bootapi covers the
provisioning/boot half.

Adds config/git.unkin.net/unkin/repository/bootapi.yaml modeled on encapi:
public repo, squash merges, delete-branch-after-merge, main branch protection
requiring the pre-commit/test/build Woodpecker checks and benvin approval.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-28 16:58:21 +10:00
benvin 38849ba79f Merge pull request 'vault provider: skip child token creation in CI' (#48) from benvin/vault-skip-child-token into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #48
2026-07-27 22:03:28 +10:00
unkinben 0385d2a1ef vault provider: skip child token creation in CI
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 21:52:24 +10:00
benvin c068f56eca Merge pull request 'Add logarchiver repository' (#47) from benvin/logarchiver-repo into main
ci/woodpecker/push/apply Pipeline failed
Reviewed-on: #47
2026-07-27 21:48:24 +10:00
benvin 99b13d81bc Merge pull request 'user: add gitea-vault-admin site-admin bot with one-time Vault KV seeding' (#46) from benvin/gitea-vault-admin into main
ci/woodpecker/push/apply Pipeline failed
Reviewed-on: #46
2026-07-27 21:47:31 +10:00
benvin 9cee6024d3 Add logarchiver repository
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Registers the logarchiver Gitea repo: a k8s Go service that archives
raw logs from NATS JetStream to S3 as zstd-compressed, GPG-encrypted,
indexed objects, plus a CLI to search and retrieve them. It replaces the
Vector archiver leg of the centralized logging stack (argocd-apps #296)
with zstd compression, OpenPGP encryption keyed from the Vault GPG
secrets engine, and a ClickHouse-backed searchable index.

- Add config/git.unkin.net/unkin/repository/logarchiver.yaml modeled on
  the existing Go-service repos (encapi, cephrgw-operator): public repo,
  main default branch, squash merges, branch protection requiring the
  woodpecker pre-commit/test/build PR checks with benvin approval.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 21:36:15 +10:00
unkinben bd7bef9f99 user: add gitea-vault-admin site-admin bot and one-time Vault KV seeding
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Why: the vault-plugin-secrets-gitea engine needs a purpose-built Gitea
site-admin credential to mint and delete per-user tokens. Provision that
account and its Vault seed here so the credential is generated once and never
exposed. Ben asked that the password be written to Vault exactly once and
never updated afterwards.

Change:
- Add config/git.unkin.net/user/gitea-vault-admin.yaml: a local site-admin bot
  (admin: true, limited visibility, no org/repo creation).
- Extend the user module with an optional vault_seed_path/vault_seed_mount: when
  set, write the account's generated password to Vault KV as admin_username and
  admin_password via vault_kv_secret_v2, with lifecycle ignore_changes on
  data_json so the write is create-only and never churns (random_password
  already never regenerates). This keeps the seed stable and prevents a re-apply
  from overwriting a password later rotated out-of-band by rotate-root.
- Add the hashicorp/vault provider (module plus root generate block); it reads
  VAULT_ADDR and VAULT_TOKEN already exported by the Makefile k8s login.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 19:26:24 +10:00
benvin 96092cf7a1 Merge pull request 'Manage Gitea users; add teabot personality bot accounts' (#45) from benvin/teabot-users into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #45
2026-07-27 17:33:25 +10:00
unkinben fd82876f5f Manage Gitea users; add teabot personality bot accounts
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add a data-driven 'user' config kind so Gitea accounts are declared as
config/git.unkin.net/user/<name>.yaml, mirroring the existing repository
and team kinds. Wire the yaml into a new modules/user submodule that
creates a gitea_user (provider go-gitea/gitea 0.7.0, already pinned).

The provider's user resource requires a password; generate a per-user
random_password so nothing sensitive is hardcoded and only a placeholder
lives in state (tokens come later from vault-plugin-secrets-gitea).

Provision teabot's implementer and reviewer personality accounts with a
conservative posture: not site admins, no org creation, no repo creation,
limited profile visibility.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 17:22:19 +10:00
benvin ee08fd5585 Merge pull request 'Add gitea vault engine + provider repositories' (#44) from benvin/gitea-vault-repos into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #44
2026-07-27 00:40:20 +10:00
unkinben 4c5fe742f0 Add gitea vault engine + provider repositories
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Create the Gitea repositories for the new vault-plugin-secrets-gitea
secrets engine and its companion terraform-provider-giteavaultsecret,
mirroring the rancher pair's settings.

- add config/git.unkin.net/unkin/repository/vault-plugin-secrets-gitea.yaml
- add config/git.unkin.net/unkin/repository/terraform-provider-giteavaultsecret.yaml

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 00:34:28 +10:00
benvin aa9b2eac80 Merge pull request 'Add teabot repository (file)' (#43) from benvin/teabot-repo-add into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #43
2026-07-26 23:22:20 +10:00
benvin 3f39229c29 Merge pull request 'Add vault-tools repository' (#41) from benvin/vault-tools-repo into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #41
2026-07-26 23:19:36 +10:00
unkinben 42187f86d5 Add vault-tools repository
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-26 23:18:08 +10:00
21 changed files with 400 additions and 0 deletions
+8
View File
@@ -33,6 +33,14 @@ locals {
})
if length(regexall("/team/", file_path)) > 0
}
user = {
for file_path, content in local.all_configs :
"${split("/", file_path)[0]}/${trimsuffix(basename(file_path), ".yaml")}" => merge(content, {
username = trimsuffix(basename(file_path), ".yaml")
gitea_url = split("/", file_path)[0]
})
if length(regexall("/user/", file_path)) > 0
}
branch_protection = merge([
for file_path, content in local.all_configs : {
for idx, rule in try(content.branch_protection, []) :
@@ -8,5 +8,6 @@ branch_protection:
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/kubeconform"
- "ci/woodpecker/pr/vector-test"
approval_whitelist_users:
- "benvin"
@@ -0,0 +1,13 @@
description: "Builds AlmaLinux node rootfs tarballs for bootapi image-based provisioning; on v* tag publishes almalinux9-node-<ver>.tar.zst to the artifactapi rootfs-images repo."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/lint"
approval_whitelist_users:
- "benvin"
@@ -0,0 +1,13 @@
description: "Live kickstart/iPXE templates + distro catalog for bootapi. Pulled by bootapi via git-sync; CI validates templates render for every distro."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/validate"
approval_whitelist_users:
- "benvin"
@@ -0,0 +1,14 @@
description: "PXE/kickstart boot service replacing Cobbler. Renders kickstart + iPXE from NetBox data over HTTP. Go API."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/build"
approval_whitelist_users:
- "benvin"
@@ -0,0 +1,14 @@
description: "Kubernetes operator for managing Kea DHCP clusters, subnets, and PXE client classes"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/build"
approval_whitelist_teams:
- "Owners"
@@ -0,0 +1,14 @@
description: "Archives raw logs from NATS JetStream to S3 as zstd-compressed, GPG-encrypted, indexed objects; plus a CLI to search and retrieve them. Go service + CLI."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/build"
approval_whitelist_users:
- "benvin"
@@ -0,0 +1,19 @@
description: "Terraform provider for the Vault/OpenBao Gitea token secrets engine (giteavaultsecret)"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
# Squash-only: the gitea provider has no "default merge style", so we restrict
# the allowed styles to squash to force it.
allow_merge_commits: false
allow_rebase: false
allow_rebase_explicit: false
allow_squash_merge: true
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
approval_whitelist_teams:
- "Owners"
@@ -0,0 +1,19 @@
description: "HashiCorp Vault / OpenBao secrets engine for Gitea: mints ephemeral, scoped per-user access tokens via the admin API, with seeded-admin basic-auth and root password rotation"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
# Squash-only: the gitea provider has no "default merge style", so we restrict
# the allowed styles to squash to force it.
allow_merge_commits: false
allow_rebase: false
allow_rebase_explicit: false
allow_squash_merge: true
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
approval_whitelist_teams:
- "Owners"
@@ -0,0 +1,14 @@
description: "Monorepo of Go CLI tools for managing Vault tokens across multiple vault instances: vctl (login/renew per-context tokens) and vctx (context-aware wrapper around the vault CLI)."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
approval_whitelist_users:
- "benvin"
@@ -0,0 +1,21 @@
# Purpose-built Gitea site-admin bot for the vault-plugin-secrets-gitea engine.
# The engine seeds itself from this account's credentials (Basic Auth) to mint
# and delete per-user tokens for any user. Its randomly generated password is
# written ONCE to Vault KV (vault_seed_path below) and never updated after; the
# Vault gitea engine reads that seed only when first creating gitea/config and
# then rotates it (rotate-root) so only Vault holds the live password.
#
# A local (not external-auth) account with 2FA disabled is required so the
# engine can change the password via the admin API during rotate-root.
email: gitea-vault-admin@unkin.net
full_name: "Gitea Vault Admin"
description: "site-admin bot; credentials seeded to Vault for vault-plugin-secrets-gitea"
# Site admin so the engine may mint/delete tokens for any user. No org/repo
# creation; profile visible only to signed-in users.
visibility: limited
admin: true
allow_create_organization: false
max_repo_creation: 0
# Seed this account's generated password to Vault KV (mount "kv") at this path,
# where the Vault gitea secrets engine reads it at creation time. Written once.
vault_seed_path: "service/vault/au/syd1/secret_backend/gitea/config"
@@ -0,0 +1,14 @@
# teabot "implementer" personality account.
# Used by the teabot daemon (unkin/teabot) to open issues/PRs as a distinct
# identity so its work is attributable. Auth is via an API token issued
# out-of-band (vault-plugin-secrets-gitea / static KV) -- no token or usable
# password lives in this repo's state beyond the generated placeholder.
email: teabot-implementer@unkin.net
full_name: "Teabot Implementer"
description: "teabot implementer bot -- automated agent (unkin/teabot)"
# Conservative bot posture: not a site admin, cannot create orgs or repos,
# profile visible only to signed-in users.
visibility: limited
admin: false
allow_create_organization: false
max_repo_creation: 0
@@ -0,0 +1,14 @@
# teabot "reviewer" personality account.
# Used by the teabot daemon (unkin/teabot) to review pull requests as a distinct
# identity so its reviews are attributable. Auth is via an API token issued
# out-of-band (vault-plugin-secrets-gitea / static KV) -- no token or usable
# password lives in this repo's state beyond the generated placeholder.
email: teabot-reviewer@unkin.net
full_name: "Teabot Reviewer"
description: "teabot reviewer bot -- automated agent (unkin/teabot)"
# Conservative bot posture: not a site admin, cannot create orgs or repos,
# profile visible only to signed-in users.
visibility: limited
admin: false
allow_create_organization: false
max_repo_creation: 0
+1
View File
@@ -21,4 +21,5 @@ inputs = {
repository = local.config.repository
branch_protection = local.config.branch_protection
team = local.config.team
user = local.config.user
}
+9
View File
@@ -11,6 +11,15 @@ provider "woodpecker" {
server = "https://ci.k8s.syd1.au.unkin.net"
}
# Address + token come from VAULT_ADDR / VAULT_TOKEN in the environment (the
# Makefile authenticates via k8s auth before running terragrunt). Used to seed
# bot-account credentials into Vault KV (see modules/user vault_seed_path).
# skip_child_token: the CI token from the k8s auth role cannot create child
# tokens (apply fails with "failed to create limited child token").
provider "vault" {
skip_child_token = true
}
terraform {
backend "consul" {
address = "https://consul.service.consul"
+21
View File
@@ -38,6 +38,27 @@ module "repository" {
depends_on = [module.organisation]
}
module "user" {
source = "./modules/user"
for_each = var.user
username = each.value.username
email = each.value.email
login_name = each.value.login_name
full_name = each.value.full_name
description = each.value.description
visibility = each.value.visibility
admin = each.value.admin
restricted = each.value.restricted
active = each.value.active
allow_create_organization = each.value.allow_create_organization
max_repo_creation = each.value.max_repo_creation
must_change_password = each.value.must_change_password
vault_seed_path = each.value.vault_seed_path
vault_seed_mount = each.value.vault_seed_mount
}
module "team" {
source = "./modules/team"
@@ -0,0 +1,58 @@
# The gitea_user resource requires a password argument. These accounts
# authenticate via API tokens (issued out-of-band by the
# vault-plugin-secrets-gitea engine, or a static KV entry initially), so the
# password is never used interactively. It is generated here so nothing
# sensitive is hardcoded, and kept only in state.
resource "random_password" "this" {
length = 32
special = true
override_special = "!@#%^&*()-_=+"
}
resource "gitea_user" "this" {
username = var.username
login_name = coalesce(var.login_name, var.username)
email = var.email
password = random_password.this.result
full_name = var.full_name
description = var.description
visibility = var.visibility
admin = var.admin
restricted = var.restricted
active = var.active
allow_create_organization = var.allow_create_organization
max_repo_creation = var.max_repo_creation
must_change_password = var.must_change_password
# Do not e-mail the (possibly unmonitored) bot address on creation.
send_notification = false
lifecycle {
# The provider cannot read the password back from the API; ignore it so the
# account never shows perpetual drift once created.
ignore_changes = [password]
}
}
# Optional one-time seed of the account's credentials to Vault KV, for the
# vault-plugin-secrets-gitea engine to consume when it first creates gitea/config.
# random_password never regenerates (no keepers), so the seed is stable; the
# ignore_changes below makes the write strictly create-only, so a later
# rotate-root on the Vault side (which diverges the live password from this seed)
# is never clobbered by a re-apply here.
resource "vault_kv_secret_v2" "seed" {
count = var.vault_seed_path != null ? 1 : 0
mount = var.vault_seed_mount
name = var.vault_seed_path
data_json = jsonencode({
admin_username = var.username
admin_password = random_password.this.result
})
lifecycle {
ignore_changes = [data_json]
}
}
@@ -0,0 +1,17 @@
terraform {
required_version = ">= 1.10"
required_providers {
gitea = {
source = "go-gitea/gitea"
version = "0.7.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.5"
}
vault = {
source = "hashicorp/vault"
version = ">= 4.3"
}
}
}
@@ -0,0 +1,87 @@
variable "username" {
description = "Username of the Gitea account (immutable; changing it recreates the user)"
type = string
}
variable "login_name" {
description = "Login name, may differ from username. Defaults to the username."
type = string
default = null
}
variable "email" {
description = "E-Mail address of the user"
type = string
}
variable "full_name" {
description = "Display / full name of the user"
type = string
default = ""
}
variable "description" {
description = "Profile description of the user"
type = string
default = ""
}
variable "visibility" {
description = "Profile visibility: public, limited or private"
type = string
default = "limited"
}
variable "admin" {
description = "Whether the user is a site administrator"
type = bool
default = false
}
variable "restricted" {
description = "Restricted users can only access repositories/orgs they are explicitly added to"
type = bool
default = false
}
variable "active" {
description = "Whether the account is active"
type = bool
default = true
}
variable "allow_create_organization" {
description = "Whether the user may create organisations"
type = bool
default = false
}
variable "max_repo_creation" {
description = "Maximum number of repositories the user may create (0 disallows creation)"
type = number
default = 0
}
variable "must_change_password" {
description = "Force a password change on first login"
type = bool
default = false
}
variable "vault_seed_path" {
description = <<-EOT
Optional. When set, the account's generated password is seeded ONCE to Vault
KV at this secret name (under vault_seed_mount) as admin_username +
admin_password, for the vault-plugin-secrets-gitea engine to consume at
creation time. The write is create-only; subsequent changes are ignored so
the seed never churns (and never overwrites a rotated password).
EOT
type = string
default = null
}
variable "vault_seed_mount" {
description = "KV v2 mount holding the seeded credential (used only when vault_seed_path is set)"
type = string
default = "kv"
}
+8
View File
@@ -9,5 +9,13 @@ terraform {
source = "Kichiyaki/woodpecker"
version = "0.5.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.5"
}
vault = {
source = "hashicorp/vault"
version = ">= 4.3"
}
}
}
+21
View File
@@ -64,6 +64,27 @@ variable "branch_protection" {
}
variable "user" {
description = "Map of Gitea user accounts to create"
type = map(object({
username = string
email = string
login_name = optional(string)
full_name = optional(string, "")
description = optional(string, "")
visibility = optional(string, "limited")
admin = optional(bool, false)
restricted = optional(bool, false)
active = optional(bool, true)
allow_create_organization = optional(bool, false)
max_repo_creation = optional(number, 0)
must_change_password = optional(bool, false)
vault_seed_path = optional(string)
vault_seed_mount = optional(string, "kv")
}))
default = {}
}
variable "team" {
description = "Map of teams to create"
type = map(object({