feat: Initial commit, empty from template.

This commit is contained in:
2026-01-27 23:27:14 +01:00
commit f77a808d74
133 changed files with 4479 additions and 0 deletions

44
.beads/.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# SQLite databases
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
sync-state.json
last-touched
# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
# Legacy database files
db.sqlite
bd.db
# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect
# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
beads.left.jsonl
beads.left.meta.json
beads.right.jsonl
beads.right.meta.json
# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
sync_base.jsonl
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
# They would override fork protection in .git/info/exclude, allowing
# contributors to accidentally commit upstream issue databases.
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
# are tracked by git by default since no pattern above ignores them.

81
.beads/README.md Normal file
View File

@@ -0,0 +1,81 @@
# Beads - AI-Native Issue Tracking
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
## What is Beads?
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
## Quick Start
### Essential Commands
```bash
# Create new issues
bd create "Add user authentication"
# View all issues
bd list
# View issue details
bd show <issue-id>
# Update issue status
bd update <issue-id> --status in_progress
bd update <issue-id> --status done
# Sync with git remote
bd sync
```
### Working with Issues
Issues in Beads are:
- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
- **Branch-aware**: Issues can follow your branch workflow
- **Always in sync**: Auto-syncs with your commits
## Why Beads?
**AI-Native Design**
- Built specifically for AI-assisted development workflows
- CLI-first interface works seamlessly with AI coding agents
- No context switching to web UIs
🚀 **Developer Focused**
- Issues live in your repo, right next to your code
- Works offline, syncs when you push
- Fast, lightweight, and stays out of your way
🔧 **Git Integration**
- Automatic sync with git commits
- Branch-aware issue tracking
- Intelligent JSONL merge resolution
## Get Started with Beads
Try Beads in your own projects:
```bash
# Install Beads
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
# Initialize in your repo
bd init
# Create your first issue
bd create "Try out Beads"
```
## Learn More
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
- **Quick Start Guide**: Run `bd quickstart`
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
---
*Beads: Issue tracking that moves at the speed of thought*

62
.beads/config.yaml Normal file
View File

@@ -0,0 +1,62 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
# issue-prefix: ""
# Use no-db mode: load from JSONL, no SQLite, write back after each command
# When true, bd will use .beads/issues.jsonl as the source of truth
# instead of SQLite database
# no-db: false
# Disable daemon for RPC communication (forces direct database access)
# no-daemon: false
# Disable auto-flush of database to JSONL after mutations
# no-auto-flush: false
# Disable auto-import from JSONL when it's newer than database
# no-auto-import: false
# Enable JSON output by default
# json: false
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
# actor: ""
# Path to database (overridden by BEADS_DB or --db)
# db: ""
# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
# auto-start-daemon: true
# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
# flush-debounce: "5s"
# Git branch for beads commits (bd sync will commit to this branch)
# IMPORTANT: Set this for team projects so all clones use the same sync branch.
# This setting persists across clones (unlike database config which is gitignored).
# Can also use BEADS_SYNC_BRANCH env var for local override.
# If not set, bd sync will require you to run 'bd config set sync.branch <branch>'.
# sync-branch: "beads-sync"
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct JSONL
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# Integration settings (access with 'bd config get/set')
# These are stored in the database, not in this file:
# - jira.url
# - jira.project
# - linear.url
# - linear.api-key
# - github.org
# - github.repo

View File

4
.beads/metadata.json Normal file
View File

@@ -0,0 +1,4 @@
{
"database": "beads.db",
"jsonl_export": "issues.jsonl"
}

22
.editorconfig Normal file
View File

@@ -0,0 +1,22 @@
; https://editorconfig.org/
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.cue]
indent_style = tab
indent_size = 4
[*.md]
indent_size = 4
trim_trailing_whitespace = false
[*.sh]
indent_size = 4

10
.gitattributes vendored Normal file
View File

@@ -0,0 +1,10 @@
* text=auto eol=lf
*.env linguist-detectable linguist-language=SHELL
*.json linguist-detectable linguist-language=JSON
*.json5 linguist-detectable linguist-language=JSON5
*.md linguist-detectable linguist-language=MARKDOWN
*.sh linguist-detectable linguist-language=SHELL
*.toml linguist-detectable linguist-language=TOML
*.yml linguist-detectable linguist-language=YAML
*.yaml linguist-detectable linguist-language=YAML
*.yaml.j2 linguist-detectable linguist-language=YAML

43
.github/labeler.yaml vendored Normal file
View File

@@ -0,0 +1,43 @@
---
area/bootstrap:
- changed-files:
- any-glob-to-any-file:
- bootstrap/**/*
area/docs:
- changed-files:
- any-glob-to-any-file:
- README.md
area/github:
- changed-files:
- any-glob-to-any-file:
- .github/**/*
area/kubernetes:
- changed-files:
- any-glob-to-any-file:
- kubernetes/**/*
area/mise:
- changed-files:
- any-glob-to-any-file:
- .mise.toml
area/renovate:
- changed-files:
- any-glob-to-any-file:
- .renovate/**/*
- .renovaterc.json5
area/scripts:
- changed-files:
- any-glob-to-any-file:
- scripts/**/*
area/talos:
- changed-files:
- any-glob-to-any-file:
- talos/**/*
area/taskfile:
- changed-files:
- any-glob-to-any-file:
- .taskfiles/**/*
- Taskfile.yaml
area/templates:
- changed-files:
- any-glob-to-any-file:
- templates/**/*

47
.github/labels.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
---
# Areas
- name: area/bootstrap
color: "0e8a16"
- name: area/docs
color: "0e8a16"
- name: area/github
color: "0e8a16"
- name: area/kubernetes
color: "0e8a16"
- name: area/mise
color: "0e8a16"
- name: area/renovate
color: "0e8a16"
- name: area/scripts
color: "0e8a16"
- name: area/talos
color: "0e8a16"
- name: area/templates
color: "0e8a16"
- name: area/taskfile
color: "0e8a16"
# Renovate Types
- name: renovate/container
color: "027fa0"
- name: renovate/github-action
color: "027fa0"
- name: renovate/grafana-dashboard
color: "027fa0"
- name: renovate/github-release
color: "027fa0"
- name: renovate/helm
color: "027fa0"
# Semantic Types
- name: type/digest
color: "ffeC19"
- name: type/patch
color: "ffeC19"
- name: type/minor
color: "ff9800"
- name: type/major
color: "f6412d"
# Uncategorized
- name: community
color: "370fb2"
- name: hold
color: "ee0701"

5
.github/release.yaml vendored Normal file
View File

@@ -0,0 +1,5 @@
changelog:
exclude:
authors:
- github-actions
- renovate

19
.github/tests/nodes.yaml vendored Normal file
View File

@@ -0,0 +1,19 @@
nodes:
- name: k8s-0
address: 10.10.10.100
controller: true
disk: /dev/sdfake
mac_addr: 00:00:00:00:00:00
schematic_id: "376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba"
- name: k8s-1
address: 10.10.10.101
controller: false
disk: /dev/sdfake
mac_addr: 00:00:00:00:00:01
schematic_id: "376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba"
mtu: 1500
secureboot: true
encrypt_disk: true
kernel_modules:
- nvidia
- nvidia_uvm

22
.github/tests/private.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
---
node_cidr: "10.10.10.0/24"
# node_default_gateway: ""
# node_vlan_tag:
# cluster_pod_cidr: ""
# cluster_svc_cidr: ""
# node_dns_servers: []
# node_ntp_servers: []
cluster_api_addr: "10.10.10.254"
# cluster_api_tls_sans: []
cluster_gateway_addr: "10.10.10.252"
cluster_dns_gateway_addr: "10.10.10.253"
repository_name: "onedr0p/cluster-template"
# repository_branch: ""
repository_visibility: "private"
cloudflare_domain: "example.com"
cloudflare_token: "fake"
cloudflare_gateway_addr: "10.10.10.251"
# cilium_bgp_router_addr: ""
# cilium_bgp_router_asn: ""
# cilium_bgp_node_asn: ""
# cilium_loadbalancer_mode: ""

22
.github/tests/public.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
---
node_cidr: "10.10.10.0/24"
node_default_gateway: "10.10.10.1"
node_vlan_tag: "100"
cluster_pod_cidr: "10.42.0.0/16"
cluster_svc_cidr: "10.43.0.0/16"
node_dns_servers: ["1.1.1.1"]
node_ntp_servers: ["162.159.200.123"]
cluster_api_addr: "10.10.10.254"
cluster_api_tls_sans: ["example.com"]
cluster_gateway_addr: "10.10.10.252"
cluster_dns_gateway_addr: "10.10.10.253"
repository_name: "onedr0p/cluster-template"
repository_branch: "main"
repository_visibility: "public"
cloudflare_domain: "example.com"
cloudflare_token: "fake"
cloudflare_gateway_addr: "10.10.10.251"
cilium_loadbalancer_mode: "dsr"
cilium_bgp_router_addr: "10.10.1.1"
cilium_bgp_router_asn: "64513"
cilium_bgp_node_asn: "64514"

71
.github/workflows/e2e.yaml vendored Normal file
View File

