commit f77a808d74f55805b9ce6a438e43c176f885694c Author: Laur IVAN Date: Tue Jan 27 23:27:14 2026 +0100 feat: Initial commit, empty from template. diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 0000000..d27a1db --- /dev/null +++ b/.beads/.gitignore @@ -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. diff --git a/.beads/README.md b/.beads/README.md new file mode 100644 index 0000000..50f281f --- /dev/null +++ b/.beads/README.md @@ -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 + +# Update issue status +bd update --status in_progress +bd update --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* ⚑ diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000..f242785 --- /dev/null +++ b/.beads/config.yaml @@ -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 '. +# 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 diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..c787975 --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,4 @@ +{ + "database": "beads.db", + "jsonl_export": "issues.jsonl" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fd65cad --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2b39659 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/labeler.yaml b/.github/labeler.yaml new file mode 100644 index 0000000..fde818b --- /dev/null +++ b/.github/labeler.yaml @@ -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/**/* diff --git a/.github/labels.yaml b/.github/labels.yaml new file mode 100644 index 0000000..6958304 --- /dev/null +++ b/.github/labels.yaml @@ -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" diff --git a/.github/release.yaml b/.github/release.yaml new file mode 100644 index 0000000..c32986c --- /dev/null +++ b/.github/release.yaml @@ -0,0 +1,5 @@ +changelog: + exclude: + authors: + - github-actions + - renovate diff --git a/.github/tests/nodes.yaml b/.github/tests/nodes.yaml new file mode 100644 index 0000000..40ac5ae --- /dev/null +++ b/.github/tests/nodes.yaml @@ -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 diff --git a/.github/tests/private.yaml b/.github/tests/private.yaml new file mode 100644 index 0000000..f189023 --- /dev/null +++ b/.github/tests/private.yaml @@ -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: "" diff --git a/.github/tests/public.yaml b/.github/tests/public.yaml new file mode 100644 index 0000000..81ca924 --- /dev/null +++ b/.github/tests/public.yaml @@ -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" diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 0000000..b4b8d38 --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -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 diff --git a/.github/workflows/flux-local.yaml b/.github/workflows/flux-local.yaml new file mode 100644 index 0000000..9b078fc --- /dev/null +++ b/.github/workflows/flux-local.yaml @@ -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<> "$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) }}" diff --git a/.github/workflows/label-sync.yaml b/.github/workflows/label-sync.yaml new file mode 100644 index 0000000..a2aed54 --- /dev/null +++ b/.github/workflows/label-sync.yaml @@ -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 diff --git a/.github/workflows/labeler.yaml b/.github/workflows/labeler.yaml new file mode 100644 index 0000000..4d089a6 --- /dev/null +++ b/.github/workflows/labeler.yaml @@ -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 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..5aa5147 --- /dev/null +++ b/.github/workflows/release.yaml @@ -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 }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb5f0b9 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..27adc7d --- /dev/null +++ b/.mise.toml @@ -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" diff --git a/.renovaterc.json5 b/.renovaterc.json5 new file mode 100644 index 0000000..d5a72a5 --- /dev/null +++ b/.renovaterc.json5 @@ -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=(?\\S+) depName=(?\\S+)( repository=(?\\S+))?\\n.+(:\\s|=)(&\\S+\\s)?(?\\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=(?\\S+) depName=(?\\S+)\\n.+/(?(v|\\d)[^/]+)", + ], + datasourceTemplate: "{{#if datasource}}{{{datasource}}}{{else}}github-releases{{/if}}", + }, + { + customType: "regex", + description: "Process OCI dependencies", + managerFilePatterns: [ + "/\\.yaml(?:\\.j2)?$/", + ], + matchStrings: [ + "oci://(?[^:]+):(?\\S+)", + ], + datasourceTemplate: "docker", + }, + ], +} diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..ddd0ada --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,2 @@ +disable=SC1091 +disable=SC2155 diff --git a/.taskfiles/bootstrap/Taskfile.yaml b/.taskfiles/bootstrap/Taskfile.yaml new file mode 100644 index 0000000..2afa86c --- /dev/null +++ b/.taskfiles/bootstrap/Taskfile.yaml @@ -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}} diff --git a/.taskfiles/talos/Taskfile.yaml b/.taskfiles/talos/Taskfile.yaml new file mode 100644 index 0000000..d4618ac --- /dev/null +++ b/.taskfiles/talos/Taskfile.yaml @@ -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 diff --git a/.taskfiles/template/Taskfile.yaml b/.taskfiles/template/Taskfile.yaml new file mode 100644 index 0000000..f689747 --- /dev/null +++ b/.taskfiles/template/Taskfile.yaml @@ -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 diff --git a/.taskfiles/template/resources/cluster.schema.cue b/.taskfiles/template/resources/cluster.schema.cue new file mode 100644 index 0000000..2ab06f1 --- /dev/null +++ b/.taskfiles/template/resources/cluster.schema.cue @@ -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 diff --git a/.taskfiles/template/resources/kubeconform.sh b/.taskfiles/template/resources/kubeconform.sh new file mode 100755 index 0000000..538cf62 --- /dev/null +++ b/.taskfiles/template/resources/kubeconform.sh @@ -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 diff --git a/.taskfiles/template/resources/nodes.schema.cue b/.taskfiles/template/resources/nodes.schema.cue new file mode 100644 index 0000000..dccb43b --- /dev/null +++ b/.taskfiles/template/resources/nodes.schema.cue @@ -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 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..bd341bc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "blueglassblock.better-json5", + "irongeek.vscode-env", + "redhat.vscode-yaml", + "signageos.signageos-vscode-sops", + "hverlin.mise-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..53fccd6 --- /dev/null +++ b/.vscode/settings.json @@ -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" + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..df7a4af --- /dev/null +++ b/AGENTS.md @@ -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 # View issue details +bd update --status in_progress # Claim work +bd close # 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 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11a02c9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..be4f275 --- /dev/null +++ b/README.md @@ -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 Ctrl + C, 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 --insecure + talosctl get links -n --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 get pods -o wide + ``` + +3. Check the logs of the pod if its there: + + ```sh + kubectl -n logs -f + ``` + +4. If a resource exists try to describe it to see what problems it might have: + + ```sh + kubectl -n describe + ``` + +5. Check the namespace events: + + ```sh + kubectl -n 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 you’re 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, I’m 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). + +
+ +Click to expand the details + +
+ +- **Rate**: $50/hour (no longer than 2 hours / day). +- **What’s 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. + +
+ +## πŸ™Œ 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 + +
+ + + + + + Star History Chart + + + +
+ +## 🀝 Thanks + +Big shout out to all the contributors, sponsors and everyone else who has helped on this project. diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..0442ee1 --- /dev/null +++ b/Taskfile.yaml @@ -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 diff --git a/cluster.sample.yaml b/cluster.sample.yaml new file mode 100644 index 0000000..83a188b --- /dev/null +++ b/cluster.sample.yaml @@ -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: "" diff --git a/makejinja.toml b/makejinja.toml new file mode 100644 index 0000000..9b25b9c --- /dev/null +++ b/makejinja.toml @@ -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 = "}#" diff --git a/nodes.sample.yaml b/nodes.sample.yaml new file mode 100644 index 0000000..1bd67ed --- /dev/null +++ b/nodes.sample.yaml @@ -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 --insecure) + # mac_addr: "" # (REQUIRED) MAC address of the NIC for this node (talosctl get links -n --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"] + # ... diff --git a/scripts/bootstrap-apps.sh b/scripts/bootstrap-apps.sh new file mode 100755 index 0000000..9fbbcbf --- /dev/null +++ b/scripts/bootstrap-apps.sh @@ -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 "$@" diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh new file mode 100755 index 0000000..e5ddcd6 --- /dev/null +++ b/scripts/lib/common.sh @@ -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[*]}" +} diff --git a/templates/config/.sops.yaml.j2 b/templates/config/.sops.yaml.j2 new file mode 100644 index 0000000..bf44c2c --- /dev/null +++ b/templates/config/.sops.yaml.j2 @@ -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 diff --git a/templates/config/bootstrap/github-deploy-key.sops.yaml.j2 b/templates/config/bootstrap/github-deploy-key.sops.yaml.j2 new file mode 100644 index 0000000..003a2b0 --- /dev/null +++ b/templates/config/bootstrap/github-deploy-key.sops.yaml.j2 @@ -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 %# diff --git a/templates/config/bootstrap/helmfile.d/00-crds.yaml.j2 b/templates/config/bootstrap/helmfile.d/00-crds.yaml.j2 new file mode 100644 index 0000000..28d81e6 --- /dev/null +++ b/templates/config/bootstrap/helmfile.d/00-crds.yaml.j2 @@ -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 diff --git a/templates/config/bootstrap/helmfile.d/01-apps.yaml.j2 b/templates/config/bootstrap/helmfile.d/01-apps.yaml.j2 new file mode 100644 index 0000000..173938e --- /dev/null +++ b/templates/config/bootstrap/helmfile.d/01-apps.yaml.j2 @@ -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'] diff --git a/templates/config/bootstrap/helmfile.d/templates/values.yaml.gotmpl.j2 b/templates/config/bootstrap/helmfile.d/templates/values.yaml.gotmpl.j2 new file mode 100644 index 0000000..2a046fa --- /dev/null +++ b/templates/config/bootstrap/helmfile.d/templates/values.yaml.gotmpl.j2 @@ -0,0 +1 @@ +{{ (fromYaml (readFile (printf "../../../kubernetes/apps/%s/%s/app/helmrelease.yaml" .Release.Namespace .Release.Name))).spec.values | toYaml }} diff --git a/templates/config/bootstrap/sops-age.sops.yaml.j2 b/templates/config/bootstrap/sops-age.sops.yaml.j2 new file mode 100644 index 0000000..f555b4b --- /dev/null +++ b/templates/config/bootstrap/sops-age.sops.yaml.j2 @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-age + namespace: flux-system +stringData: + age.agekey: "#{ age_key('private') }#" diff --git a/templates/config/kubernetes/apps/cert-manager/cert-manager/app/clusterissuer.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/clusterissuer.yaml.j2 new file mode 100644 index 0000000..42bbd48 --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/clusterissuer.yaml.j2 @@ -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}"] diff --git a/templates/config/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..c6dd026 --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml.j2 new file mode 100644 index 0000000..353783a --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml.j2 @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./clusterissuer.yaml + - ./helmrelease.yaml + - ./ocirepository.yaml + - ./secret.sops.yaml diff --git a/templates/config/kubernetes/apps/cert-manager/cert-manager/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..53ec44f --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/cert-manager/cert-manager/app/secret.sops.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/secret.sops.yaml.j2 new file mode 100644 index 0000000..f1a8750 --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/cert-manager/app/secret.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-manager-secret +stringData: + api-token: "#{ cloudflare_token }#" diff --git a/templates/config/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 new file mode 100644 index 0000000..c0a08eb --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/cert-manager/kustomization.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/kustomization.yaml.j2 new file mode 100644 index 0000000..e24fb0b --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/kustomization.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/cert-manager/namespace.yaml.j2 b/templates/config/kubernetes/apps/cert-manager/namespace.yaml.j2 new file mode 100644 index 0000000..6e34540 --- /dev/null +++ b/templates/config/kubernetes/apps/cert-manager/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/templates/config/kubernetes/apps/default/echo/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/default/echo/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..b6c34cd --- /dev/null +++ b/templates/config/kubernetes/apps/default/echo/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/default/echo/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/default/echo/app/kustomization.yaml.j2 new file mode 100644 index 0000000..2ccd5a8 --- /dev/null +++ b/templates/config/kubernetes/apps/default/echo/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/default/echo/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/default/echo/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..ff5d836 --- /dev/null +++ b/templates/config/kubernetes/apps/default/echo/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/default/echo/ks.yaml.j2 b/templates/config/kubernetes/apps/default/echo/ks.yaml.j2 new file mode 100644 index 0000000..f942a81 --- /dev/null +++ b/templates/config/kubernetes/apps/default/echo/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/default/kustomization.yaml.j2 b/templates/config/kubernetes/apps/default/kustomization.yaml.j2 new file mode 100644 index 0000000..d7271b1 --- /dev/null +++ b/templates/config/kubernetes/apps/default/kustomization.yaml.j2 @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default + +components: + - ../../components/sops + +resources: + - ./namespace.yaml + - ./echo/ks.yaml diff --git a/templates/config/kubernetes/apps/default/namespace.yaml.j2 b/templates/config/kubernetes/apps/default/namespace.yaml.j2 new file mode 100644 index 0000000..e3f8a49 --- /dev/null +++ b/templates/config/kubernetes/apps/default/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: default + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/templates/config/kubernetes/apps/flux-system/flux-instance/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-instance/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..e886f21 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-instance/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/flux-instance/app/httproute.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-instance/app/httproute.yaml.j2 new file mode 100644 index 0000000..2f089a0 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-instance/app/httproute.yaml.j2 @@ -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/ diff --git a/templates/config/kubernetes/apps/flux-system/flux-instance/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-instance/app/kustomization.yaml.j2 new file mode 100644 index 0000000..09ae970 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-instance/app/kustomization.yaml.j2 @@ -0,0 +1,9 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml + - ./secret.sops.yaml + - ./httproute.yaml + - ./receiver.yaml diff --git a/templates/config/kubernetes/apps/flux-system/flux-instance/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-instance/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..618bae6 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-instance/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/flux-instance/app/receiver.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-instance/app/receiver.yaml.j2 new file mode 100644 index 0000000..d7d2942 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-instance/app/receiver.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/flux-instance/app/secret.sops.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-instance/app/secret.sops.yaml.j2 new file mode 100644 index 0000000..5b780af --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-instance/app/secret.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: github-webhook-token-secret +stringData: + token: "#{ github_push_token() }#" diff --git a/templates/config/kubernetes/apps/flux-system/flux-instance/ks.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-instance/ks.yaml.j2 new file mode 100644 index 0000000..1a35461 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-instance/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/flux-operator/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-operator/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..08fba38 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-operator/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/flux-operator/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-operator/app/kustomization.yaml.j2 new file mode 100644 index 0000000..2ccd5a8 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-operator/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/flux-system/flux-operator/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-operator/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..701f9d9 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-operator/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/flux-operator/ks.yaml.j2 b/templates/config/kubernetes/apps/flux-system/flux-operator/ks.yaml.j2 new file mode 100644 index 0000000..2e39824 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/flux-operator/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/kustomization.yaml.j2 b/templates/config/kubernetes/apps/flux-system/kustomization.yaml.j2 new file mode 100644 index 0000000..4b0dd43 --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/kustomization.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/flux-system/namespace.yaml.j2 b/templates/config/kubernetes/apps/flux-system/namespace.yaml.j2 new file mode 100644 index 0000000..120760a --- /dev/null +++ b/templates/config/kubernetes/apps/flux-system/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: flux-system + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/templates/config/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..05d0b14 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/cilium/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/kube-system/cilium/app/kustomization.yaml.j2 new file mode 100644 index 0000000..791e352 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/cilium/app/kustomization.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml + - ./networks.yaml diff --git a/templates/config/kubernetes/apps/kube-system/cilium/app/networks.yaml.j2 b/templates/config/kubernetes/apps/kube-system/cilium/app/networks.yaml.j2 new file mode 100644 index 0000000..d13f149 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/cilium/app/networks.yaml.j2 @@ -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 %# diff --git a/templates/config/kubernetes/apps/kube-system/cilium/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/kube-system/cilium/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..dd68c52 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/cilium/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/cilium/ks.yaml.j2 b/templates/config/kubernetes/apps/kube-system/cilium/ks.yaml.j2 new file mode 100644 index 0000000..ea0a835 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/cilium/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..e57f862 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/coredns/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/kube-system/coredns/app/kustomization.yaml.j2 new file mode 100644 index 0000000..2ccd5a8 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/coredns/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/kube-system/coredns/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/kube-system/coredns/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..5c40bea --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/coredns/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/coredns/ks.yaml.j2 b/templates/config/kubernetes/apps/kube-system/coredns/ks.yaml.j2 new file mode 100644 index 0000000..5ac2a87 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/coredns/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/kustomization.yaml.j2 b/templates/config/kubernetes/apps/kube-system/kustomization.yaml.j2 new file mode 100644 index 0000000..2586f65 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/kustomization.yaml.j2 @@ -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 %# diff --git a/templates/config/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..befc886 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml.j2 new file mode 100644 index 0000000..2ccd5a8 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/kube-system/metrics-server/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/kube-system/metrics-server/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..d86e7a5 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/metrics-server/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 b/templates/config/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 new file mode 100644 index 0000000..60042b7 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/namespace.yaml.j2 b/templates/config/kubernetes/apps/kube-system/namespace.yaml.j2 new file mode 100644 index 0000000..7f6e64f --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kube-system + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/templates/config/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..100b095 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml.j2 @@ -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 }}" diff --git a/templates/config/kubernetes/apps/kube-system/reloader/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/kube-system/reloader/app/kustomization.yaml.j2 new file mode 100644 index 0000000..2ccd5a8 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/reloader/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/kube-system/reloader/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/kube-system/reloader/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..0c87ef7 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/reloader/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/reloader/ks.yaml.j2 b/templates/config/kubernetes/apps/kube-system/reloader/ks.yaml.j2 new file mode 100644 index 0000000..5f5c65a --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/reloader/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..86c443a --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml.j2 @@ -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 %# diff --git a/templates/config/kubernetes/apps/kube-system/spegel/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/kube-system/spegel/app/kustomization.yaml.j2 new file mode 100644 index 0000000..025c449 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/spegel/app/kustomization.yaml.j2 @@ -0,0 +1,8 @@ +#% if spegel_enabled %# +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml +#% endif %# diff --git a/templates/config/kubernetes/apps/kube-system/spegel/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/kube-system/spegel/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..6068ab7 --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/spegel/app/ocirepository.yaml.j2 @@ -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 %# diff --git a/templates/config/kubernetes/apps/kube-system/spegel/ks.yaml.j2 b/templates/config/kubernetes/apps/kube-system/spegel/ks.yaml.j2 new file mode 100644 index 0000000..7bc482e --- /dev/null +++ b/templates/config/kubernetes/apps/kube-system/spegel/ks.yaml.j2 @@ -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 %# diff --git a/templates/config/kubernetes/apps/network/cloudflare-dns/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-dns/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..3aa14a9 --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-dns/app/helmrelease.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/network/cloudflare-dns/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-dns/app/kustomization.yaml.j2 new file mode 100644 index 0000000..0684e08 --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-dns/app/kustomization.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./secret.sops.yaml + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/network/cloudflare-dns/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-dns/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..e57e928 --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-dns/app/ocirepository.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/network/cloudflare-dns/app/secret.sops.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-dns/app/secret.sops.yaml.j2 new file mode 100644 index 0000000..0c798d3 --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-dns/app/secret.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cloudflare-dns-secret +stringData: + api-token: "#{ cloudflare_token }#" diff --git a/templates/config/kubernetes/apps/network/cloudflare-dns/ks.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-dns/ks.yaml.j2 new file mode 100644 index 0000000..18d5db5 --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-dns/ks.yaml.j2 @@ -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 diff --git a/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/dnsendpoint.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/dnsendpoint.yaml.j2 new file mode 100644 index 0000000..188047a --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/dnsendpoint.yaml.j2 @@ -0,0 +1,10 @@ +--- +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: cloudflare-tunnel +spec: + endpoints: + - dnsName: "external.${SECRET_DOMAIN}" + recordType: CNAME + targets: ["#{ cloudflare_tunnel_id() }#.cfargotunnel.com"] diff --git a/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..fa3196c --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/helmrelease.yaml.j2 @@ -0,0 +1,84 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cloudflare-tunnel +spec: + chartRef: + kind: OCIRepository + name: cloudflare-tunnel + interval: 1h + values: + controllers: + cloudflare-tunnel: + strategy: RollingUpdate + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: docker.io/cloudflare/cloudflared + tag: 2026.1.1 + env: + NO_AUTOUPDATE: true + TUNNEL_METRICS: 0.0.0.0:8080 + TUNNEL_POST_QUANTUM: true # disable when using http2 + TUNNEL_TRANSPORT_PROTOCOL: quic # or http2 + envFrom: + - secretRef: + name: cloudflare-tunnel-secret + args: ["tunnel", "run"] + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /ready + port: &port 8080 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + resources: + requests: + cpu: 10m + limits: + memory: 256Mi + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + service: + app: + ports: + http: + port: *port + serviceMonitor: + app: + endpoints: + - port: http + configMaps: + config: + data: + config.yaml: |- + ingress: + - hostname: "*.${SECRET_DOMAIN}" + originRequest: + http2Origin: true + originServerName: external.${SECRET_DOMAIN} + service: https://envoy-external.{{ .Release.Namespace }}.svc.cluster.local:443 + - service: http_status:404 + persistence: + config-file: + type: configMap + identifier: config + globalMounts: + - path: /etc/cloudflared/config.yaml + subPath: config.yaml diff --git a/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/kustomization.yaml.j2 new file mode 100644 index 0000000..dbc11de --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/kustomization.yaml.j2 @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./dnsendpoint.yaml + - ./secret.sops.yaml + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..67904bd --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/ocirepository.yaml.j2 @@ -0,0 +1,13 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: OCIRepository +metadata: + name: cloudflare-tunnel +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 diff --git a/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/secret.sops.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/secret.sops.yaml.j2 new file mode 100644 index 0000000..898f2d3 --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-tunnel/app/secret.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cloudflare-tunnel-secret +stringData: + TUNNEL_TOKEN: "#{ cloudflare_tunnel_secret() }#" diff --git a/templates/config/kubernetes/apps/network/cloudflare-tunnel/ks.yaml.j2 b/templates/config/kubernetes/apps/network/cloudflare-tunnel/ks.yaml.j2 new file mode 100644 index 0000000..10ef301 --- /dev/null +++ b/templates/config/kubernetes/apps/network/cloudflare-tunnel/ks.yaml.j2 @@ -0,0 +1,19 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cloudflare-tunnel +spec: + interval: 1h + path: ./kubernetes/apps/network/cloudflare-tunnel/app + postBuild: + substituteFrom: + - name: cluster-secrets + kind: Secret + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + targetNamespace: network + wait: false diff --git a/templates/config/kubernetes/apps/network/envoy-gateway/app/certificate.yaml.j2 b/templates/config/kubernetes/apps/network/envoy-gateway/app/certificate.yaml.j2 new file mode 100644 index 0000000..1f02d1f --- /dev/null +++ b/templates/config/kubernetes/apps/network/envoy-gateway/app/certificate.yaml.j2 @@ -0,0 +1,18 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: "${SECRET_DOMAIN/./-}-production" +spec: + dnsNames: + - "${SECRET_DOMAIN}" + - "*.${SECRET_DOMAIN}" + duration: 160h + issuerRef: + name: letsencrypt-production + kind: ClusterIssuer + privateKey: + algorithm: ECDSA + secretName: "${SECRET_DOMAIN/./-}-production-tls" + usages: + - digital signature diff --git a/templates/config/kubernetes/apps/network/envoy-gateway/app/envoy.yaml.j2 b/templates/config/kubernetes/apps/network/envoy-gateway/app/envoy.yaml.j2 new file mode 100644 index 0000000..1d8fe2c --- /dev/null +++ b/templates/config/kubernetes/apps/network/envoy-gateway/app/envoy.yaml.j2 @@ -0,0 +1,170 @@ +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: envoy +spec: + logging: + level: + default: info + provider: + type: Kubernetes + kubernetes: + envoyDeployment: + replicas: 2 + container: + imageRepository: mirror.gcr.io/envoyproxy/envoy + resources: + requests: + cpu: 100m + limits: + memory: 1Gi + envoyService: + externalTrafficPolicy: Cluster + shutdown: + drainTimeout: 180s + telemetry: + metrics: + prometheus: + compression: + type: Zstd +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: envoy +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: envoy + namespace: network +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: envoy-external + annotations: + external-dns.alpha.kubernetes.io/target: external.${SECRET_DOMAIN} +spec: + gatewayClassName: envoy + infrastructure: + annotations: + external-dns.alpha.kubernetes.io/hostname: external.${SECRET_DOMAIN} + lbipam.cilium.io/ips: "#{ cloudflare_gateway_addr }#" + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + - name: https + protocol: HTTPS + port: 443 + allowedRoutes: + namespaces: + from: All + tls: + certificateRefs: + - kind: Secret + name: ${SECRET_DOMAIN/./-}-production-tls +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: envoy-internal + annotations: + external-dns.alpha.kubernetes.io/target: internal.${SECRET_DOMAIN} +spec: + gatewayClassName: envoy + infrastructure: + annotations: + external-dns.alpha.kubernetes.io/hostname: internal.${SECRET_DOMAIN} + lbipam.cilium.io/ips: "#{ cluster_gateway_addr }#" + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + - name: https + protocol: HTTPS + port: 443 + allowedRoutes: + namespaces: + from: All + tls: + certificateRefs: + - kind: Secret + name: ${SECRET_DOMAIN/./-}-production-tls +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: envoy +spec: + compressor: + - type: Zstd + zstd: {} + - type: Brotli + brotli: {} + - type: Gzip + gzip: {} + retry: + numRetries: 2 + retryOn: + triggers: + - reset + targetSelectors: + - group: gateway.networking.k8s.io + kind: Gateway + tcpKeepalive: {} + timeout: + http: + requestTimeout: 0s +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: ClientTrafficPolicy +metadata: + name: envoy +spec: + clientIPDetection: + xForwardedFor: + trustedCIDRs: + - "#{ cluster_pod_cidr }#" + http2: + onInvalidMessage: TerminateStream + http3: {} + targetSelectors: + - group: gateway.networking.k8s.io + kind: Gateway + tcpKeepalive: {} + tls: + minVersion: "1.2" + alpnProtocols: + - h2 + - http/1.1 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: https-redirect + annotations: + external-dns.alpha.kubernetes.io/controller: none +spec: + parentRefs: + - name: envoy-external + namespace: network + sectionName: http + - name: envoy-internal + namespace: network + sectionName: http + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 diff --git a/templates/config/kubernetes/apps/network/envoy-gateway/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/network/envoy-gateway/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..55f3dea --- /dev/null +++ b/templates/config/kubernetes/apps/network/envoy-gateway/app/helmrelease.yaml.j2 @@ -0,0 +1,20 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: envoy-gateway +spec: + chartRef: + kind: OCIRepository + name: envoy-gateway + interval: 1h + values: + global: + imageRegistry: mirror.gcr.io + config: + envoyGateway: + provider: + type: Kubernetes + kubernetes: + deploy: + type: GatewayNamespace diff --git a/templates/config/kubernetes/apps/network/envoy-gateway/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/network/envoy-gateway/app/kustomization.yaml.j2 new file mode 100644 index 0000000..545ef59 --- /dev/null +++ b/templates/config/kubernetes/apps/network/envoy-gateway/app/kustomization.yaml.j2 @@ -0,0 +1,9 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./certificate.yaml + - ./envoy.yaml + - ./helmrelease.yaml + - ./ocirepository.yaml + - ./podmonitor.yaml diff --git a/templates/config/kubernetes/apps/network/envoy-gateway/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/network/envoy-gateway/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..abc4672 --- /dev/null +++ b/templates/config/kubernetes/apps/network/envoy-gateway/app/ocirepository.yaml.j2 @@ -0,0 +1,13 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: OCIRepository +metadata: + name: envoy-gateway +spec: + interval: 15m + layerSelector: + mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip + operation: copy + ref: + tag: v1.6.2 + url: oci://mirror.gcr.io/envoyproxy/gateway-helm diff --git a/templates/config/kubernetes/apps/network/envoy-gateway/app/podmonitor.yaml.j2 b/templates/config/kubernetes/apps/network/envoy-gateway/app/podmonitor.yaml.j2 new file mode 100644 index 0000000..b199aa8 --- /dev/null +++ b/templates/config/kubernetes/apps/network/envoy-gateway/app/podmonitor.yaml.j2 @@ -0,0 +1,18 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: envoy-proxy +spec: + jobLabel: envoy-proxy + namespaceSelector: + matchNames: + - network + podMetricsEndpoints: + - port: metrics + path: /stats/prometheus + honorLabels: true + selector: + matchLabels: + app.kubernetes.io/component: proxy + app.kubernetes.io/name: envoy diff --git a/templates/config/kubernetes/apps/network/envoy-gateway/ks.yaml.j2 b/templates/config/kubernetes/apps/network/envoy-gateway/ks.yaml.j2 new file mode 100644 index 0000000..78a3ce9 --- /dev/null +++ b/templates/config/kubernetes/apps/network/envoy-gateway/ks.yaml.j2 @@ -0,0 +1,19 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: envoy-gateway +spec: + interval: 1h + path: ./kubernetes/apps/network/envoy-gateway/app + postBuild: + substituteFrom: + - name: cluster-secrets + kind: Secret + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + targetNamespace: network + wait: false diff --git a/templates/config/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml.j2 b/templates/config/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml.j2 new file mode 100644 index 0000000..4d3cbeb --- /dev/null +++ b/templates/config/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml.j2 @@ -0,0 +1,21 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: k8s-gateway +spec: + chartRef: + kind: OCIRepository + name: k8s-gateway + interval: 1h + values: + fullnameOverride: k8s-gateway + domain: "${SECRET_DOMAIN}" + ttl: 1 + service: + type: LoadBalancer + port: 53 + annotations: + lbipam.cilium.io/ips: "#{ cluster_dns_gateway_addr }#" + externalTrafficPolicy: Cluster + watchedResources: ["HTTPRoute", "Service"] diff --git a/templates/config/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml.j2 b/templates/config/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml.j2 new file mode 100644 index 0000000..2ccd5a8 --- /dev/null +++ b/templates/config/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./ocirepository.yaml diff --git a/templates/config/kubernetes/apps/network/k8s-gateway/app/ocirepository.yaml.j2 b/templates/config/kubernetes/apps/network/k8s-gateway/app/ocirepository.yaml.j2 new file mode 100644 index 0000000..5e108a9 --- /dev/null +++ b/templates/config/kubernetes/apps/network/k8s-gateway/app/ocirepository.yaml.j2 @@ -0,0 +1,13 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: OCIRepository +metadata: + name: k8s-gateway +spec: + interval: 1h + layerSelector: + mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip + operation: copy + ref: + tag: 3.4.1 + url: oci://ghcr.io/k8s-gateway/charts/k8s-gateway diff --git a/templates/config/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 b/templates/config/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 new file mode 100644 index 0000000..844476f --- /dev/null +++ b/templates/config/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 @@ -0,0 +1,19 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: k8s-gateway +spec: + interval: 1h + path: ./kubernetes/apps/network/k8s-gateway/app + postBuild: + substituteFrom: + - name: cluster-secrets + kind: Secret + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + targetNamespace: network + wait: false diff --git a/templates/config/kubernetes/apps/network/kustomization.yaml.j2 b/templates/config/kubernetes/apps/network/kustomization.yaml.j2 new file mode 100644 index 0000000..6e9cf9c --- /dev/null +++ b/templates/config/kubernetes/apps/network/kustomization.yaml.j2 @@ -0,0 +1,14 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: network + +components: + - ../../components/sops + +resources: + - ./namespace.yaml + - ./cloudflare-dns/ks.yaml + - ./cloudflare-tunnel/ks.yaml + - ./envoy-gateway/ks.yaml + - ./k8s-gateway/ks.yaml diff --git a/templates/config/kubernetes/apps/network/namespace.yaml.j2 b/templates/config/kubernetes/apps/network/namespace.yaml.j2 new file mode 100644 index 0000000..17866ba --- /dev/null +++ b/templates/config/kubernetes/apps/network/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: network + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/templates/config/kubernetes/components/sops/cluster-secrets.sops.yaml.j2 b/templates/config/kubernetes/components/sops/cluster-secrets.sops.yaml.j2 new file mode 100644 index 0000000..881f897 --- /dev/null +++ b/templates/config/kubernetes/components/sops/cluster-secrets.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cluster-secrets +stringData: + SECRET_DOMAIN: "#{ cloudflare_domain }#" diff --git a/templates/config/kubernetes/components/sops/kustomization.yaml.j2 b/templates/config/kubernetes/components/sops/kustomization.yaml.j2 new file mode 100644 index 0000000..1948b44 --- /dev/null +++ b/templates/config/kubernetes/components/sops/kustomization.yaml.j2 @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +resources: + - ./cluster-secrets.sops.yaml diff --git a/templates/config/kubernetes/flux/cluster/ks.yaml.j2 b/templates/config/kubernetes/flux/cluster/ks.yaml.j2 new file mode 100644 index 0000000..77d76e8 --- /dev/null +++ b/templates/config/kubernetes/flux/cluster/ks.yaml.j2 @@ -0,0 +1,57 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cluster-apps + namespace: flux-system +spec: + decryption: + provider: sops + deletionPolicy: WaitForTermination + interval: 1h + path: ./kubernetes/apps + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + wait: false + patches: + - # Add Kustomization defaults for all child Kustomizations + patch: |- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: _ + spec: + decryption: + provider: sops + deletionPolicy: WaitForTermination + patches: + - patch: |- + apiVersion: helm.toolkit.fluxcd.io/v2 + kind: HelmRelease + metadata: + name: _ + spec: + install: + crds: CreateReplace + strategy: + name: RetryOnFailure + rollback: + cleanupOnFail: true + recreate: true + upgrade: + cleanupOnFail: true + crds: CreateReplace + strategy: + name: RemediateOnFailure + remediation: + remediateLastFailure: true + retries: 2 + target: + group: helm.toolkit.fluxcd.io + kind: HelmRelease + target: + group: kustomize.toolkit.fluxcd.io + kind: Kustomization diff --git a/templates/config/talos/patches/README.md.j2 b/templates/config/talos/patches/README.md.j2 new file mode 100644 index 0000000..b968188 --- /dev/null +++ b/templates/config/talos/patches/README.md.j2 @@ -0,0 +1,15 @@ +# Talos Patching + +This directory contains Kustomization patches that are added to the talhelper configuration file. + + + +## Patch Directories + +Under this `patches` directory, there are several sub-directories that can contain patches that are added to the talhelper configuration file. +Each directory is optional and therefore might not created by default. + +- `global/`: patches that are applied to both the controller and worker configurations +- `controller/`: patches that are applied to the controller configurations +- `worker/`: patches that are applied to the worker configurations +- `${node-hostname}/`: patches that are applied to the node with the specified name diff --git a/templates/config/talos/patches/controller/cluster.yaml.j2 b/templates/config/talos/patches/controller/cluster.yaml.j2 new file mode 100644 index 0000000..f23e9d6 --- /dev/null +++ b/templates/config/talos/patches/controller/cluster.yaml.j2 @@ -0,0 +1,23 @@ +cluster: + allowSchedulingOnControlPlanes: true + apiServer: + admissionControl: + $$patch: delete + extraArgs: + # https://kubernetes.io/docs/tasks/extend-kubernetes/configure-aggregation-layer/ + enable-aggregator-routing: true + controllerManager: + extraArgs: + bind-address: 0.0.0.0 + coreDNS: + disabled: true + etcd: + extraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + advertisedSubnets: + - #{ node_cidr }# + proxy: + disabled: true + scheduler: + extraArgs: + bind-address: 0.0.0.0 diff --git a/templates/config/talos/patches/global/machine-files.yaml.j2 b/templates/config/talos/patches/global/machine-files.yaml.j2 new file mode 100644 index 0000000..327c8e3 --- /dev/null +++ b/templates/config/talos/patches/global/machine-files.yaml.j2 @@ -0,0 +1,9 @@ +machine: + files: + - op: create + path: /etc/cri/conf.d/20-customization.part + content: | + [plugins."io.containerd.cri.v1.images"] + discard_unpacked_layers = false + [plugins."io.containerd.cri.v1.runtime"] + device_ownership_from_security_context = true diff --git a/templates/config/talos/patches/global/machine-kubelet.yaml.j2 b/templates/config/talos/patches/global/machine-kubelet.yaml.j2 new file mode 100644 index 0000000..5b04023 --- /dev/null +++ b/templates/config/talos/patches/global/machine-kubelet.yaml.j2 @@ -0,0 +1,7 @@ +machine: + kubelet: + extraConfig: + serializeImagePulls: false + nodeIP: + validSubnets: + - #{ node_cidr }# diff --git a/templates/config/talos/patches/global/machine-network.yaml.j2 b/templates/config/talos/patches/global/machine-network.yaml.j2 new file mode 100644 index 0000000..6356f37 --- /dev/null +++ b/templates/config/talos/patches/global/machine-network.yaml.j2 @@ -0,0 +1,7 @@ +machine: + network: + disableSearchDomain: true + nameservers: + #% for item in node_dns_servers %# + - #{ item }# + #% endfor %# diff --git a/templates/config/talos/patches/global/machine-sysctls.yaml.j2 b/templates/config/talos/patches/global/machine-sysctls.yaml.j2 new file mode 100644 index 0000000..c56aa6d --- /dev/null +++ b/templates/config/talos/patches/global/machine-sysctls.yaml.j2 @@ -0,0 +1,11 @@ +machine: + sysctls: + fs.inotify.max_user_watches: "1048576" # Watchdog + fs.inotify.max_user_instances: "8192" # Watchdog + net.core.rmem_max: "7500000" # Cloudflared | QUIC + net.core.wmem_max: "7500000" # Cloudflared | QUIC + net.ipv4.neigh.default.gc_thresh1: "4096" # Prevent ARP cache overflows + net.ipv4.neigh.default.gc_thresh2: "8192" # Prevent ARP cache overflows + net.ipv4.neigh.default.gc_thresh3: "16384" # Prevent ARP cache overflows + net.ipv4.tcp_slow_start_after_idle: "0" # Preserve congestion window after idle + user.max_user_namespaces: "11255" # User Namespaces diff --git a/templates/config/talos/patches/global/machine-time.yaml.j2 b/templates/config/talos/patches/global/machine-time.yaml.j2 new file mode 100644 index 0000000..b3004e7 --- /dev/null +++ b/templates/config/talos/patches/global/machine-time.yaml.j2 @@ -0,0 +1,7 @@ +machine: + time: + disabled: false + servers: + #% for item in node_ntp_servers %# + - #{ item }# + #% endfor %# diff --git a/templates/config/talos/talconfig.yaml.j2 b/templates/config/talos/talconfig.yaml.j2 new file mode 100644 index 0000000..bff070c --- /dev/null +++ b/templates/config/talos/talconfig.yaml.j2 @@ -0,0 +1,156 @@ +--- +clusterName: kubernetes + +talosVersion: "${talosVersion}" +kubernetesVersion: "${kubernetesVersion}" + +endpoint: https://#{ cluster_api_addr }#:6443 +additionalApiServerCertSans: &sans + - "127.0.0.1" + - "#{ cluster_api_addr }#" + #% for item in cluster_api_tls_sans %# + - "#{ item }#" + #% endfor %# +additionalMachineCertSans: *sans + +clusterPodNets: ["#{ cluster_pod_cidr }#"] +clusterSvcNets: ["#{ cluster_svc_cidr }#"] + +# Disable built-in CNI to use Cilium +cniConfig: + name: none + +nodes: + #% for item in nodes %# + - hostname: "#{ item.name }#" + ipAddress: "#{ item.address }#" + #% if item.disk.startswith('/') %# + installDisk: "#{ item.disk }#" + #% else %# + installDiskSelector: + serial: "#{ item.disk }#" + #% endif %# + machineSpec: + secureboot: #{ (true if item.secureboot else false) | string | lower }# + talosImageURL: factory.talos.dev/installer#{ "-secureboot" if item.secureboot | default(false, true) }#/#{ item.schematic_id }# + controlPlane: #{ (item.controller) | string | lower }# + networkInterfaces: + - deviceSelector: + hardwareAddr: "#{ item.mac_addr | lower }#" + #% if node_vlan_tag %# + vlans: + - vlanId: #{ node_vlan_tag }# + addresses: + - "#{ item.address }#/#{ node_cidr.split('/') | last }#" + mtu: #{ item.mtu | default(1500, true) }# + routes: + - gateway: "#{ node_default_gateway }#" + network: 0.0.0.0/0 + #% if item.controller %# + vip: + ip: "#{ cluster_api_addr }#" + #% endif %# + #% else %# + dhcp: false + addresses: + - "#{ item.address }#/#{ node_cidr.split('/') | last }#" + routes: + - gateway: "#{ node_default_gateway }#" + network: 0.0.0.0/0 + mtu: #{ item.mtu | default(1500, true) }# + #% if item.controller %# + vip: + ip: "#{ cluster_api_addr }#" + #% endif %# + #% endif %# + #% if talos_patches('%s' % (item.name)) | length == 0 %# + #% if item.encrypt_disk | default(false, true) or (item.kernel_modules | default([], true) | length > 0) %# + patches: + #% if item.encrypt_disk | default(false, true) %# + - # Encrypt system disk with TPM + |- + machine: + systemDiskEncryption: + state: + provider: luks2 + keys: + - slot: 0 + tpm: {} + ephemeral: + provider: luks2 + keys: + - slot: 0 + tpm: {} + #% endif %# + #% if item.kernel_modules | default([], true) | length > 0 %# + - # Load kernel modules + |- + machine: + kernel: + modules: + #% for module in item.kernel_modules %# + - name: #{ module }# + #% endfor %# + #% endif %# + #% endif %# + #% else %# + #% for file in talos_patches('%s' % (item.name)) %# + #% if loop.index == 1 %# + patches: + #% if item.encrypt_disk | default(false, true) %# + - |- + machine: + systemDiskEncryption: + state: + provider: luks2 + keys: + - slot: 0 + tpm: {} + ephemeral: + provider: luks2 + keys: + - slot: 0 + tpm: {} + #% endif %# + #% if item.kernel_modules | default([], true) | length > 0 %# + - |- + machine: + kernel: + modules: + #% for module in item.kernel_modules %# + - name: #{ module }# + #% endfor %# + #% endif %# + #% endif %# + - "@./patches/#{ item.name }#/#{ file | basename }#" + #% endfor %# + #% endif %# + #% endfor %# + +#% for file in talos_patches('global') %# +#% if loop.index == 1 %# +# Global patches +patches: +#% endif %# + - "@./patches/global/#{ file | basename }#" +#% endfor %# + +#% for file in talos_patches('controller') %# +#% if loop.index == 1 %# +# Controller patches +controlPlane: + patches: +#% endif %# + - "@./patches/controller/#{ file | basename }#" +#% endfor %# + +#% if (nodes | selectattr('controller', 'equalto', False) | list | length) and (talos_patches('worker') | length) %# +#% for file in talos_patches('worker') %# +#% if loop.index == 1 %# +# Worker patches +worker: + patches: +#% endif %# + - "@./patches/worker/#{ file | basename }#" +#% endfor %# +#% endif %# diff --git a/templates/config/talos/talenv.yaml.j2 b/templates/config/talos/talenv.yaml.j2 new file mode 100644 index 0000000..d64ee92 --- /dev/null +++ b/templates/config/talos/talenv.yaml.j2 @@ -0,0 +1,4 @@ +# renovate: datasource=docker depName=ghcr.io/siderolabs/installer +talosVersion: v1.12.2 +# renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet +kubernetesVersion: v1.35.0 diff --git a/templates/overrides/readme.partial.yaml.j2 b/templates/overrides/readme.partial.yaml.j2 new file mode 100644 index 0000000..b73f753 --- /dev/null +++ b/templates/overrides/readme.partial.yaml.j2 @@ -0,0 +1,5 @@ +#| Place user jinja template overrides in this file's directory |# +#| Docs: https://mirkolenz.github.io/makejinja/makejinja.html |# +#| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/makejinja.toml |# +#| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input1/not-empty.yaml.jinja |# +#| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input2/not-empty.yaml.jinja |# diff --git a/templates/scripts/plugin.py b/templates/scripts/plugin.py new file mode 100644 index 0000000..82f00ed --- /dev/null +++ b/templates/scripts/plugin.py @@ -0,0 +1,168 @@ +from pathlib import Path +from typing import Any + +import base64 +import ipaddress +import makejinja +import re +import json + + +# Return the filename of a path without the j2 extension +def basename(value: str) -> str: + return Path(value).stem + + +# Return the nth host in a CIDR range +def nthhost(value: str, query: int) -> str: + try: + network = ipaddress.ip_network(value, strict=False) + if 0 <= query < network.num_addresses: + return str(network[query]) + except ValueError: + pass + return False + + +# Return the age public or private key from age.key +def age_key(key_type: str, file_path: str = 'age.key') -> str: + try: + with open(file_path, 'r') as file: + file_content = file.read().strip() + if key_type == 'public': + key_match = re.search(r"# public key: (age1[\w]+)", file_content) + if not key_match: + raise ValueError("Could not find public key in the age key file.") + return key_match.group(1) + elif key_type == 'private': + key_match = re.search(r"(AGE-SECRET-KEY-[\w]+)", file_content) + if not key_match: + raise ValueError("Could not find private key in the age key file.") + return key_match.group(1) + else: + raise ValueError("Invalid key type. Use 'public' or 'private'.") + except FileNotFoundError: + raise FileNotFoundError(f"File not found: {file_path}") + except Exception as e: + raise RuntimeError(f"Unexpected error while processing {file_path}: {e}") + + +# Return cloudflare tunnel fields from cloudflare-tunnel.json +def cloudflare_tunnel_id(file_path: str = 'cloudflare-tunnel.json') -> str: + try: + with open(file_path, 'r') as file: + data = json.load(file) + tunnel_id = data.get("TunnelID") + if tunnel_id is None: + raise KeyError(f"Missing 'TunnelID' key in {file_path}") + return tunnel_id + + except FileNotFoundError: + raise FileNotFoundError(f"File not found: {file_path}") + except json.JSONDecodeError: + raise ValueError(f"Could not decode JSON file: {file_path}") + except KeyError as e: + raise KeyError(f"Error in JSON structure: {e}") + except Exception as e: + raise RuntimeError(f"Unexpected error while processing {file_path}: {e}") + + +# Return cloudflare tunnel fields from cloudflare-tunnel.json in TUNNEL_TOKEN format +def cloudflare_tunnel_secret(file_path: str = 'cloudflare-tunnel.json') -> str: + try: + with open(file_path, 'r') as file: + data = json.load(file) + transformed_data = { + "a": data["AccountTag"], + "t": data["TunnelID"], + "s": data["TunnelSecret"] + } + json_string = json.dumps(transformed_data, separators=(',', ':')) + return base64.b64encode(json_string.encode('utf-8')).decode('utf-8') + + except FileNotFoundError: + raise FileNotFoundError(f"File not found: {file_path}") + except json.JSONDecodeError: + raise ValueError(f"Could not decode JSON file: {file_path}") + except KeyError as e: + raise KeyError(f"Missing key in JSON file {file_path}: {e}") + except Exception as e: + raise RuntimeError(f"Unexpected error while processing {file_path}: {e}") + + +# Return the GitHub deploy key from github-deploy.key +def github_deploy_key(file_path: str = 'github-deploy.key') -> str: + try: + with open(file_path, 'r') as file: + return file.read().strip() + except FileNotFoundError: + raise FileNotFoundError(f"File not found: {file_path}") + except Exception as e: + raise RuntimeError(f"Unexpected error while reading {file_path}: {e}") + + +# Return the Flux / GitHub push token from github-push-token.txt +def github_push_token(file_path: str = 'github-push-token.txt') -> str: + try: + with open(file_path, 'r') as file: + return file.read().strip() + except FileNotFoundError: + raise FileNotFoundError(f"File not found: {file_path}") + except Exception as e: + raise RuntimeError(f"Unexpected error while reading {file_path}: {e}") + + +# Return a list of files in the talos patches directory +def talos_patches(value: str) -> list[str]: + path = Path(f'templates/config/talos/patches/{value}') + if not path.is_dir(): + return [] + return [str(f) for f in sorted(path.glob('*.yaml.j2')) if f.is_file()] + + +class Plugin(makejinja.plugin.Plugin): + def __init__(self, data: dict[str, Any]): + self._data = data + + + def data(self) -> makejinja.plugin.Data: + data = self._data + + # Set default values for optional fields + data.setdefault('node_default_gateway', nthhost(data.get('node_cidr'), 1)) + data.setdefault('node_dns_servers', ['1.1.1.1', '1.0.0.1']) + data.setdefault('node_ntp_servers', ['162.159.200.1', '162.159.200.123']) + data.setdefault('cluster_pod_cidr', '10.42.0.0/16') + data.setdefault('cluster_svc_cidr', '10.43.0.0/16') + data.setdefault('repository_branch', 'main') + data.setdefault('repository_visibility', 'public') + data.setdefault('cilium_loadbalancer_mode', 'dsr') + + # If all BGP keys are set, enable BGP + bgp_keys = ['cilium_bgp_router_addr', 'cilium_bgp_router_asn', 'cilium_bgp_node_asn'] + bgp_enabled = all(data.get(key) for key in bgp_keys) + data.setdefault('cilium_bgp_enabled', bgp_enabled) + + # If there is more than one node, enable spegel + spegel_enabled = len(data.get('nodes')) > 1 + data.setdefault('spegel_enabled', spegel_enabled) + + return data + + + def filters(self) -> makejinja.plugin.Filters: + return [ + basename, + nthhost + ] + + + def functions(self) -> makejinja.plugin.Functions: + return [ + age_key, + cloudflare_tunnel_id, + cloudflare_tunnel_secret, + github_deploy_key, + github_push_token, + talos_patches + ]