diff --git a/.github/tests/addons.yaml b/.github/tests/addons.yaml new file mode 100644 index 00000000..c2248a94 --- /dev/null +++ b/.github/tests/addons.yaml @@ -0,0 +1,34 @@ +--- +homepage: + enabled: true + +grafana: + enabled: true + password: fake + +kube_prometheus_stack: + enabled: true + +kubernetes_dashboard: + enabled: true + +weave_gitops: + enabled: true + password: fake + +csi_driver_nfs: + enabled: true + storage_class: + - name: fake + server: fake + share: /fake + +system_upgrade_controller: + enabled: true + +discord_template_notifier: + enabled: true + webhook_url: https://discord.com/api/webhooks/fake/fake + +volsync: + enabled: true diff --git a/.github/tests/config.yaml b/.github/tests/config.yaml new file mode 100644 index 00000000..25269fe5 --- /dev/null +++ b/.github/tests/config.yaml @@ -0,0 +1,36 @@ +--- +ci_test: true + +bootstrap_distribution: k3s +bootstrap_github_username: onedr0p +bootstrap_github_repository_name: flux-cluster-template +bootstrap_github_repository_branch: main +bootstrap_age_public_key: $BOOTSTRAP_AGE_PUBLIC_KEY +bootstrap_timezone: Etc/UTC +bootstrap_acme_email: fake +bootstrap_acme_production_enabled: false +bootstrap_flux_github_webhook_token: fake +bootstrap_cloudflare_domain: fake +bootstrap_cloudflare_token: fake +bootstrap_cloudflare_account_tag: fake +bootstrap_cloudflare_tunnel_secret: fake +bootstrap_cloudflare_tunnel_id: fake +bootstrap_node_cidr: 10.10.10.0/24 +bootstrap_kubeapi_addr: 10.10.10.254 +bootstrap_k8s_gateway_addr: 10.10.10.253 +bootstrap_external_ingress_addr: 10.10.10.252 +bootstrap_internal_ingress_addr: 10.10.10.251 +bootstrap_cilium_loadbalancer_mode: dsr +bootstrap_ipv6_enabled: false +bootstrap_cluster_cidr: 10.42.0.0/16 +bootstrap_service_cidr: 10.43.0.0/16 +bootstrap_local_storage_path: /var/openebs/local +bootstrap_nodes: + master: + - name: k8s-0 + address: 10.10.10.100 + username: fake + worker: + - name: k8s-1 + address: 10.10.10.101 + username: fake diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 00000000..5d73cb6c --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,77 @@ +--- +name: "e2e" + +on: + pull_request: + branches: ["main"] + paths: ["bootstrap/**"] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +jobs: + e2e: + name: e2e + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: "3.12" + + - name: Cache venv + uses: actions/cache@v3 + with: + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'requirements.yaml') }} + path: .venv + + - name: Setup Homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Setup Workflow Tools + shell: bash + run: brew install go-task + + - name: Install Brew dependencies + shell: bash + run: task brew:deps + + - name: Initialize Direnv + shell: bash + run: direnv allow . + + - name: Initialize Sops Age key + shell: bash + run: task sops:init + + - name: Install Ansible dependencies + shell: bash + run: task ansible:deps + + - name: Initialize config + shell: bash + run: | + task init + cp ./.github/tests/config.yaml ./bootstrap/vars/config.yaml + cp ./.github/tests/addons.yaml ./bootstrap/vars/addons.yaml + export BOOTSTRAP_AGE_PUBLIC_KEY=$(sed -n 's/# public key: //gp' age.key) + envsubst < ./bootstrap/vars/config.yaml | sponge ./bootstrap/vars/config.yaml + + - name: Run configure + shell: bash + run: task --yes configure + + - name: List Hosts + shell: bash + run: task ansible:list + + - name: Run repo clean and reset + shell: bash + run: | + task repo:clean + task --yes repo:reset diff --git a/.taskfiles/BrewTasks.yaml b/.taskfiles/BrewTasks.yaml index b6b79d12..60a26cc5 100644 --- a/.taskfiles/BrewTasks.yaml +++ b/.taskfiles/BrewTasks.yaml @@ -7,24 +7,27 @@ tasks: deps: desc: Install workstation dependencies with Brew cmd: brew install {{.DEPS}} {{.CLI_ARGS}} - preconditions: - - sh: command -v brew - msg: | - Homebrew is not installed. Using MacOS, Linux or WSL? - Head over to https://brew.sh to get up and running. vars: DEPS: >- age cilium-cli cloudflared + direnv fluxcd/tap/flux helm jq k0sproject/tap/k0sctl k9s kubeconform + kubecolor/tap/kubecolor kubernetes-cli kustomize + moreutils sops stern yq + preconditions: + - sh: command -v brew + msg: | + Homebrew is not installed. Using MacOS, Linux or WSL? + Head over to https://brew.sh to get up and running. diff --git a/.taskfiles/RepoTasks.yaml b/.taskfiles/RepoTasks.yaml index dffddb18..d832e500 100644 --- a/.taskfiles/RepoTasks.yaml +++ b/.taskfiles/RepoTasks.yaml @@ -7,12 +7,15 @@ tasks: clean: desc: Clean files and directories no longer needed after cluster bootstrap cmds: + # Clean up CI + - rm -rf {{.ROOT_DIR}}/.github/tests + - rm -rf {{.ROOT_DIR}}/.github/workflows/e2e.yaml # Move bootstrap directory to gitignored directory - mkdir -p {{.ROOT_DIR}}/.private - mv {{.ROOT_DIR}}/bootstrap {{.ROOT_DIR}}/.private # Update renovate.json5 - - sed -i '' 's/(..\.j2)\?(..\.j2)\?//g' {{.ROOT_DIR}}/.github/renovate.json5 - - sed -i '' 's/addons/d' {{.ROOT_DIR}}/.github/renovate.json5 + - sed -i {{if eq OS "darwin"}}''{{end}} 's/(..\.j2)\?(..\.j2)\?//g' {{.ROOT_DIR}}/.github/renovate.json5 + - sed -i {{if eq OS "darwin"}}''{{end}} '/addons/d' {{.ROOT_DIR}}/.github/renovate.json5 preconditions: - { msg: "bootstrap dir not found", sh: "test -d {{.ROOT_DIR}}/bootstrap" } - { msg: "renovate.json5 not found", sh: "test -f {{.ROOT_DIR}}/.github/renovate.json5" } diff --git a/.taskfiles/SopsTasks.yaml b/.taskfiles/SopsTasks.yaml new file mode 100644 index 00000000..53906ee1 --- /dev/null +++ b/.taskfiles/SopsTasks.yaml @@ -0,0 +1,35 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +tasks: + + init: + desc: Initialize Sops Age Key + cmd: age-keygen --output {{.ROOT_DIR}}/age.key + status: + - test -f "{{.ROOT_DIR}}/age.key" + + encrypt: + desc: Encrypt a secret + summary: | + Args: + file: Path to file to encrypt (required) + cmd: sops --encrypt --in-place {{.file}} + vars: + claim: '{{ or .file (fail "Argument (file) is required") }}' + preconditions: + - { msg: "Sops config not found", sh: "test -f {{.ROOT_DIR}}/.sops.yaml" } + - { msg: "Age key not found", sh: "test -f {{.ROOT_DIR}}/age.key" } + + decrypt: + desc: Decrypt a secret + summary: | + Args: + file: Path to file to decrypt (required) + cmd: sops --decrypt --in-place {{.SECRET}} + vars: + claim: '{{ or .file (fail "Argument (file) is required") }}' + preconditions: + - { msg: "Sops config not found", sh: "test -f {{.ROOT_DIR}}/.sops.yaml" } + - { msg: "Age key not found", sh: "test -f {{.ROOT_DIR}}/age.key" } diff --git a/README.md b/README.md index 2b8c1d7e..2f73352c 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ Once you have installed Debian on your nodes, there are six stages to getting a task ansible:deps ``` -4. Install the required tools: [age](https://github.com/FiloSottile/age), [flux](https://toolkit.fluxcd.io/), [cloudflared](https://github.com/cloudflare/cloudflared), [kubectl](https://kubernetes.io/docs/tasks/tools/), [sops](https://github.com/getsops/sops), [kubeconform](https://github.com/yannh/kubeconform) +4. Install the required tools: [age](https://github.com/FiloSottile/age), [flux](https://toolkit.fluxcd.io/), [cloudflared](https://github.com/cloudflare/cloudflared), [kubectl](https://kubernetes.io/docs/tasks/tools/), [sops](https://github.com/getsops/sops), [kubeconform](https://github.com/yannh/kubeconform), [kustomize](https://github.com/kubernetes-sigs/kustomize) 📍 _Not using brew? Make sure to look up how to install the latest version of each of these CLI tools yourself._ diff --git a/Taskfile.yaml b/Taskfile.yaml index 791a2db7..2689b402 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -19,6 +19,7 @@ includes: flux: .taskfiles/FluxTasks.yaml k0s: .taskfiles/K0sTasks.yaml repo: .taskfiles/RepoTasks.yaml + sops: .taskfiles/SopsTasks.yaml tasks: @@ -27,7 +28,7 @@ tasks: init: desc: Initialize configuration files cmds: - - cp -n {{.BOOTSTRAP_DIR}}vars/addons.sample.yaml {{.BOOTSTRAP_DIR}}/vars/addons.yaml + - cp -n {{.BOOTSTRAP_DIR}}/vars/addons.sample.yaml {{.BOOTSTRAP_DIR}}/vars/addons.yaml - cp -n {{.BOOTSTRAP_DIR}}/vars/config.sample.yaml {{.BOOTSTRAP_DIR}}/vars/config.yaml - cmd: echo "=== Configuration files copied ===" silent: true diff --git a/bootstrap/configure.yaml b/bootstrap/configure.yaml index 3b409276..8a5c3699 100644 --- a/bootstrap/configure.yaml +++ b/bootstrap/configure.yaml @@ -2,6 +2,7 @@ - name: Cluster Installation hosts: localhost connection: local + gather_facts: false vars_files: - vars/config.yaml - vars/addons.yaml diff --git a/bootstrap/tasks/validation/cli.yaml b/bootstrap/tasks/validation/cli.yaml index 7e0215b9..8c3ea04c 100644 --- a/bootstrap/tasks/validation/cli.yaml +++ b/bootstrap/tasks/validation/cli.yaml @@ -2,7 +2,7 @@ - name: Check for required CLI tools ansible.builtin.shell: | command -v {{ item }} >/dev/null 2>&1 - loop: [age, cloudflared, flux, sops] + loop: [age, cloudflared, flux, sops, kubeconform, kustomize, jq] changed_when: false check_mode: false register: result diff --git a/bootstrap/tasks/validation/cloudflare.yaml b/bootstrap/tasks/validation/cloudflare.yaml index 4c2885eb..cc9ac633 100644 --- a/bootstrap/tasks/validation/cloudflare.yaml +++ b/bootstrap/tasks/validation/cloudflare.yaml @@ -1,5 +1,6 @@ --- - name: Query Cloudflare zone + when: not ci_test | default(false) ansible.builtin.uri: url: https://api.cloudflare.com/client/v4/zones?name={{ bootstrap_cloudflare_domain }}&status=active headers: @@ -11,12 +12,14 @@ register: result - name: Check if Cloudflare zone exists + when: not ci_test | default(false) ansible.builtin.assert: that: result.json.success is true success_msg: Cloudflare zone {{ bootstrap_cloudflare_domain }} exists fail_msg: Cloudflare zone {{ bootstrap_cloudflare_domain }} does not exist - name: Query Cloudflared tunnel + when: not ci_test | default(false) ansible.builtin.uri: url: https://api.cloudflare.com/client/v4/accounts/{{ bootstrap_cloudflare_account_tag }}/cfd_tunnel/{{ bootstrap_cloudflare_tunnel_id }} headers: @@ -28,6 +31,7 @@ register: result - name: Check if Cloudflared tunnel exists + when: not ci_test | default(false) ansible.builtin.assert: that: result.json.success is true success_msg: Cloudflared tunnel {{ bootstrap_cloudflare_tunnel_id }} exists diff --git a/bootstrap/tasks/validation/net.yaml b/bootstrap/tasks/validation/net.yaml index c17ca08a..f65b862e 100644 --- a/bootstrap/tasks/validation/net.yaml +++ b/bootstrap/tasks/validation/net.yaml @@ -194,6 +194,7 @@ quiet: true - name: Verify SSH port is reachable + when: not ci_test | default(false) ansible.builtin.wait_for: host: "{{ current_address }}" port: 22