@@ -0,0 +1,71 @@
---
name: "e2e"
on:
workflow_dispatch:
pull_request:
branches: ["main"]
paths-ignore:
- kubernetes/**
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
cancel-in-progress: true
jobs:
configure:
if: ${{ github.repository == 'onedr0p/cluster-template' }}
name: configure
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
config-files:
- public
- private
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup mise
uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
with:
cache: false
- name: Run init task
run: task init
- name: Prepare files
run: |
cp ./.github/tests/${{ matrix.config-files }}.yaml cluster.yaml
cp ./.github/tests/nodes.yaml nodes.yaml
echo '{"AccountTag":"fake","TunnelSecret":"fake","TunnelID":"fake"}' > cloudflare-tunnel.json
touch kubeconfig
- name: Run configure task
run: task configure --yes
- name: Run generate talconfig task
run: |
FILENAME=talos/talsecret.sops.yaml
talhelper gensecret | sops --filename-override $FILENAME --encrypt /dev/stdin > $FILENAME
task talos:generate-config
- name: Run flux-local test
uses: docker://ghcr.io/allenporter/flux-local:v8.1.0@sha256:37c3c4309a351830b04f93c323adfcb0e28c368001818cd819cbce3e08828261
with:
args: test --enable-helm --all-namespaces --path /github/workspace/kubernetes/flux/cluster -v
- name: Dry run bootstrap talos task
run: task bootstrap:talos --dry
- name: Dry run bootstrap apps task
run: task bootstrap:apps --dry
- name: Run reset task
run: task template:reset --yes
- name: Run cleanup task
run: task template:tidy --yes

121
.github/workflows/flux-local.yaml vendored Normal file
View File

@@ -0,0 +1,121 @@
---
name: "Flux Local"
on:
pull_request:
branches: ["main"]
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
cancel-in-progress: true
jobs:
pre-job:
name: Flux Local Pre-Job
runs-on: ubuntu-latest
outputs:
any_changed: ${{ steps.changed-files.outputs.any_changed }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get Changed Files
id: changed-files
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: kubernetes/**
test:
name: Flux Local Test
needs: pre-job
runs-on: ubuntu-latest
if: ${{ needs.pre-job.outputs.any_changed == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run flux-local test
uses: docker://ghcr.io/allenporter/flux-local:v8.1.0
with:
args: test --enable-helm --all-namespaces --path /github/workspace/kubernetes/flux/cluster -v
diff:
name: Flux Local Diff
needs: pre-job
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
strategy:
matrix:
resources: ["helmrelease", "kustomization"]
max-parallel: 4
fail-fast: false
if: ${{ needs.pre-job.outputs.any_changed == 'true' }}
steps:
- name: Checkout Pull Request Branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: pull
- name: Checkout Default Branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: "${{ github.event.repository.default_branch }}"
path: default
- name: Run flux-local diff
uses: docker://ghcr.io/allenporter/flux-local:v8.1.0
with:
args: >-
diff ${{ matrix.resources }}
--unified 6
--path /github/workspace/pull/kubernetes/flux/cluster
--path-orig /github/workspace/default/kubernetes/flux/cluster
--strip-attrs "helm.sh/chart,checksum/config,app.kubernetes.io/version,chart"
--limit-bytes 10000
--all-namespaces
--sources "flux-system"
--output-file diff.patch
- name: Generate Diff
id: diff
run: |
cat diff.patch;
{
echo 'diff<<EOF'
cat diff.patch
echo EOF
} >> "$GITHUB_OUTPUT";
{
echo "### Diff"
echo '```diff'
cat diff.patch
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Add Comment
if: ${{ steps.diff.outputs.diff != '' }}
continue-on-error: true
uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
with:
message-id: "${{ github.event.pull_request.number }}/kubernetes/${{ matrix.resources }}"
message-failure: Diff was not successful
message: |
```diff
${{ steps.diff.outputs.diff }}
```
flux-local-status:
name: Flux Local Success
needs: ["test", "diff"]
runs-on: ubuntu-latest
if: ${{ always() }}
steps:
- name: Any jobs failed?
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
- name: All jobs passed or skipped?
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"

25
.github/workflows/label-sync.yaml vendored Normal file
View File

@@ -0,0 +1,25 @@
---
name: "Label Sync"
on:
workflow_dispatch:
push:
branches: ["main"]
paths: [".github/labels.yaml"]
jobs:
label-sync:
name: Label Sync
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Sync Labels
uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
with:
config-file: .github/labels.yaml
delete-other-labels: true

21
.github/workflows/labeler.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
---
name: "Labeler"
on:
workflow_dispatch:
pull_request_target:
branches: ["main"]
jobs:
labeler:
name: Labeler
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Labeler
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
configuration-path: .github/labeler.yaml

56
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,56 @@
---
name: "Release"
on:
workflow_dispatch:
schedule:
- cron: "0 0 1 * *" # 1st of every month at midnight
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Get Previous Release Tag and Determine Next Tag
id: determine-next-tag
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
result-encoding: string
script: |
const { data: releases } = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 1,
});
let previousTag = "0.0.0"; // Default if no previous release exists
if (releases.length > 0) {
previousTag = releases[0].tag_name;
}
const [previousMajor, previousMinor, previousPatch] = previousTag.split('.').map(Number);
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1; // Months are 0-indexed in JavaScript
const nextMajorMinor = `${currentYear}.${currentMonth}`;
let nextPatch;
if (`${previousMajor}.${previousMinor}` === nextMajorMinor) {
console.log("Month release already exists for the year. Incrementing patch number by 1.");
nextPatch = previousPatch + 1;
} else {
console.log("Month release does not exist for the year. Starting with patch number 0.");
nextPatch = 0;
}
return `${nextMajorMinor}.${nextPatch}`;
- name: Create Release
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
with:
generateReleaseNotes: true
tag: "${{ steps.determine-next-tag.outputs.result }}"
token: "${{ secrets.GITHUB_TOKEN }}"

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Secrets
*.pub
*.key
*.decrypted~*.yaml
/age.key
/cloudflare-tunnel.json
/github-deploy.key
/github-deploy.key.pub
/github-push-token.txt
# Template config files
/cluster.yaml
/nodes.yaml
# Kubernetes
kubeconfig
talosconfig
# Misc.
.private/
.task/
.venv/
.DS_Store
Thumbs.db

26
.mise.toml Normal file
View File

@@ -0,0 +1,26 @@
[env]
_.python.venv = { path = "{{config_root}}/.venv", create = true }
KUBECONFIG = "{{config_root}}/kubeconfig"
SOPS_AGE_KEY_FILE = "{{config_root}}/age.key"
TALOSCONFIG = "{{config_root}}/talos/clusterconfig/talosconfig"
[tools]
"python" = "3.14.2"
"pipx:makejinja" = "2.8.2"
"aqua:budimanjojo/talhelper" = "3.1.3"
"aqua:cilium/cilium-cli" = "0.19.0"
"aqua:cli/cli" = "2.86.0"
"aqua:cloudflare/cloudflared" = "2026.1.1"
"aqua:cue-lang/cue" = "0.15.3"
"aqua:FiloSottile/age" = "1.3.1"
"aqua:fluxcd/flux2" = "2.7.5"
"aqua:getsops/sops" = "3.11.0"
"aqua:go-task/task" = "3.47.0"
"aqua:helm/helm" = "4.1.0"
"aqua:helmfile/helmfile" = "1.2.3"
"aqua:jqlang/jq" = "1.8.1"
"aqua:kubernetes-sigs/kustomize" = "5.7.1"
"aqua:kubernetes/kubernetes/kubectl" = "1.35.0"
"aqua:mikefarah/yq" = "4.50.1"
"aqua:siderolabs/talos" = "1.12.2"
"aqua:yannh/kubeconform" = "0.7.0"

180
.renovaterc.json5 Normal file
View File

@@ -0,0 +1,180 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: [
"config:recommended",
"docker:enableMajor",
"helpers:pinGitHubActionDigests",
":automergeBranch",
":dependencyDashboard",
":disableRateLimiting",
":semanticCommits",
],
dependencyDashboard: true,
dependencyDashboardTitle: "Renovate Dashboard :robot:",
schedule: ["every weekend"],
ignorePaths: ["**/*.sops.*"],
flux: {
managerFilePatterns: ["/(^|/)kubernetes/.+\\.ya?ml(?:\\.j2)?$/"],
},
helmfile: {
managerFilePatterns: [
"/(^|/)helmfile\\.ya?ml(?:\\.gotmpl)?(?:\\.j2)?$/",
"/(^|/)helmfile\\.d/.+\\.ya?ml(?:\\.gotmpl)?(?:\\.j2)?$/",
],
},
kubernetes: {
managerFilePatterns: ["/(^|/)kubernetes/.+\\.ya?ml(?:\\.j2)?$/"],
},
kustomize: {
managerFilePatterns: ["/^kustomization\\.ya?ml(?:\\.j2)?$/"],
},
packageRules: [
{
description: "Override Helmfile Dependency Name",
matchDatasources: ["docker"],
matchManagers: ["helmfile"],
overrideDepName: "{{packageName}}",
},
{
description: "Flux Operator Group",
groupName: "flux-operator",
matchDatasources: ["docker"],
matchPackageNames: ["/flux-operator/", "/flux-instance/", "/flux-operator-manifests/"],
group: {
commitMessageTopic: "{{{groupName}}} group",
},
minimumGroupSize: 3,
},
{
description: "Auto-merge GitHub Actions",
matchManagers: ["github-actions"],
automerge: true,
automergeType: "branch",
matchUpdateTypes: ["minor", "patch", "digest"],
minimumReleaseAge: "3 days",
ignoreTests: true,
},
{
description: "Auto-merge Mise Tools",
matchManagers: ["mise"],
automerge: true,
automergeType: "branch",
matchUpdateTypes: ["minor", "patch"],
ignoreTests: true,
},
{
matchUpdateTypes: ["major"],
semanticCommitType: "feat",
commitMessagePrefix: "{{semanticCommitType}}({{semanticCommitScope}})!:",
commitMessageExtra: "( {{currentVersion}} ➔ {{newVersion}} )",
},
{
matchUpdateTypes: ["minor"],
semanticCommitType: "feat",
commitMessageExtra: "( {{currentVersion}} ➔ {{newVersion}} )",
},
{
matchUpdateTypes: ["patch"],
semanticCommitType: "fix",
commitMessageExtra: "( {{currentVersion}} ➔ {{newVersion}} )",
},
{
matchUpdateTypes: ["digest"],
semanticCommitType: "chore",
commitMessageExtra: "( {{currentDigestShort}} ➔ {{newDigestShort}} )",
},
{
matchDatasources: ["docker"],
semanticCommitScope: "container",
commitMessageTopic: "image {{depName}}",
},
{
matchDatasources: ["helm"],
semanticCommitScope: "helm",
commitMessageTopic: "chart {{depName}}",
},
{
matchManagers: ["github-actions"],
semanticCommitType: "ci",
semanticCommitScope: "github-action",
commitMessageTopic: "action {{depName}}",
},
{
matchDatasources: ["github-releases"],
semanticCommitScope: "github-release",
commitMessageTopic: "release {{depName}}",
},
{
matchManagers: ["mise"],
semanticCommitScope: "mise",
commitMessageTopic: "tool {{depName}}",
},
{
matchUpdateTypes: ["major"],
labels: ["type/major"],
},
{
matchUpdateTypes: ["minor"],
labels: ["type/minor"],
},
{
matchUpdateTypes: ["patch"],
labels: ["type/patch"],
},
{
matchUpdateTypes: ["digest"],
labels: ["type/digest"],
},
{
matchDatasources: ["docker"],
addLabels: ["renovate/container"],
},
{
matchDatasources: ["helm"],
addLabels: ["renovate/helm"],
},
{
matchManagers: ["github-actions"],
addLabels: ["renovate/github-action"],
},
{
matchDatasources: ["github-releases"],
addLabels: ["renovate/github-release"],
},
],
customManagers: [
{
description: "Process annotated dependencies",
customType: "regex",
managerFilePatterns: [
"/(^|/).+\\.env(?:\\.j2)?$/",
"/(^|/).+\\.sh(?:\\.j2)?$/",
"/(^|/).+\\.ya?ml(?:\\.j2)?$/",
],
matchStrings: [
// # renovate: datasource=github-releases depName=k3s-io/k3s
// k3s_release_version: &version v1.29.0+k3s1
// # renovate: datasource=helm depName=cilium repository=https://helm.cilium.io
// version: 1.15.1
// # renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet
// KUBERNETES_VERSION=v1.31.1
"datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( repository=(?<registryUrl>\\S+))?\\n.+(:\\s|=)(&\\S+\\s)?(?<currentValue>\\S+)",
// # renovate: datasource=docker depName=ghcr.io/prometheus-operator/prometheus-operator
// https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.80.0/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagerconfigs.yaml
"datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)\\n.+/(?<currentValue>(v|\\d)[^/]+)",
],
datasourceTemplate: "{{#if datasource}}{{{datasource}}}{{else}}github-releases{{/if}}",
},
{
customType: "regex",
description: "Process OCI dependencies",
managerFilePatterns: [
"/\\.yaml(?:\\.j2)?$/",
],
matchStrings: [
"oci://(?<depName>[^:]+):(?<currentValue>\\S+)",
],
datasourceTemplate: "docker",
},
],
}

2
.shellcheckrc Normal file
View File

@@ -0,0 +1,2 @@
disable=SC1091
disable=SC2155

View File

@@ -0,0 +1,30 @@
---
version: '3'
tasks:
talos:
desc: Bootstrap the Talos cluster
dir: '{{.TALOS_DIR}}'
cmds:
- '[ -f talsecret.sops.yaml ] || talhelper gensecret | sops --filename-override talos/talsecret.sops.yaml --encrypt /dev/stdin > talsecret.sops.yaml'
- talhelper genconfig
- talhelper gencommand apply --extra-flags="--insecure" | bash
- until talhelper gencommand bootstrap | bash; do sleep 10; done
- until talhelper gencommand kubeconfig --extra-flags="{{.ROOT_DIR}} --force" | bash; do sleep 10; done
preconditions:
- test -f {{.ROOT_DIR}}/.sops.yaml
- test -f {{.SOPS_AGE_KEY_FILE}}
- test -f {{.TALOS_DIR}}/talconfig.yaml
- which talhelper talosctl sops
apps:
desc: Bootstrap apps into the Talos cluster
cmd: bash {{.SCRIPTS_DIR}}/bootstrap-apps.sh
preconditions:
- msg: Unsupported bash version, run `brew install bash` to upgrade
sh: '{{if eq OS "darwin"}}test -f /opt/homebrew/bin/bash || test -f /usr/local/bin/bash{{end}}'
- test -f {{.KUBECONFIG}}
- test -f {{.ROOT_DIR}}/.sops.yaml
- test -f {{.SCRIPTS_DIR}}/bootstrap-apps.sh
- test -f {{.SOPS_AGE_KEY_FILE}}

View File

@@ -0,0 +1,65 @@
---
version: '3'
tasks:
generate-config:
desc: Generate Talos configuration
dir: '{{.TALOS_DIR}}'
cmd: talhelper genconfig
preconditions:
- test -f {{.TALOS_DIR}}/talconfig.yaml
- test -f {{.ROOT_DIR}}/.sops.yaml
- test -f {{.SOPS_AGE_KEY_FILE}}
- which talhelper
apply-node:
desc: Apply Talos config to a node [IP=required]
dir: '{{.TALOS_DIR}}'
cmd: talhelper gencommand apply --node {{.IP}} --extra-flags '--mode={{.MODE}}' | bash
vars:
MODE: '{{.MODE | default "auto"}}'
requires:
vars: [IP]
preconditions:
- talosctl --nodes {{.IP}} get machineconfig
- talosctl config info
- test -f {{.TALOSCONFIG}}
- which talhelper talosctl yq
upgrade-node:
desc: Upgrade Talos on a single node [IP=required]
dir: '{{.TALOS_DIR}}'
cmd: talhelper gencommand upgrade --node {{.IP}} --extra-flags "--image='{{.TALOS_IMAGE}}:{{.TALOS_VERSION}}' --timeout=10m" | bash
vars:
TALOS_IMAGE:
sh: yq '.nodes[] | select(.ipAddress == "{{.IP}}") | .talosImageURL' {{.TALOS_DIR}}/talconfig.yaml
TALOS_VERSION:
sh: yq '.talosVersion' {{.TALOS_DIR}}/talenv.yaml
requires:
vars: [IP]
preconditions:
- talosctl --nodes {{.IP}} get machineconfig
- talosctl config info
- test -f {{.TALOSCONFIG}}
- which kubectl talhelper talosctl yq
upgrade-k8s:
desc: Upgrade Kubernetes
dir: '{{.TALOS_DIR}}'
cmd: talhelper gencommand upgrade-k8s --extra-flags "--to '{{.KUBERNETES_VERSION}}'" | bash
vars:
KUBERNETES_VERSION:
sh: yq '.kubernetesVersion' {{.TALOS_DIR}}/talenv.yaml
preconditions:
- talosctl config info
- test -f {{.TALOSCONFIG}}
- which talhelper talosctl yq
reset:
desc: Resets nodes back to maintenance mode
dir: '{{.TALOS_DIR}}'
prompt: This will destroy your cluster and reset the nodes back to maintenance mode... continue?
cmd: talhelper gencommand reset --extra-flags="--reboot {{- if eq .CLI_FORCE false }} --system-labels-to-wipe STATE --system-labels-to-wipe EPHEMERAL{{ end }} --graceful=false --wait=false" | bash
preconditions:
- which talhelper

View File

@@ -0,0 +1,170 @@
---
version: '3'
vars:
MAKEJINJA_CONFIG_FILE: '{{.ROOT_DIR}}/makejinja.toml'
TEMPLATE_DIR: '{{.ROOT_DIR}}/templates'
TEMPLATE_RESOURCES_DIR: '{{.ROOT_DIR}}/.taskfiles/template/resources'
TEMPLATE_CONFIG_FILE: '{{.ROOT_DIR}}/cluster.yaml'
TEMPLATE_NODE_CONFIG_FILE: '{{.ROOT_DIR}}/nodes.yaml'
tasks:
:init:
desc: Initialize configuration files
cmds:
- task: generate-template-config
- task: generate-age-key
- task: generate-deploy-key
- task: generate-push-token
generate-template-config:
internal: true
cmds:
- mv {{.TEMPLATE_CONFIG_FILE | replace ".yaml" ".sample.yaml"}} {{.TEMPLATE_CONFIG_FILE}}
- mv {{.TEMPLATE_NODE_CONFIG_FILE | replace ".yaml" ".sample.yaml"}} {{.TEMPLATE_NODE_CONFIG_FILE}}
status:
- test -f {{.TEMPLATE_CONFIG_FILE}}
- test -f {{.TEMPLATE_NODE_CONFIG_FILE}}
generate-age-key:
internal: true
cmd: age-keygen --output {{.SOPS_AGE_KEY_FILE}}
status:
- test -f {{.SOPS_AGE_KEY_FILE}}
preconditions:
- which age-keygen
generate-deploy-key:
internal: true
cmd: ssh-keygen -t ed25519 -C "deploy-key" -f {{.ROOT_DIR}}/github-deploy.key -q -P ""
status:
- test -f {{.ROOT_DIR}}/github-deploy.key
preconditions:
- which ssh-keygen
generate-push-token:
internal: true
cmd: python -c "import secrets; print(secrets.token_hex(16))" > {{.ROOT_DIR}}/github-push-token.txt
status:
- test -f {{.ROOT_DIR}}/github-push-token.txt
:configure:
desc: Render and validate configuration files
prompt: Any conflicting files in the kubernetes directory will be overwritten... continue?
cmds:
- task: validate-schemas
- task: render-configs
- task: encrypt-secrets
- task: validate-kubernetes-config
- task: validate-talos-config
preconditions:
- msg: An existing Age key interferes with the age key in this repository, rename or delete ~/.config/sops/age/keys.txt
sh: '! test -f ~/.config/sops/age/keys.txt'
- msg: File cluster.yaml not found, did you run `task init`?
sh: test -f {{.TEMPLATE_CONFIG_FILE}}
- msg: File nodes.yaml not found, did you run `task init`?
sh: test -f {{.TEMPLATE_NODE_CONFIG_FILE}}
- msg: File cloudflare-tunnel.json not found, see the README for information on creating it.
sh: test -f {{.ROOT_DIR}}/cloudflare-tunnel.json
validate-schemas:
internal: true
cmds:
- cue vet {{.TEMPLATE_CONFIG_FILE}} {{.TEMPLATE_RESOURCES_DIR}}/cluster.schema.cue
- cue vet {{.TEMPLATE_NODE_CONFIG_FILE}} {{.TEMPLATE_RESOURCES_DIR}}/nodes.schema.cue
preconditions:
- test -f {{.TEMPLATE_RESOURCES_DIR}}/cluster.schema.cue
- test -f {{.TEMPLATE_RESOURCES_DIR}}/nodes.schema.cue
- which cue
render-configs:
internal: true
cmd: makejinja
env:
PYTHONDONTWRITEBYTECODE: '1'
preconditions:
- test -f {{.TEMPLATE_DIR}}/scripts/plugin.py
- test -f {{.MAKEJINJA_CONFIG_FILE}}
- which makejinja
encrypt-secrets:
internal: true
cmds:
- for: { var: SECRET_FILES }
cmd: |
if [ $(sops filestatus "{{.ITEM}}" | jq ".encrypted") == "false" ]; then
sops --encrypt --in-place "{{.ITEM}}"
fi
vars:
SECRET_FILES:
sh: find "{{.BOOTSTRAP_DIR}}" "{{.KUBERNETES_DIR}}" "{{.TALOS_DIR}}" -type f -name "*.sops.*" -print
preconditions:
- test -f {{.SOPS_AGE_KEY_FILE}}
- test -f {{.ROOT_DIR}}/.sops.yaml
- which jq sops
validate-kubernetes-config:
internal: true
cmd: bash {{.TEMPLATE_RESOURCES_DIR}}/kubeconform.sh {{.KUBERNETES_DIR}}
preconditions:
- test -f {{.TEMPLATE_RESOURCES_DIR}}/kubeconform.sh
- which kubeconform
validate-talos-config:
internal: true
dir: '{{.TALOS_DIR}}'
cmd: talhelper validate talconfig {{.TALOS_DIR}}/talconfig.yaml
preconditions:
- test -f {{.TALOS_DIR}}/talconfig.yaml
- which talhelper
debug:
desc: Gather common resources in your cluster
cmds:
- for:
matrix:
RESOURCE: [certificates, certificaterequests, gitrepositories, helmrepositories, helmreleases, httproutes, kustomizations, nodes, pods]
cmd: kubectl get --all-namespaces {{.ITEM.RESOURCE}}
preconditions:
- test -f {{.KUBECONFIG}}
- which kubectl
tidy:
desc: Archive template related files and directories
prompt: All files and directories related to the templating process will be archived... continue?
cmds:
- mkdir -p {{.TIDY_FOLDER}}
- rm -rf {{.ROOT_DIR}}/.github/tests
- rm -rf {{.ROOT_DIR}}/.github/workflows/e2e.yaml
- rm -rf {{.ROOT_DIR}}/.github/workflows/mise.yaml
- rm -rf {{.ROOT_DIR}}/.github/workflows/release.yaml
- |
{{.SED}} -i 's/(..\.j2)\?//g' {{.ROOT_DIR}}/.renovaterc.json5
- mv {{.TEMPLATE_DIR}} {{.TIDY_FOLDER}}/templates
- mv {{.MAKEJINJA_CONFIG_FILE}} {{.TIDY_FOLDER}}/makejinja.toml
- mv {{.TEMPLATE_CONFIG_FILE}} {{.TIDY_FOLDER}}/cluster.yaml
- mv {{.TEMPLATE_NODE_CONFIG_FILE}} {{.TIDY_FOLDER}}/nodes.yaml
- |
{{.SED}} -i '/template:/d' {{.ROOT_DIR}}/Taskfile.yaml
- mv {{.ROOT_DIR}}/.taskfiles/template {{.TIDY_FOLDER}}/.taskfiles/
vars:
TIDY_FOLDER: '{{.PRIVATE_DIR}}/{{now | unixEpoch}}'
SED:
sh: which gsed || which sed
preconditions:
- msg: Unsupported sed version, run `brew install gsed` to upgrade
sh: '{{if eq OS "darwin"}}test -f /opt/homebrew/bin/gsed || test -f /usr/local/bin/gsed{{end}}'
- test -d {{.ROOT_DIR}}/.taskfiles/template
- test -d {{.TEMPLATE_DIR}}
- test -f {{.MAKEJINJA_CONFIG_FILE}}
- test -f {{.ROOT_DIR}}/.renovaterc.json5
reset:
desc: Remove templated files and directories
prompt: Remove all templated files and directories... continue?
cmds:
- rm -rf {{.BOOTSTRAP_DIR}}
- rm -rf {{.KUBERNETES_DIR}}
- rm -rf {{.TALOS_DIR}}
- rm -rf {{.ROOT_DIR}}/.sops.yaml

View File

@@ -0,0 +1,31 @@
package config
import (
"net"
)
#Config: {
node_cidr: net.IPCIDR & !=cluster_pod_cidr & !=cluster_svc_cidr
node_dns_servers?: [...net.IPv4]
node_ntp_servers?: [...net.IPv4]
node_default_gateway?: net.IPv4 & !=""
node_vlan_tag?: string & !=""
cluster_pod_cidr: *"10.42.0.0/16" | net.IPCIDR & !=node_cidr & !=cluster_svc_cidr
cluster_svc_cidr: *"10.43.0.0/16" | net.IPCIDR & !=node_cidr & !=cluster_pod_cidr
cluster_api_addr: net.IPv4
cluster_api_tls_sans?: [...net.FQDN]
cluster_gateway_addr: net.IPv4 & !=cluster_api_addr & !=cluster_dns_gateway_addr & !=cloudflare_gateway_addr
cluster_dns_gateway_addr: net.IPv4 & !=cluster_api_addr & !=cluster_gateway_addr & !=cloudflare_gateway_addr
repository_name: string
repository_branch?: string & !=""
repository_visibility?: *"public" | "private"
cloudflare_domain: net.FQDN
cloudflare_token: string
cloudflare_gateway_addr: net.IPv4 & !=cluster_api_addr & !=cluster_gateway_addr & !=cluster_dns_gateway_addr
cilium_bgp_router_addr?: net.IPv4 & !=""
cilium_bgp_router_asn?: string & !=""
cilium_bgp_node_asn?: string & !=""
cilium_loadbalancer_mode?: *"dsr" | "snat"
}
#Config

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
KUBERNETES_DIR=$1
[[ -z "${KUBERNETES_DIR}" ]] && echo "Kubernetes location not specified" && exit 1
kustomize_args=("--load-restrictor=LoadRestrictionsNone")
kustomize_config="kustomization.yaml"
kubeconform_args=(
"-strict"
"-ignore-missing-schemas"
"-skip"
"Gateway,HTTPRoute,Secret"
"-schema-location"
"default"
"-schema-location"
"https://kubernetes-schemas.pages.dev/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json"
"-verbose"
)
echo "=== Validating standalone manifests in ${KUBERNETES_DIR}/flux ==="
find "${KUBERNETES_DIR}/flux" -maxdepth 1 -type f -name '*.yaml' -print0 | while IFS= read -r -d $'\0' file;
do
kubeconform "${kubeconform_args[@]}" "${file}"
if [[ ${PIPESTATUS[0]} != 0 ]]; then
exit 1
fi
done
echo "=== Validating kustomizations in ${KUBERNETES_DIR}/flux ==="
find "${KUBERNETES_DIR}/flux" -type f -name $kustomize_config -print0 | while IFS= read -r -d $'\0' file;
do
echo "=== Validating kustomizations in ${file/%$kustomize_config} ==="
kustomize build "${file/%$kustomize_config}" "${kustomize_args[@]}" | kubeconform "${kubeconform_args[@]}"
if [[ ${PIPESTATUS[0]} != 0 ]]; then
exit 1
fi
done
echo "=== Validating kustomizations in ${KUBERNETES_DIR}/apps ==="
find "${KUBERNETES_DIR}/apps" -type f -name $kustomize_config -print0 | while IFS= read -r -d $'\0' file;
do
echo "=== Validating kustomizations in ${file/%$kustomize_config} ==="
kustomize build "${file/%$kustomize_config}" "${kustomize_args[@]}" | kubeconform "${kubeconform_args[@]}"
if [[ ${PIPESTATUS[0]} != 0 ]]; then
exit 1
fi
done

View File

@@ -0,0 +1,30 @@
package config
import (
"net"
"list"
)
#Config: {
nodes: [...#Node]
_nodes_check: {
name: list.UniqueItems() & [for item in nodes {item.name}]
address: list.UniqueItems() & [for item in nodes {item.address}]
mac_addr: list.UniqueItems() & [for item in nodes {item.mac_addr}]
}
}
#Node: {
name: =~"^[a-z0-9][a-z0-9\\-]{0,61}[a-z0-9]$|^[a-z0-9]$" & !="global" & !="controller" & !="worker"
address: net.IPv4
controller: bool
disk: string
mac_addr: =~"^([0-9a-f]{2}[:]){5}([0-9a-f]{2})$"
schematic_id: =~"^[a-z0-9]{64}$"
mtu?: >=1450 & <=9000
secureboot?: bool
encrypt_disk?: bool
kernel_modules?: [...string]
}
#Config

9
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"blueglassblock.better-json5",
"irongeek.vscode-env",
"redhat.vscode-yaml",
"signageos.signageos-vscode-sops",
"hverlin.mise-vscode"
]
}

18
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"editor.bracketPairColorization.enabled": true,
"files.associations": {
"**/*.json5": "json5"
},
"files.trimTrailingWhitespace": true,
"sops.defaults.ageKeyFile": "age.key",
"vs-kubernetes": {
"vs-kubernetes.kubeconfig": "./kubeconfig",
"vs-kubernetes.knownKubeconfigs": [
"./kubeconfig"
]
},
"yaml.schemaStore.enable": true,
"yaml.schemas": {
"kubernetes": "./kubernetes/**/*.yaml"
}
}

40
AGENTS.md Normal file
View File

@@ -0,0 +1,40 @@
# Agent Instructions
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
## Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
bd sync # Sync with git
```
## Landing the Plane (Session Completion)
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd sync
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 onedr0p
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

494
README.md Normal file
View File

@@ -0,0 +1,494 @@
# ⛵ Cluster Template
Welcome to my template designed for deploying a single Kubernetes cluster. Whether you're setting up a cluster at home on bare-metal or virtual machines (VMs), this project aims to simplify the process and make Kubernetes more accessible. This template is inspired by my personal [home-ops](https://github.com/onedr0p/home-ops) repository, providing a practical starting point for anyone interested in managing their own Kubernetes environment.
At its core, this project leverages [makejinja](https://github.com/mirkolenz/makejinja), a powerful tool for rendering templates. By reading configuration files—such as [cluster.yaml](./cluster.sample.yaml) and [nodes.yaml](./nodes.sample.yaml)—Makejinja generates the necessary configurations to deploy a Kubernetes cluster with the following features:
- Easy configuration through YAML files.
- Compatibility with home setups, whether on physical hardware or VMs.
- A modular and extensible approach to cluster deployment and management.
With this approach, you'll gain a solid foundation to build and manage your Kubernetes cluster efficiently.
## ✨ Features
A Kubernetes cluster deployed with [Talos Linux](https://github.com/siderolabs/talos) and an opinionated implementation of [Flux](https://github.com/fluxcd/flux2) using [GitHub](https://github.com/) as the Git provider, [sops](https://github.com/getsops/sops) to manage secrets and [cloudflared](https://github.com/cloudflare/cloudflared) to access applications external to your local network.
- **Required:** Some knowledge of [Containers](https://opencontainers.org/), [YAML](https://noyaml.com/), [Git](https://git-scm.com/), and a **Cloudflare account** with a **domain**.
- **Included components:** [flux](https://github.com/fluxcd/flux2), [cilium](https://github.com/cilium/cilium), [cert-manager](https://github.com/cert-manager/cert-manager), [spegel](https://github.com/spegel-org/spegel), [reloader](https://github.com/stakater/Reloader), [envoy-gateway](https://github.com/envoyproxy/gateway), [external-dns](https://github.com/kubernetes-sigs/external-dns) and [cloudflared](https://github.com/cloudflare/cloudflared).
**Other features include:**
- Dev env managed w/ [mise](https://mise.jdx.dev/)
- Workflow automation w/ [GitHub Actions](https://github.com/features/actions)
- Dependency automation w/ [Renovate](https://www.mend.io/renovate)
- Flux `HelmRelease` and `Kustomization` diffs w/ [flux-local](https://github.com/allenporter/flux-local)
Does this sound cool to you? If so, continue to read on! 👇
## 🚀 Let's Go!
There are **6 stages** outlined below for completing this project, make sure you follow the stages in order.
### Stage 1: Hardware Configuration
For a **stable** and **high-availability** production Kubernetes cluster, hardware selection is critical. NVMe/SSDs are strongly preferred over HDDs, and **Bare Metal is strongly recommended** over virtualized platforms like Proxmox.
Using **enterprise NVMe or SATA SSDs on Bare Metal** (even used drives) provides the most reliable performance and rock-solid stability. Consumer **NVMe or SATA SSDs**, on the other hand, carry risks such as latency spikes, corruption, and fsync delays, particularly in multi-node setups.
**Proxmox with enterprise drives can work** for testing or carefully tuned production clusters, but it introduces additional layers of potential I/O contention — especially if consumer drives are used. Any **replicated storage** (e.g., Rook-Ceph, Longhorn) should always use **dedicated disks separate from control plane and etcd nodes** to ensure reliability. Worker nodes are more flexible, but risky configurations should still be avoided for stateful workloads to maintain cluster stability.
These guidelines provide a strong baseline, but there are always exceptions and nuances. The best way to ensure your hardware configuration works is to **test it thoroughly and benchmark performance** under realistic workloads.
### Stage 2: Machine Preparation
> [!IMPORTANT]
> If you have **3 or more nodes** it is recommended to make 3 of them controller nodes for a highly available control plane. This project configures **all nodes** to be able to run workloads. **Worker nodes** are therefore **optional**.
>
> **Minimum system requirements**
> | Role | Cores | Memory | System Disk |
> |---------|----------|---------------|---------------------------|
> | Control/Worker | 4 | 16GB | 256GB SSD/NVMe |
1. Head over to the [Talos Linux Image Factory](https://factory.talos.dev) and follow the instructions. Be sure to only choose the **bare-minimum system extensions** as some might require additional configuration and prevent Talos from booting without it. Depending on your CPU start with the Intel/AMD system extensions (`i915`, `intel-ucode` & `mei` **or** `amdgpu` & `amd-ucode`), you can always add system extensions after Talos is installed and working.
2. This will eventually lead you to download a Talos Linux ISO (or for SBCs a RAW) image. Make sure to note the **schematic ID** you will need this later on.
3. Flash the Talos ISO or RAW image to a USB drive and boot from it on your nodes.
4. Verify with `nmap` that your nodes are available on the network. (Replace `192.168.1.0/24` with the network your nodes are on.)
```sh
nmap -Pn -n -p 50000 192.168.1.0/24 -vv | grep 'Discovered'
```
### Stage 3: Local Workstation
> [!TIP]
> It is recommended to set the visibility of your repository to `Public` so you can easily request help if you get stuck.
1. Create a new repository by clicking the green `Use this template` button at the top of this page, then clone the new repo you just created and `cd` into it. Alternatively you can us the [GitHub CLI](https://cli.github.com/) ...
```sh
export REPONAME="home-ops"
gh repo create $REPONAME --template onedr0p/cluster-template --disable-wiki --public --clone && cd $REPONAME
```
2. **Install** the [Mise CLI](https://mise.jdx.dev/getting-started.html#installing-mise-cli) on your workstation.
3. **Activate** Mise in your shell by following the [activation guide](https://mise.jdx.dev/getting-started.html#activate-mise).
4. Use `mise` to install the **required** CLI tools:
```sh
mise trust
pip install pipx
mise install
```
📍 _**Having trouble installing the tools?** Try unsetting the `GITHUB_TOKEN` env var and then run these commands again_
📍 _**Having trouble compiling Python?** Try running `mise settings python.compile=0` and then run these commands again_
5. Logout of GitHub Container Registry (GHCR) as this may cause authorization problems when using the public registry:
```sh
docker logout ghcr.io
helm registry logout ghcr.io
```
### Stage 4: Cloudflare configuration
> [!WARNING]
> If any of the commands fail with `command not found` or `unknown command` it means `mise` is either not install or configured incorrectly.
1. Create a Cloudflare API token for use with cloudflared and external-dns by reviewing the official [documentation](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) and following the instructions below.
- Click the blue `Use template` button for the `Edit zone DNS` template.
- Name your token `kubernetes`
- Under `Permissions`, click `+ Add More` and add permissions `Zone - DNS - Edit` and `Account - Cloudflare Tunnel - Read`
- Limit the permissions to a specific account and/or zone resources and then click `Continue to Summary` and then `Create Token`.
- **Save this token somewhere safe**, you will need it later on.
2. Create the Cloudflare Tunnel:
```sh
cloudflared tunnel login
cloudflared tunnel create --credentials-file cloudflare-tunnel.json kubernetes
```
### Stage 5: Cluster configuration
1. Generate the config files from the sample files:
```sh
task init
```
2. Fill out `cluster.yaml` and `nodes.yaml` configuration files using the comments in those file as a guide.
3. Template out the kubernetes and talos configuration files, if any issues come up be sure to read the error and adjust your config files accordingly.
```sh
task configure
```
4. Push your changes to git:
📍 _**Verify** all the `./kubernetes/**/*.sops.*` files are **encrypted** with SOPS_
```sh
git add -A
git commit -m "chore: initial commit :rocket:"
git push
```
> [!TIP]
> Using a **private repository**? Make sure to paste the public key from `github-deploy.key.pub` into the deploy keys section of your GitHub repository settings. This will make sure Flux has read/write access to your repository.
### Stage 6: Bootstrap Talos, Kubernetes, and Flux
> [!WARNING]
> It might take a while for the cluster to be setup (10+ minutes is normal). During which time you will see a variety of error messages like: "couldn't get current server API group list," "error: no matching resources found", etc. 'Ready' will remain "False" as no CNI is deployed yet. **This is a normal.** If this step gets interrupted, e.g. by pressing <kbd>Ctrl</kbd> + <kbd>C</kbd>, you likely will need to [reset the cluster](#-reset) before trying again
1. Install Talos:
```sh
task bootstrap:talos
```
2. Push your changes to git:
```sh
git add -A
git commit -m "chore: add talhelper encrypted secret :lock:"
git push
```
3. Install cilium, coredns, spegel, flux and sync the cluster to the repository state:
```sh
task bootstrap:apps
```
4. Watch the rollout of your cluster happen:
```sh
kubectl get pods --all-namespaces --watch
```
## 📣 Post installation
### ✅ Verifications
1. Check the status of Cilium:
```sh
cilium status
```
2. Check the status of Flux and if the Flux resources are up-to-date and in a ready state:
📍 _Run `task reconcile` to force Flux to sync your Git repository state_
```sh
flux check
flux get sources git flux-system
flux get ks -A
flux get hr -A
```
3. Check TCP connectivity to both the internal and external gateways:
📍 _The variables are only placeholders, replace them with your actual values_
```sh
nmap -Pn -n -p 443 ${cluster_gateway_addr} ${cloudflare_gateway_addr} -vv
```
4. Check you can resolve DNS for `echo`, this should resolve to `${cloudflare_gateway_addr}`:
📍 _The variables are only placeholders, replace them with your actual values_
```sh
dig @${cluster_dns_gateway_addr} echo.${cloudflare_domain}
```
5. Check the status of your wildcard `Certificate`:
```sh
kubectl -n network describe certificates
```
### 🌐 Public DNS
> [!TIP]
> Use the `envoy-external` gateway on `HTTPRoutes` to make applications public to the internet. These are also accessible on your private network once you set up split DNS.
The `external-dns` application created in the `network` namespace will handle creating public DNS records. By default, `echo` and the `flux-webhook` are the only subdomains reachable from the public internet. In order to make additional applications public you must **set the correct gateway** like in the HelmRelease for `echo`.
### 🏠 Home DNS
> [!TIP]
> Use the `envoy-internal` gateway on `HTTPRoutes` to make applications private to your network. If you're having trouble with internal DNS resolution check out [this](https://github.com/onedr0p/cluster-template/discussions/719) GitHub discussion.
`k8s_gateway` will provide DNS resolution to external Kubernetes resources (i.e. points of entry to the cluster) from any device that uses your home DNS server. For this to work, your home DNS server must be configured to forward DNS queries for `${cloudflare_domain}` to `${cluster_dns_gateway_addr}` instead of the upstream DNS server(s) it normally uses. This is a form of **split DNS** (aka split-horizon DNS / conditional forwarding).
_... Nothing working? That is expected, this is DNS after all!_
### 🪝 Github Webhook
By default Flux will periodically check your git repository for changes. In-order to have Flux reconcile on `git push` you must configure Github to send `push` events to Flux.
1. Obtain the webhook path:
📍 _Hook id and path should look like `/hook/12ebd1e363c641dc3c2e430ecf3cee2b3c7a5ac9e1234506f6f5f3ce1230e123`_
```sh
kubectl -n flux-system get receiver github-webhook --output=jsonpath='{.status.webhookPath}'
```
2. Piece together the full URL with the webhook path appended:
```text
https://flux-webhook.${cloudflare_domain}/hook/12ebd1e363c641dc3c2e430ecf3cee2b3c7a5ac9e1234506f6f5f3ce1230e123
```
3. Navigate to the settings of your repository on Github, under "Settings/Webhooks" press the "Add webhook" button. Fill in the webhook URL and your token from `github-push-token.txt`, Content type: `application/json`, Events: Choose Just the push event, and save.
## 💥 Reset
> [!CAUTION]
> **Resetting** the cluster **multiple times in a short period of time** could lead to being **rate limited by DockerHub or Let's Encrypt**.
There might be a situation where you want to destroy your Kubernetes cluster. The following command will reset your nodes back to maintenance mode.
```sh
task talos:reset
```
## 🛠️ Talos and Kubernetes Maintenance
### ⚙️ Updating Talos node configuration
> [!TIP]
> Ensure you have updated `talconfig.yaml` and any patches with your updated configuration. In some cases you **not only need to apply the configuration but also upgrade talos** to apply new configuration.
```sh
# (Re)generate the Talos config
task talos:generate-config
# Apply the config to the node
task talos:apply-node IP=? MODE=?
# e.g. task talos:apply-node IP=10.10.10.10 MODE=auto
```
### ⬆️ Updating Talos and Kubernetes versions
> [!TIP]
> Ensure the `talosVersion` and `kubernetesVersion` in `talenv.yaml` are up-to-date with the version you wish to upgrade to.
```sh
# Upgrade node to a newer Talos version
task talos:upgrade-node IP=?
# e.g. task talos:upgrade-node IP=10.10.10.10
```
```sh
# Upgrade cluster to a newer Kubernetes version
task talos:upgrade-k8s
# e.g. task talos:upgrade-k8s
```
### Adding a node to your cluster
At some point you might want to expand your cluster to run more workloads and/or improve the reliability of your cluster. Keep in mind it is recommended to have an **odd number** of control plane nodes for quorum reasons.
You don't need to re-bootstrap the cluster to add new nodes. Follow these steps:
1. **Prepare the new node**: Review the [Stage 2: Machine Preparation](#stage-2-machine-preparation) section and boot your new node into maintenance mode.
2. **Get the node information**: While the node is in maintenance mode, retrieve the disk and MAC address information needed for configuration:
```sh
talosctl get disks -n <ip> --insecure
talosctl get links -n <ip> --insecure
```
3. **Update the configuration**: Read the documentation for [talhelper](https://budimanjojo.github.io/talhelper/latest/) and extend the `talconfig.yaml` file manually with the new node information (including the disk and MAC address from step 2).
4. **Generate and apply the configuration**:
```sh
# Render your talosconfig based on the talconfig.yaml file
task talos:generate-config
# Apply the configuration to the node
task talos:apply-node IP=?
# e.g. task talos:apply-node IP=10.10.10.10
```
The node should join the cluster automatically and workloads will be scheduled once they report as ready.
## 🤖 Renovate
[Renovate](https://www.mend.io/renovate) is a tool that automates dependency management. It is designed to scan your repository around the clock and open PRs for out-of-date dependencies it finds. Common dependencies it can discover are Helm charts, container images, GitHub Actions and more! In most cases merging a PR will cause Flux to apply the update to your cluster.
To enable Renovate, click the 'Configure' button over at their [Github app page](https://github.com/apps/renovate) and select your repository. Renovate creates a "Dependency Dashboard" as an issue in your repository, giving an overview of the status of all updates. The dashboard has interactive checkboxes that let you do things like advance scheduling or reattempt update PRs you closed without merging.
The base Renovate configuration in your repository can be viewed at [.renovaterc.json5](.renovaterc.json5). By default it is scheduled to be active with PRs every weekend, but you can [change the schedule to anything you want](https://docs.renovatebot.com/presets-schedule), or remove it if you want Renovate to open PRs immediately.
## 🐛 Debugging
Below is a general guide on trying to debug an issue with an resource or application. For example, if a workload/resource is not showing up or a pod has started but in a `CrashLoopBackOff` or `Pending` state. These steps do not include a way to fix the problem as the problem could be one of many different things.
1. Check if the Flux resources are up-to-date and in a ready state:
📍 _Run `task reconcile` to force Flux to sync your Git repository state_
```sh
flux get sources git -A
flux get ks -A
flux get hr -A
```
2. Do you see the pod of the workload you are debugging:
```sh
kubectl -n <namespace> get pods -o wide
```
3. Check the logs of the pod if its there:
```sh
kubectl -n <namespace> logs <pod-name> -f
```
4. If a resource exists try to describe it to see what problems it might have:
```sh
kubectl -n <namespace> describe <resource> <name>
```
5. Check the namespace events:
```sh
kubectl -n <namespace> get events --sort-by='.metadata.creationTimestamp'
```
Resolving problems that you have could take some tweaking of your YAML manifests in order to get things working, other times it could be a external factor like permissions on a NFS server. If you are unable to figure out your problem see the support sections below.
## 🧹 Tidy up
Once your cluster is fully configured and you no longer need to run `task configure`, it's a good idea to clean up the repository by removing the [templates](./templates) directory and any files related to the templating process. This will help eliminate unnecessary clutter from the upstream template repository and resolve any "duplicate registry" warnings from Renovate.
1. Tidy up your repository:
```sh
task template:tidy
```
2. Push your changes to git:
```sh
git add -A
git commit -m "chore: tidy up :broom:"
git push
```
## ❔ What's next
There's a lot to absorb here, especially if you're new to these tools. Take some time to familiarize yourself with the tooling and understand how all the components interconnect. Dive into the documentation of the various tools included — they are a valuable resource. This shouldn't be a production environment yet, so embrace the freedom to experiment. Move fast, break things intentionally, and challenge yourself to fix them.
Below are some optional considerations you may want to explore.
### DNS
The template uses [k8s_gateway](https://github.com/k8s-gateway/k8s_gateway) to provide DNS for your applications, consider exploring [external-dns](https://github.com/kubernetes-sigs/external-dns) as an alternative.
External-DNS offers broad support for various DNS providers, including but not limited to:
- [Pi-hole](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/pihole.md)
- [UniFi](https://github.com/kashalls/external-dns-unifi-webhook)
- [Adguard Home](https://github.com/muhlba91/external-dns-provider-adguard)
- [Bind](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/rfc2136.md)
This flexibility allows you to integrate seamlessly with a range of DNS solutions to suit your environment and offload DNS from your cluster to your router, or external device.
### Secrets
SOPs is an excellent tool for managing secrets in a GitOps workflow. However, it can become cumbersome when rotating secrets or maintaining a single source of truth for secret items.
For a more streamlined approach to those issues, consider [External Secrets](https://external-secrets.io/latest/). This tool allows you to move away from SOPs and leverage an external provider for managing your secrets. External Secrets supports a wide range of providers, from cloud-based solutions to self-hosted options.
### Storage
If your workloads require persistent storage with features like replication or connectivity to NFS, SMB, or iSCSI servers, there are several projects worth exploring:
- [rook-ceph](https://github.com/rook/rook)
- [longhorn](https://github.com/longhorn/longhorn)
- [openebs](https://github.com/openebs/openebs)
- [democratic-csi](https://github.com/democratic-csi/democratic-csi)
- [csi-driver-nfs](https://github.com/kubernetes-csi/csi-driver-nfs)
- [csi-driver-smb](https://github.com/kubernetes-csi/csi-driver-smb)
- [synology-csi](https://github.com/SynologyOpenSource/synology-csi)
These tools offer a variety of solutions to meet your persistent storage needs, whether youre using cloud-native or self-hosted infrastructures.
### Community Repositories
Community member [@whazor](https://github.com/whazor) created [Kubesearch](https://kubesearch.dev) to allow searching Flux HelmReleases across Github and Gitlab repositories with the `kubesearch` topic.
## 🙋 Support
### Community
- Make a post in this repository's Github [Discussions](https://github.com/onedr0p/cluster-template/discussions).
- Start a thread in the `#support` or `#cluster-template` channels in the [Home Operations](https://discord.gg/home-operations) Discord server.
### GitHub Sponsors
If you're having difficulty with this project, can't find the answers you need through the community support options above, or simply want to show your appreciation while gaining deeper insights, Im offering one-on-one paid support through GitHub Sponsors for a limited time. Payment and scheduling will be coordinated through [GitHub Sponsors](https://github.com/sponsors/onedr0p).
<details>
<summary>Click to expand the details</summary>
<br>
- **Rate**: $50/hour (no longer than 2 hours / day).
- **Whats Included**: Assistance with deployment, debugging, or answering questions related to this project.
- **What to Expect**:
1. Sessions will focus on specific questions or issues you are facing.
2. I will provide guidance, explanations, and actionable steps to help resolve your concerns.
3. Support is limited to this project and does not extend to unrelated tools or custom feature development.
</details>
## 🙌 Related Projects
If this repo is too hot to handle or too cold to hold check out these following projects.
- [ajaykumar4/cluster-template](https://github.com/ajaykumar4/cluster-template) - _A template for deploying a Talos Kubernetes cluster including Argo for GitOps_
- [khuedoan/homelab](https://github.com/khuedoan/homelab) - _Fully automated homelab from empty disk to running services with a single command._
- [mitchross/k3s-argocd-starter](https://github.com/mitchross/k3s-argocd-starter) - starter kit for k3s, argocd
- [ricsanfre/pi-cluster](https://github.com/ricsanfre/pi-cluster) - _Pi Kubernetes Cluster. Homelab kubernetes cluster automated with Ansible and FluxCD_
- [techno-tim/k3s-ansible](https://github.com/techno-tim/k3s-ansible) - _The easiest way to bootstrap a self-hosted High Availability Kubernetes cluster. A fully automated HA k3s etcd install with kube-vip, MetalLB, and more. Build. Destroy. Repeat._
## ⭐ Stargazers
<div align="center">
<a href="https://star-history.com/#onedr0p/cluster-template&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=onedr0p/cluster-template&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=onedr0p/cluster-template&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=onedr0p/cluster-template&type=Date" />
</picture>
</a>
</div>
## 🤝 Thanks
Big shout out to all the contributors, sponsors and everyone else who has helped on this project.

34
Taskfile.yaml Normal file
View File

@@ -0,0 +1,34 @@
---
version: '3'
set: [pipefail]
shopt: [globstar]
vars:
BOOTSTRAP_DIR: '{{.ROOT_DIR}}/bootstrap'
KUBERNETES_DIR: '{{.ROOT_DIR}}/kubernetes'
SCRIPTS_DIR: '{{.ROOT_DIR}}/scripts'
TALOS_DIR: '{{.ROOT_DIR}}/talos'
PRIVATE_DIR: '{{.ROOT_DIR}}/.private'
TALOSCONFIG: '{{.ROOT_DIR}}/talos/clusterconfig/talosconfig'
env:
KUBECONFIG: '{{.ROOT_DIR}}/kubeconfig'
SOPS_AGE_KEY_FILE: '{{.ROOT_DIR}}/age.key'
TALOSCONFIG: '{{.TALOSCONFIG}}'
includes:
bootstrap: .taskfiles/bootstrap
talos: .taskfiles/talos
template: .taskfiles/template
tasks:
default: task --list
reconcile:
desc: Force Flux to pull in changes from your Git repository
cmd: flux --namespace flux-system reconcile kustomization flux-system --with-source
preconditions:
- test -f {{.KUBECONFIG}}
- which flux

84
cluster.sample.yaml Normal file
View File

@@ -0,0 +1,84 @@
---
# -- The network CIDR for the nodes.
# (REQUIRED) / (e.g. 192.168.1.0/24)
node_cidr: ""
# -- DNS servers to use for the cluster.
# (OPTIONAL) / (DEFAULT: ["1.1.1.1", "1.0.0.1"]) / (Cloudflare DNS)
# node_dns_servers: []
# -- NTP servers to use for the cluster.
# (OPTIONAL) / (DEFAULT: ["162.159.200.1", "162.159.200.123"]) / (Cloudflare NTP)
# node_ntp_servers: []
# -- The default gateway for the nodes.
# (OPTIONAL) / (DEFAULT: the first IP in the node_cidr)
# node_default_gateway: ""
# -- Attach a vlan tag to the Talos nodes. Not needed if ports on your switch are tagged or you are not using VLANs.
# (OPTIONAL) / (REF: https://www.talos.dev/latest/advanced/advanced-networking/#vlans)
# node_vlan_tag: ""
# -- The IP address of the Kube API.
# (REQUIRED) / (NOTE: Choose an unused IP in node_cidr)
cluster_api_addr: ""
# -- Additional SANs to add to the Kube API cert. This is useful if you want to call the Kube API by hostname rather than IP
# (OPTIONAL) / (e.g. ["mycluster.example.com"])
# cluster_api_tls_sans: []
# -- The pod CIDR for the cluster, this must NOT overlap with any existing networks and should be a /16 (64K IPs).
# (OPTIONAL) / (DEFAULT: "10.42.0.0/16")
# cluster_pod_cidr: ""
# -- The service CIDR for the cluster, this must NOT overlap with any existing networks and should be a /16 (64K IPs).
# (OPTIONAL) / (DEFAULT: "10.43.0.0/16")
# cluster_svc_cidr: ""
# -- The Load balancer IP for k8s_gateway, this provides DNS to all your gateways when split DNS is configured on your internal DNS server (Dnsmasq, Pi-hole, etc)
# (REQUIRED) / (NOTE: Choose an unused IP in node_cidr)
cluster_dns_gateway_addr: ""
# -- The Load balancer IP for the internal gateway
# (REQUIRED) / (NOTE: Choose an unused IP in node_cidr)
cluster_gateway_addr: ""
# -- GitHub repository
# (REQUIRED) / (e.g. "onedr0p/cluster-template")
repository_name: ""
# -- GitHub repository branch
# (OPTIONAL) / (DEFAULT: "main")
# repository_branch: ""
# -- Repository visibility (public or private)
# (OPTIONAL) / (DEFAULT: "public") / (NOTE: See the README for information when set private)
# repository_visibility: ""
# -- Domain you wish to use from your Cloudflare account
# (REQUIRED) / (e.g. "example.com")
cloudflare_domain: ""
# -- API Token for Cloudflare with the 'Zone:DNS:Edit' and 'Account:Cloudflare Tunnel:Read' permissions
# (REQUIRED) (NOTE: See the README for information on creating this)
cloudflare_token: ""
# -- The Load balancer IP for the external gateway
# (REQUIRED) / (NOTE: Choose an unused IP in node_cidr)
cloudflare_gateway_addr: ""
# -- The load balancer mode for cilium.
# (OPTIONAL) / (DEFAULT: "dsr") / (NOTE: accepted values are 'dsr' or 'snat') / (REF: https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/)
# cilium_loadbalancer_mode: ""
# -- The IP address of the BGP router, to keep things simple, node network will be used for BGP peering.
# (OPTIONAL) / (e.g. "192.168.1.1") / (REF: https://docs.cilium.io/en/latest/network/bgp-control-plane/bgp-control-plane/)
# cilium_bgp_router_addr: ""
# -- The BGP router ASN
# (OPTIONAL) / (e.g. "64513")
# cilium_bgp_router_asn: ""
# -- The BGP node ASN
# (OPTIONAL) / (e.g. "64514")
# cilium_bgp_node_asn: ""

19
makejinja.toml Normal file
View File

@@ -0,0 +1,19 @@
[makejinja]
inputs = ["./templates/overrides","./templates/config"]
output = "./"
exclude_patterns = ["*.partial.yaml.j2"]
data = ["./cluster.yaml", "./nodes.yaml"]
import_paths = ["./templates/scripts"]
loaders = ["plugin:Plugin"]
jinja_suffix = ".j2"
copy_metadata = true
force = true
undefined = "chainable"
[makejinja.delimiter]
block_start = "#%"
block_end = "%#"
comment_start = "#|"
comment_end = "#|"
variable_start = "#{"
variable_end = "}#"

13
nodes.sample.yaml Normal file
View File

@@ -0,0 +1,13 @@
---
nodes: []
# - name: "" # (REQUIRED) Name of the node (must match [a-z0-9-\]+)
# address: "" # (REQUIRED) IP address of the node (must be in the node_cidr)
# controller: true # (REQUIRED) Set to true if this is a controller node
# disk: "" # (REQUIRED) Device path or serial number of the disk for this node (talosctl get disks -n <ip> --insecure)
# mac_addr: "" # (REQUIRED) MAC address of the NIC for this node (talosctl get links -n <ip> --insecure)
# schematic_id: "" # (REQUIRED) Schematic ID from https://factory.talos.dev/
# mtu: 1500 # (ADVANCED/OPTIONAL) MTU for the NIC. DEFAULT: 1500
# secureboot: false # (ADVANCED/OPTIONAL) SecureBoot mode on UEFI platforms. Ref: https://www.talos.dev/latest/talos-guides/install/bare-metal-platforms/secureboot
# encrypt_disk: false # (ADVANCED/OPTIONAL) TPM-based disk encryption. Ref: https://www.talos.dev/latest/talos-guides/install/bare-metal-platforms/secureboot
# kernel_modules: [] # (ADVANCED/OPTIONAL) Only applicable if the `schematic_id` you've provided contains system extensions that require kernel modules to correctly load - Example: ["nvidia", "nvidia_uvm", "nvidia_drm", "nvidia_modeset", "zfs"]
# ...

144
scripts/bootstrap-apps.sh Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source "$(dirname "${0}")/lib/common.sh"
export LOG_LEVEL="debug"
export ROOT_DIR="$(git rev-parse --show-toplevel)"
# Talos requires the nodes to be 'Ready=False' before applying resources
function wait_for_nodes() {
log debug "Waiting for nodes to be available"
# Skip waiting if all nodes are 'Ready=True'
if kubectl wait nodes --for=condition=Ready=True --all --timeout=10s &>/dev/null; then
log info "Nodes are available and ready, skipping wait for nodes"
return
fi
# Wait for all nodes to be 'Ready=False'
until kubectl wait nodes --for=condition=Ready=False --all --timeout=10s &>/dev/null; do
log info "Nodes are not available, waiting for nodes to be available. Retrying in 10 seconds..."
sleep 10
done
}
# Namespaces to be applied before the SOPS secrets are installed
function apply_namespaces() {
log debug "Applying namespaces"
local -r apps_dir="${ROOT_DIR}/kubernetes/apps"
if [[ ! -d "${apps_dir}" ]]; then
log error "Directory does not exist" "directory=${apps_dir}"
fi
for app in "${apps_dir}"/*/; do
namespace=$(basename "${app}")
# Check if the namespace resources are up-to-date
if kubectl get namespace "${namespace}" &>/dev/null; then
log info "Namespace resource is up-to-date" "resource=${namespace}"
continue
fi
# Apply the namespace resources
if kubectl create namespace "${namespace}" --dry-run=client --output=yaml \
| kubectl apply --server-side --filename - &>/dev/null;
then
log info "Namespace resource applied" "resource=${namespace}"
else
log error "Failed to apply namespace resource" "resource=${namespace}"
fi
done
}
# SOPS secrets to be applied before the helmfile charts are installed
function apply_sops_secrets() {
log debug "Applying secrets"
local -r secrets=(
"${ROOT_DIR}/bootstrap/github-deploy-key.sops.yaml"
"${ROOT_DIR}/bootstrap/sops-age.sops.yaml"
"${ROOT_DIR}/kubernetes/components/sops/cluster-secrets.sops.yaml"
)
for secret in "${secrets[@]}"; do
if [ ! -f "${secret}" ]; then
log warn "File does not exist" "file=${secret}"
continue
fi
# Check if the secret resources are up-to-date
if sops exec-file "${secret}" "kubectl --namespace flux-system diff --filename {}" &>/dev/null; then
log info "Secret resource is up-to-date" "resource=$(basename "${secret}" ".sops.yaml")"
continue
fi
# Apply secret resources
if sops exec-file "${secret}" "kubectl --namespace flux-system apply --server-side --filename {}" &>/dev/null; then
log info "Secret resource applied successfully" "resource=$(basename "${secret}" ".sops.yaml")"
else
log error "Failed to apply secret resource" "resource=$(basename "${secret}" ".sops.yaml")"
fi
done
}
# CRDs to be applied before the helmfile charts are installed
function apply_crds() {
log debug "Applying CRDs"
local -r helmfile_file="${ROOT_DIR}/bootstrap/helmfile.d/00-crds.yaml"
if [[ ! -f "${helmfile_file}" ]]; then
log fatal "File does not exist" "file" "${helmfile_file}"
fi
if ! crds=$(helmfile --file "${helmfile_file}" template --quiet | yq eval-all --exit-status 'select(.kind == "CustomResourceDefinition")') || [[ -z "${crds}" ]]; then
log fatal "Failed to render CRDs from Helmfile" "file" "${helmfile_file}"
fi
if echo "${crds}" | kubectl diff --filename - &>/dev/null; then
log info "CRDs are up-to-date"
return
fi
if ! echo "${crds}" | kubectl apply --server-side --filename - &>/dev/null; then
log fatal "Failed to apply crds from Helmfile" "file" "${helmfile_file}"
fi
log info "CRDs applied successfully"
}
# Sync Helm releases
function sync_helm_releases() {
log debug "Syncing Helm releases"
local -r helmfile_file="${ROOT_DIR}/bootstrap/helmfile.d/01-apps.yaml"
if [[ ! -f "${helmfile_file}" ]]; then
log error "File does not exist" "file=${helmfile_file}"
fi
if ! helmfile --file "${helmfile_file}" sync --hide-notes; then
log error "Failed to sync Helm releases"
fi
log info "Helm releases synced successfully"
}
function main() {
check_env KUBECONFIG TALOSCONFIG
check_cli helmfile kubectl kustomize sops talhelper yq
# Apply resources and Helm releases
wait_for_nodes
apply_namespaces
apply_sops_secrets
apply_crds
sync_helm_releases
log info "Congrats! The cluster is bootstrapped and Flux is syncing the Git repository"
}
main "$@"

107
scripts/lib/common.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Log messages with different levels
function log() {
local level="${1:-info}"
shift
# Define log levels with their priorities
local -A level_priority=(
[debug]=1
[info]=2
[warn]=3
[error]=4
)
# Get the current log level's priority
local current_priority=${level_priority[$level]:-2} # Default to "info" priority
# Get the configured log level from the environment, default to "info"
local configured_level=${LOG_LEVEL:-info}
local configured_priority=${level_priority[$configured_level]:-2}
# Skip log messages below the configured log level
if ((current_priority < configured_priority)); then
return
fi
# Define log colors
local -A colors=(
[debug]="\033[1m\033[38;5;63m" # Blue
[info]="\033[1m\033[38;5;87m" # Cyan
[warn]="\033[1m\033[38;5;192m" # Yellow
[error]="\033[1m\033[38;5;198m" # Red
)
# Fallback to "info" if the color for the given level is not defined
local color="${colors[$level]:-${colors[info]}}"
local msg="$1"
shift
# Prepare additional data
local data=
if [[ $# -gt 0 ]]; then
for item in "$@"; do
if [[ "${item}" == *=* ]]; then
data+="\033[1m\033[38;5;236m${item%%=*}=\033[0m\"${item#*=}\" "
else
data+="${item} "
fi
done
fi
# Determine output stream based on log level
local output_stream="/dev/stdout"
if [[ "$level" == "error" ]]; then
output_stream="/dev/stderr"
fi
# Print the log message
printf "%s %b%s%b %s %b\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
"${color}" "${level^^}" "\033[0m" "${msg}" "${data}" >"${output_stream}"
# Exit if the log level is error
if [[ "$level" == "error" ]]; then
exit 1
fi
}
# Check if required environment variables are set
function check_env() {
local envs=("${@}")
local missing=()
local values=()
for env in "${envs[@]}"; do
if [[ -z "${!env-}" ]]; then
missing+=("${env}")
else
values+=("${env}=${!env}")
fi
done
if [ ${#missing[@]} -ne 0 ]; then
log error "Missing required env variables" "envs=${missing[*]}"
fi
log debug "Env variables are set" "envs=${values[*]}"
}
# Check if required CLI tools are installed
function check_cli() {
local deps=("${@}")
local missing=()
for dep in "${deps[@]}"; do
if ! command -v "${dep}" &>/dev/null; then
missing+=("${dep}")
fi
done
if [ ${#missing[@]} -ne 0 ]; then
log error "Missing required deps" "deps=${missing[*]}"
fi
log debug "Deps are installed" "deps=${deps[*]}"
}

View File

@@ -0,0 +1,12 @@
---
creation_rules:
- path_regex: talos/.*\.sops\.ya?ml
mac_only_encrypted: true
age: "#{ age_key('public') }#"
- path_regex: (bootstrap|kubernetes)/.*\.sops\.ya?ml
encrypted_regex: "^(data|stringData)$"
mac_only_encrypted: true
age: "#{ age_key('public') }#"
stores:
yaml:
indent: 2

View File

@@ -0,0 +1,17 @@
#% if repository_visibility == 'private' %#
---
apiVersion: v1
kind: Secret
metadata:
name: github-deploy-key
namespace: flux-system
stringData:
identity: |
#% filter indent(width=4, first=False) %#
#{ github_deploy_key() }#
#% endfilter %#
known_hosts: |
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
#% endif %#

View File

@@ -0,0 +1,25 @@
---
# This helmfile is for extracting and installing Custom Resource Definitions (CRDs) from Helm charts.
# It is not intended to be used with helmfile apply or helmfile sync.
helmDefaults:
args:
- --include-crds
- --no-hooks
releases:
- name: cloudflare-dns
namespace: network
chart: oci://ghcr.io/home-operations/charts-mirror/external-dns
version: 1.20.0
- name: envoy-gateway
namespace: network
chart: oci://mirror.gcr.io/envoyproxy/gateway-helm
version: v1.6.2
- name: kube-prometheus-stack
namespace: observability
chart: oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack
version: 81.2.2

View File

@@ -0,0 +1,54 @@
---
helmDefaults:
cleanupOnFail: true
wait: true
waitForJobs: true
releases:
- name: cilium
namespace: kube-system
chart: oci://quay.io/cilium/charts/cilium
version: 1.18.6
values: ['./templates/values.yaml.gotmpl']
- name: coredns
namespace: kube-system
chart: oci://ghcr.io/coredns/charts/coredns
version: 1.45.2
values: ['./templates/values.yaml.gotmpl']
needs: ['kube-system/cilium']
#% if spegel_enabled %#
- name: spegel
namespace: kube-system
chart: oci://ghcr.io/spegel-org/helm-charts/spegel
version: 0.6.0
values: ['./templates/values.yaml.gotmpl']
needs: ['kube-system/coredns']
#% endif %#
- name: cert-manager
namespace: cert-manager
chart: oci://quay.io/jetstack/charts/cert-manager
version: v1.19.2
values: ['./templates/values.yaml.gotmpl']
#% if spegel_enabled %#
needs: ['kube-system/spegel']
#% else %#
needs: ['kube-system/coredns']
#% endif %#
- name: flux-operator
namespace: flux-system
chart: oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator
version: 0.40.0
values: ['./templates/values.yaml.gotmpl']
needs: ['cert-manager/cert-manager']
- name: flux-instance
namespace: flux-system
chart: oci://ghcr.io/controlplaneio-fluxcd/charts/flux-instance
version: 0.40.0
values: ['./templates/values.yaml.gotmpl']
needs: ['flux-system/flux-operator']

View File

@@ -0,0 +1 @@
{{ (fromYaml (readFile (printf "../../../kubernetes/apps/%s/%s/app/helmrelease.yaml" .Release.Namespace .Release.Name))).spec.values | toYaml }}

View File

@@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Secret
metadata:
name: sops-age
namespace: flux-system
stringData:
age.agekey: "#{ age_key('private') }#"

View File

@@ -0,0 +1,19 @@
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
privateKeySecretRef:
name: letsencrypt-production
profile: shortlived
server: https://acme-v02.api.letsencrypt.org/directory
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cert-manager-secret
key: api-token
selector:
dnsZones: ["${SECRET_DOMAIN}"]

View File

@@ -0,0 +1,20 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cert-manager
spec:
chartRef:
kind: OCIRepository
name: cert-manager
interval: 1h
values:
crds:
enabled: true
replicaCount: 1
dns01RecursiveNameservers: https://1.1.1.1:443/dns-query,https://1.0.0.1:443/dns-query
dns01RecursiveNameserversOnly: true
prometheus:
enabled: true
servicemonitor:
enabled: true

View File

@@ -0,0 +1,8 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./clusterissuer.yaml
- ./helmrelease.yaml
- ./ocirepository.yaml
- ./secret.sops.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: cert-manager
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: v1.19.2
url: oci://quay.io/jetstack/charts/cert-manager

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Secret
metadata:
name: cert-manager-secret
stringData:
api-token: "#{ cloudflare_token }#"

View File

@@ -0,0 +1,31 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cert-manager
spec:
healthChecks:
- apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
name: cert-manager
namespace: cert-manager
- apiVersion: cert-manager.io/v1
kind: ClusterIssuer
name: letsencrypt-production
healthCheckExprs:
- apiVersion: cert-manager.io/v1
kind: ClusterIssuer
failed: status.conditions.filter(e, e.type == 'Ready').all(e, e.status == 'False')
current: status.conditions.filter(e, e.type == 'Ready').all(e, e.status == 'True')
interval: 1h
path: ./kubernetes/apps/cert-manager/cert-manager/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: cert-manager

View File

@@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: cert-manager
components:
- ../../components/sops
resources:
- ./namespace.yaml
- ./cert-manager/ks.yaml

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled

View File

@@ -0,0 +1,71 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: echo
spec:
chartRef:
kind: OCIRepository
name: echo
interval: 1h
values:
controllers:
echo:
strategy: RollingUpdate
containers:
app:
image:
repository: ghcr.io/mendhak/http-https-echo
tag: 39
env:
HTTP_PORT: &port 80
LOG_WITHOUT_NEWLINE: true
LOG_IGNORE_PATH: /healthz
PROMETHEUS_ENABLED: true
probes:
liveness: &probes
enabled: true
custom: true
spec:
httpGet:
path: /healthz
port: *port
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 3
readiness: *probes
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
resources:
requests:
cpu: 10m
limits:
memory: 64Mi
defaultPodOptions:
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
service:
app:
ports:
http:
port: *port
serviceMonitor:
app:
endpoints:
- port: http
route:
app:
hostnames: ["{{ .Release.Name }}.${SECRET_DOMAIN}"]
parentRefs:
- name: envoy-external
namespace: network
sectionName: https
rules:
- backendRefs:
- identifier: app
port: *port

View File

@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: echo
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 4.6.2
url: oci://ghcr.io/bjw-s-labs/helm/app-template

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: echo
spec:
interval: 1h
path: ./kubernetes/apps/default/echo/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: default
wait: false

View File

@@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
components:
- ../../components/sops
resources:
- ./namespace.yaml
- ./echo/ks.yaml

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: default
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled

View File

@@ -0,0 +1,138 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: flux-instance
spec:
chartRef:
kind: OCIRepository
name: flux-instance
interval: 1h
values:
instance:
distribution:
artifact: oci://ghcr.io/controlplaneio-fluxcd/flux-operator-manifests:v0.40.0
cluster:
networkPolicy: false
components:
- source-controller
- kustomize-controller
- helm-controller
- notification-controller
sync:
kind: GitRepository
#% if repository_visibility == 'private' %#
url: "ssh://git@github.com/#{ repository_name }#.git"
pullSecret: github-deploy-key
#% else %#
url: "https://github.com/#{ repository_name }#.git"
#% endif %#
ref: "refs/heads/#{ repository_branch }#"
path: kubernetes/flux/cluster
commonMetadata:
labels:
app.kubernetes.io/name: flux
kustomize:
patches:
- # Increase the number of workers
patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --concurrent=10
- op: add
path: /spec/template/spec/containers/0/args/-
value: --requeue-dependency=5s
target:
kind: Deployment
name: (kustomize-controller|helm-controller|source-controller)
- # Increase the memory limits
patch: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: all
spec:
template:
spec:
containers:
- name: manager
resources:
limits:
memory: 1Gi
target:
kind: Deployment
name: (kustomize-controller|helm-controller|source-controller)
- # Enable in-memory kustomize builds
patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --concurrent=20
- op: replace
path: /spec/template/spec/volumes/0
value:
name: temp
emptyDir:
medium: Memory
target:
kind: Deployment
name: kustomize-controller
- # Enable Helm repositories caching
patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --helm-cache-max-size=10
- op: add
path: /spec/template/spec/containers/0/args/-
value: --helm-cache-ttl=60m
- op: add
path: /spec/template/spec/containers/0/args/-
value: --helm-cache-purge-interval=5m
target:
kind: Deployment
name: source-controller
- # Flux near OOM detection for Helm
patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --feature-gates=OOMWatch=true
- op: add
path: /spec/template/spec/containers/0/args/-
value: --oom-watch-memory-threshold=95
- op: add
path: /spec/template/spec/containers/0/args/-
value: --oom-watch-interval=500ms
target:
kind: Deployment
name: helm-controller
- # Disable chart digest tracking
patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --feature-gates=DisableChartDigestTracking=true
target:
kind: Deployment
name: helm-controller
- # Controller-level SOPS decryption
patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --sops-age-secret=sops-age
target:
kind: Deployment
name: kustomize-controller
- # Watch configmaps and secrets attached to HelmReleases and Kustomizations
patch: |-
- op: add
path: /spec/template/spec/containers/0/args/-
value: --watch-configs-label-selector=owner!=helm
target:
kind: Deployment
name: (helm-controller|kustomize-controller)
- # Cancel health checks on new Kustomizations revisions
patch: |-
- op: add
path: /spec/template/spec/containers/0/args/-
value: --feature-gates=CancelHealthCheckOnNewRevision=true
target:
kind: Deployment
name: kustomize-controller

View File

@@ -0,0 +1,20 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: github-webhook
spec:
hostnames: ["flux-webhook.${SECRET_DOMAIN}"]
parentRefs:
- name: envoy-external
namespace: network
sectionName: https
rules:
- backendRefs:
- name: webhook-receiver
namespace: flux-system
port: 80
matches:
- path:
type: PathPrefix
value: /hook/

View File

@@ -0,0 +1,9 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml
- ./secret.sops.yaml
- ./httproute.yaml
- ./receiver.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: flux-instance
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 0.40.0
url: oci://ghcr.io/controlplaneio-fluxcd/charts/flux-instance

View File

@@ -0,0 +1,19 @@
---
apiVersion: notification.toolkit.fluxcd.io/v1
kind: Receiver
metadata:
name: github-webhook
spec:
type: github
events: ["ping", "push"]
secretRef:
name: github-webhook-token-secret
resources:
- apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
name: flux-system
namespace: flux-system
- apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
name: flux-system
namespace: flux-system

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Secret
metadata:
name: github-webhook-token-secret
stringData:
token: "#{ github_push_token() }#"

View File

@@ -0,0 +1,21 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: flux-instance
spec:
dependsOn:
- name: flux-operator
interval: 1h
path: ./kubernetes/apps/flux-system/flux-instance/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: flux-system
wait: false

View File

@@ -0,0 +1,13 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: flux-operator
spec:
chartRef:
kind: OCIRepository
name: flux-operator
interval: 1h
values:
serviceMonitor:
create: true

View File

@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: flux-operator
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 0.40.0
url: oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: flux-operator
spec:
interval: 1h
path: ./kubernetes/apps/flux-system/flux-operator/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: flux-system
wait: true

View File

@@ -0,0 +1,12 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: flux-system
components:
- ../../components/sops
resources:
- ./namespace.yaml
- ./flux-instance/ks.yaml
- ./flux-operator/ks.yaml

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: flux-system
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled

View File

@@ -0,0 +1,91 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cilium
spec:
chartRef:
kind: OCIRepository
name: cilium
interval: 1h
values:
autoDirectNodeRoutes: true
bpf:
masquerade: true
# Ref: https://github.com/siderolabs/talos/issues/10002
hostLegacyRouting: true
#% if cilium_bgp_enabled %#
bgpControlPlane:
enabled: true
#% endif %#
cni:
# Required for pairing with Multus CNI
exclusive: false
cgroup:
automount:
enabled: false
hostRoot: /sys/fs/cgroup
# NOTE: devices might need to be set if you have more than one active NIC on your hosts
# devices: eno+ eth+
dashboards:
enabled: true
endpointRoutes:
enabled: true
envoy:
enabled: false
gatewayAPI:
enabled: false
hubble:
enabled: false
ipam:
mode: kubernetes
ipv4NativeRoutingCIDR: "#{ cluster_pod_cidr }#"
k8sServiceHost: 127.0.0.1
k8sServicePort: 7445
kubeProxyReplacement: true
kubeProxyReplacementHealthzBindAddr: 0.0.0.0:10256
l2announcements:
enabled: true
loadBalancer:
algorithm: maglev
mode: "#{ cilium_loadbalancer_mode }#"
localRedirectPolicy: true
operator:
dashboards:
enabled: true
prometheus:
enabled: true
serviceMonitor:
enabled: true
replicas: 1
rollOutPods: true
prometheus:
enabled: true
serviceMonitor:
enabled: true
trustCRDsExist: true
rollOutCiliumPods: true
routingMode: native
securityContext:
capabilities:
ciliumAgent:
- CHOWN
- KILL
- NET_ADMIN
- NET_RAW
- IPC_LOCK
- SYS_ADMIN
- SYS_RESOURCE
- PERFMON
- BPF
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
cleanCiliumState:
- NET_ADMIN
- SYS_ADMIN
- SYS_RESOURCE
socketLB:
enabled: true
hostNamespaceOnly: true

View File

@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml
- ./networks.yaml

View File

@@ -0,0 +1,71 @@
---
apiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: pool
spec:
allowFirstLastIPs: "No"
blocks:
- cidr: "#{ node_cidr }#"
---
apiVersion: cilium.io/v2alpha1
kind: CiliumL2AnnouncementPolicy
metadata:
name: l2-policy
spec:
loadBalancerIPs: true
# NOTE: interfaces might need to be set if you have more than one active NIC on your hosts
# interfaces:
# - ^eno[0-9]+
# - ^eth[0-9]+
nodeSelector:
matchLabels:
kubernetes.io/os: linux
#% if cilium_bgp_enabled %#
---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPAdvertisement
metadata:
name: bgp-advertisement-config
labels:
advertise: bgp
spec:
advertisements:
- advertisementType: Service
service:
addresses:
- LoadBalancerIP
selector:
matchExpressions:
- { key: somekey, operator: NotIn, values: ["never-used-value"] }
---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPPeerConfig
metadata:
name: bgp-peer-config-v4
spec:
families:
- afi: ipv4
safi: unicast
advertisements:
matchLabels:
advertise: bgp
---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPClusterConfig
metadata:
name: bgp-cluster-config
spec:
nodeSelector:
matchLabels:
kubernetes.io/os: linux
bgpInstances:
- name: instance-#{ cilium_bgp_node_asn }#
localASN: #{ cilium_bgp_node_asn }#
peers:
- name: peer-#{ cilium_bgp_router_asn }#-v4
peerASN: #{ cilium_bgp_router_asn }#
peerAddress: #{ cilium_bgp_router_addr }#
peerConfigRef:
name: bgp-peer-config-v4
#% endif %#

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: cilium
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 1.18.6
url: oci://quay.io/cilium/charts/cilium

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cilium
spec:
interval: 1h
path: ./kubernetes/apps/kube-system/cilium/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: kube-system
wait: false

View File

@@ -0,0 +1,67 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: coredns
spec:
chartRef:
kind: OCIRepository
name: coredns
interval: 1h
values:
fullnameOverride: coredns
image:
repository: mirror.gcr.io/coredns/coredns
k8sAppLabelOverride: kube-dns
serviceAccount:
create: true
service:
name: kube-dns
clusterIP: "#{ cluster_svc_cidr | nthhost(10) }#"
replicaCount: 2
servers:
- zones:
- zone: .
scheme: dns://
use_tcp: true
port: 53
plugins:
- name: errors
- name: health
configBlock: |-
lameduck 5s
- name: ready
- name: kubernetes
parameters: cluster.local in-addr.arpa ip6.arpa
configBlock: |-
pods verified
fallthrough in-addr.arpa ip6.arpa
- name: autopath
parameters: "@kubernetes"
- name: forward
parameters: . /etc/resolv.conf
- name: cache
configBlock: |-
prefetch 20
serve_stale
- name: loop
- name: reload
- name: loadbalance
- name: prometheus
parameters: 0.0.0.0:9153
- name: log
configBlock: |-
class error
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: Exists
tolerations:
- key: CriticalAddonsOnly
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule

View File

@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: coredns
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
url: oci://ghcr.io/coredns/charts/coredns
ref:
tag: 1.45.2

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: coredns
spec:
interval: 1h
path: ./kubernetes/apps/kube-system/coredns/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: kube-system
wait: false

View File

@@ -0,0 +1,17 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kube-system
components:
- ../../components/sops
resources:
- ./namespace.yaml
- ./cilium/ks.yaml
- ./coredns/ks.yaml
- ./metrics-server/ks.yaml
- ./reloader/ks.yaml
#% if spegel_enabled %#
- ./spegel/ks.yaml
#% endif %#

View File

@@ -0,0 +1,21 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: metrics-server
spec:
chartRef:
kind: OCIRepository
name: metrics-server
interval: 1h
values:
args:
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=10s
- --kubelet-request-timeout=2s
metrics:
enabled: true
serviceMonitor:
enabled: true

View File

@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: metrics-server
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 3.13.0
url: oci://ghcr.io/home-operations/charts-mirror/metrics-server

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: metrics-server
spec:
interval: 1h
path: ./kubernetes/apps/kube-system/metrics-server/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: kube-system
wait: false

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-system
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled

View File

@@ -0,0 +1,17 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: reloader
spec:
chartRef:
kind: OCIRepository
name: reloader
interval: 1h
values:
fullnameOverride: reloader
reloader:
readOnlyRootFileSystem: true
podMonitor:
enabled: true
namespace: "{{ .Release.Namespace }}"

View File

@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: reloader
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 2.2.7
url: oci://ghcr.io/stakater/charts/reloader

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: reloader
spec:
interval: 1h
path: ./kubernetes/apps/kube-system/reloader/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: kube-system
wait: false

View File

@@ -0,0 +1,21 @@
#% if spegel_enabled %#
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: spegel
spec:
chartRef:
kind: OCIRepository
name: spegel
interval: 1h
values:
spegel:
containerdSock: /run/containerd/containerd.sock
containerdRegistryConfigPath: /etc/cri/conf.d/hosts
service:
registry:
hostPort: 29999
serviceMonitor:
enabled: true
#% endif %#

View File

@@ -0,0 +1,8 @@
#% if spegel_enabled %#
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
- ./ocirepository.yaml
#% endif %#

View File

@@ -0,0 +1,15 @@
#% if spegel_enabled %#
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: spegel
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 0.6.0
url: oci://ghcr.io/spegel-org/helm-charts/spegel
#% endif %#

View File

@@ -0,0 +1,21 @@
#% if spegel_enabled %#
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: spegel
spec:
interval: 1h
path: ./kubernetes/apps/kube-system/spegel/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: kube-system
wait: false
#% endif %#

View File

@@ -0,0 +1,35 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: &app cloudflare-dns
spec:
chartRef:
kind: OCIRepository
name: cloudflare-dns
interval: 1h
values:
fullnameOverride: *app
provider: cloudflare
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: &secret cloudflare-dns-secret
key: api-token
extraArgs:
- --cloudflare-dns-records-per-page=1000
- --cloudflare-proxied
- --crd-source-apiversion=externaldns.k8s.io/v1alpha1
- --crd-source-kind=DNSEndpoint
- --gateway-name=envoy-external
triggerLoopOnEvent: true
policy: sync
sources: ["crd", "gateway-httproute"]
txtPrefix: k8s.
txtOwnerId: default
domainFilters: ["${SECRET_DOMAIN}"]
serviceMonitor:
enabled: true
podAnnotations:
secret.reloader.stakater.com/reload: *secret

View File

@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./secret.sops.yaml
- ./helmrelease.yaml
- ./ocirepository.yaml

View File

@@ -0,0 +1,13 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: cloudflare-dns
spec:
interval: 15m
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 1.20.0
url: oci://ghcr.io/home-operations/charts-mirror/external-dns

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-dns-secret
stringData:
api-token: "#{ cloudflare_token }#"

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cloudflare-dns
spec:
interval: 1h
path: ./kubernetes/apps/network/cloudflare-dns/app
postBuild:
substituteFrom:
- name: cluster-secrets
kind: Secret
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: network
wait: true

Some files were not shown because too many files have changed in this diff Show More