├── .gitignore ├── .gitlab-ci.yml ├── Argcfile.sh ├── README.md ├── ansible ├── .gitignore ├── ansible.cfg ├── playbooks │ ├── 10-general.yml │ ├── 20-vault.yml │ ├── 30-vault-agent.yml │ ├── 40-wireguard.yml │ ├── 50-consul.yml │ ├── 60-nomad.yml │ ├── library │ │ └── nomad_job.py │ ├── nomad-jobs.yml │ └── verify.yml ├── site.yml └── templates │ ├── consul │ ├── client.hcl │ ├── consul.hcl.j2 │ ├── consul.service.j2 │ ├── initial-setup.sh │ └── server.hcl │ ├── nomad │ ├── client.hcl.j2 │ └── nomad.hcl.j2 │ ├── vault-agent │ ├── cron.monthly │ └── vault-agent.service.j2 │ ├── vault │ ├── initial-setup.sh │ └── vault.hcl.j2 │ └── wireguard │ └── wg-add-peer.j2 ├── docs ├── Upgrading.md ├── general.md ├── ssl.md └── wireguard.md ├── nomad ├── .gitignore ├── .terraform.lock.hcl ├── backup │ ├── README.md │ ├── main.tf │ └── nomad.hcl ├── dashboard │ ├── README.md │ ├── index.html │ ├── main.tf │ └── nomad.hcl.tpl ├── joplin │ ├── README.md │ ├── joplin.nomad │ └── tasks.yml ├── jupyter │ ├── README.md │ ├── jupyter.nomad │ └── tasks.yml ├── librechat │ ├── README.md │ ├── main.tf │ └── nomad.hcl ├── lobechat │ ├── README.md │ ├── main.tf │ └── nomad.hcl ├── main.tf ├── nextcloud │ ├── README.md │ ├── nextcloud.nomad │ ├── nginx.conf │ └── tasks.yml ├── open-webui │ ├── README.md │ ├── main.tf │ └── nomad.hcl ├── seafile │ ├── README.md │ ├── main.tf │ └── nomad.hcl ├── traefik │ ├── README.md │ ├── main.tf │ └── nomad.hcl.tpl └── whoami │ ├── README.md │ ├── main.tf │ └── nomad.hcl ├── poetry.lock ├── pyproject.toml ├── share ├── Deploy.gitlab-ci.yml ├── Dockerfile └── ca.crt └── terraform ├── .terraform.lock.hcl ├── main.tf ├── master_user_data.py ├── prod-control.tfvars └── test-control.tfvars /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | terraform.tfstate 3 | terraform.tfstate.* 4 | /vault.txt 5 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | build-deploy-image: 2 | image: docker:latest 3 | stage: build 4 | services: 5 | - docker:dind 6 | interruptible: true 7 | before_script: 8 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 9 | - docker buildx create --use --bootstrap 10 | script: 11 | - cd share 12 | - docker buildx build 13 | --provenance false 14 | -t $CI_REGISTRY_IMAGE/deploy:latest --push . 15 | # Run this job only on the main branch and only when the a related file 16 | # changes. 17 | rules: 18 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 19 | changes: 20 | - share/Dockerfile 21 | - share/ca.crt 22 | -------------------------------------------------------------------------------- /Argcfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # @describe Example Argcfile 3 | # For more information about argc, see https://github.com/sigoden/argc 4 | 5 | set -eu 6 | 7 | ansible_variables=( 8 | -e DC=nbg1 -e wireguard_ip=172.30.0.1 -e traefik_domain="${BASE_DOMAIN:?}" 9 | ) 10 | 11 | # @cmd Ensure that all cloud resources are up to date 12 | # 13 | # Example: preview infrastructure changes 14 | # argc infra plan 15 | # 16 | # Example: apply infrastructure changes 17 | # argc infra apply 18 | # 19 | # Example: see terraform usage 20 | # argc infra -- --help 21 | # 22 | # @arg args+ Arguments to terraform 23 | # @option -e --environment=prod-control Environment to use 24 | infra() { 25 | export TF_WORKSPACE=${argc_environment:?} 26 | exec terraform -chdir=terraform "${argc_args[0]:?}" -var-file="${TF_WORKSPACE}.tfvars" "${argc_args[@]:1}" 27 | } 28 | 29 | # @cmd Get the IP address of master.node.consul 30 | # @option -e --environment=prod-control Environment to use 31 | master-ip() { 32 | export TF_WORKSPACE=${argc_environment:?} 33 | terraform -chdir=terraform output -raw master_ip 34 | echo 35 | } 36 | 37 | # @cmd Sync all configuration files on the master server 38 | # 39 | # Example: preview changes 40 | # argc ansible -CD 41 | # 42 | # Example: apply changes 43 | # argc ansible 44 | # 45 | # Example: see ansible usage 46 | # argc ansible -- --help 47 | # @arg args* Arguments to ansible 48 | ansible() { 49 | ANSIBLE_CONFIG=ansible/ansible.cfg exec ansible-playbook -u ubuntu -i master.node.consul, "${ansible_variables[@]}" ansible/site.yml ${argc_args+"${argc_args[@]}"} 50 | } 51 | 52 | # @cmd Verify that all services are running 53 | # 54 | # Example: see ansible usage 55 | # argc ansible -- --help 56 | # @arg args* Arguments to ansible 57 | verify() { 58 | exec ansible-playbook -u ubuntu -i master.node.consul, "${ansible_variables[@]}" ansible/playbooks/verify.yml ${argc_args+"${argc_args[@]}"} 59 | } 60 | 61 | # @cmd Print environment variables to use services 62 | env() { 63 | if ! ping -c 1 master.node.consul >/dev/null 2>&1; then 64 | echo "This script requires that WireGuard is connected and DNS is configured." >&2 65 | exit 1 66 | fi 67 | export VAULT_ADDR=https://vault.service.consul:8200/ 68 | VAULT_TOKEN=$(ssh master.node.consul -- sudo cat /root/.vault-token) 69 | export VAULT_TOKEN 70 | echo "export VAULT_ADDR=$VAULT_ADDR" 71 | echo "export VAULT_TOKEN=$VAULT_TOKEN" 72 | cat <<-EOF 73 | export CONSUL_HTTP_ADDR=consul.service.consul:8500 74 | export CONSUL_HTTP_TOKEN=$(vault read -field=token kv/cluster/consul) 75 | export NOMAD_ADDR=https://nomad.service.consul:4646/ 76 | export NOMAD_TOKEN=$(vault read -field=token kv/cluster/nomad) 77 | EOF 78 | } 79 | 80 | # @cmd Ensure that all nomad jobs are up to date 81 | # 82 | # Example: preview infrastructure changes 83 | # argc nomad plan 84 | # 85 | # Example: apply infrastructure changes 86 | # argc nomad apply 87 | # 88 | # Example: see terraform usage 89 | # argc nomad -- --help 90 | # 91 | # @arg subcommand Terraform subcommand 92 | # @arg args~ Additional arguments to terraform 93 | nomad() { 94 | exec terraform -chdir=nomad "${argc_subcommand:-}" ${argc_args+"${argc_args[@]}"} 95 | } 96 | 97 | 98 | # @cmd Add Consul DNS to system settings 99 | setup-dns() { 100 | exec sudo scutil <<-EOF 101 | d.init 102 | d.add ServerAddresses * 172.30.0.1 103 | d.add SupplementalMatchDomains * consul 104 | set State:/Network/Service/Consul/DNS 105 | EOF 106 | } 107 | 108 | # @cmd Complete integration test in staging environment 109 | integration-test() { 110 | argc terraform -e test-control apply -- -auto-approve 111 | # Give some time for ssh to come up. Doesn't slow us down because 112 | # cloud-init takes time to finish anyways. 113 | sleep 10 114 | ANSIBLE_HOST_KEY_CHECKING=False argc ansible -e test-control 115 | } 116 | 117 | if ! command -v argc >/dev/null; then 118 | echo "This command requires argc. Install from https://github.com/sigoden/argc" >&2 119 | exit 100 120 | fi 121 | eval "$(argc --argc-eval "$0" "$@")" 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Personal Cloud IaC 2 | 3 | This is the repo I use for [my personal cloud server](https://cgamesplay.com/post/2021/10/27/creating-my-personal-cloud-with-hashicorp/), hosted as a VPS. At the time of writing this document, the system is a single node, therefore the security model is somewhat simpler than a production system and no container storage interfaces need to be configured. 4 | 5 | **Features:** 6 | 7 | - Single-node [Nomad](https://www.nomadproject.io), [Consul](https://www.consul.io), and [Vault](https://www.vaultproject.io) installation. 8 | - mTLS configured between all services. 9 | - [WireGuard](https://www.wireguard.com) used to secure communication between nodes (theoretically, since there's only one node). 10 | - Access to GUI management portals via WireGuard (presently, the real use of WireGuard). 11 | - A [variety of Nomad jobs](./nomad) that I've developed. Some highlights: 12 | - [backup](./nomad/backup) - back up the system to S3 using [Restic](https://restic.net) on a periodic basis. 13 | - [registry](./nomad/registry) - host a private [Docker](https://www.docker.com/) registry, which can be referenced by other Nomad jobs. 14 | - [traefik](./nomad/traefik) - expose Nomad jobs to the internet using [Traefik](https://traefik.io/traefik/) and [LetsEncrypt](https://letsencrypt.org). 15 | - See the full list [here](./nomad). 16 | 17 | ## System components 18 | 19 | **Terraform** 20 | 21 | [Terraform](https://www.terraform.io) is used to describe the very simple infrastructure requirements for the cluster. This is primarily intended to be a base for future improvements, if the cluster ever needs to move to a multi-node setup. 22 | 23 | **Ansible** 24 | 25 | [Ansible](https://www.ansible.com) is used to update all configuration files on all nodes. This includes the configuration for Vault, Vault Agent, Consul, Nomad, and WireGuard. 26 | 27 | **Nomad** 28 | 29 | The master node runs the Nomad server, and all other nodes run Nomad clients. Nomad is responsible for running Traefik and all of the actual workloads. Nomad needs a way to know which machines are Nomad clients, and what workloads they are running, for which it uses Consul. 30 | 31 | **Consul** 32 | 33 | Consul is used to store configuration and state information about the cluster. Each Nomad workload will register as a service in Consul, which in turn can be used to resolve the IP addresses and port information to reach those services from anywhere in the cluster. 34 | 35 | **Vault** 36 | 37 | Vault is used to store secrets and issue internal TLS certificates. It is not directly required by Nomad, but Nomad does have a tight Vault integration to allow workloads to securely receive secrets. Vault stores its data in Consul (:warning: in a real production system we would want to use a separate Consul cluster specifically to store Vault data). Vault Agent is a component of Vault which is used to update configuration files which contain secret data, and is used to rotate TLS certificates as well as to manage the encryption keys and tokens used by the other services. 38 | 39 | **WireGuard** 40 | 41 | WireGuard is used to secure communications between cluster nodes. This allows us to securely keep a private network even between multiple regions and cloud providers. 42 | 43 | ## Installation 44 | 45 | 1. Configure your environment. The requirements: 46 | - `HCLOUD_TOKEN` is the Hetzner Cloud token. 47 | - `ssh-add -L` needs to show at least one key (it will be used as the key for the created instances). 48 | - `python --version` needs to be 3. `apt install python-is-python3` on Ubuntu. 49 | - `pip3 install ansible hvac ansible-modules-hashivault` 50 | 2. Run `argc infra apply` to sync the infrastructure. This command requires confirmation before continuing, but you can also use `plan` or any other Terraform arguments. 51 | 3. Run `argc ansible` to apply the configuration. The change detection does not work correctly on the first run, so `-CD` cannot be used here. They will work after a run has completed at least once. 52 | - The Vault creation will drop `vault.txt` in the repository root, which contains the Vault unseal keys and root token. Store these safely and delete the file. 53 | - Optionally, `argc verify` can be used to diagnose some basic issues now and in the future. 54 | 4. Connect to the machine using ssh (use `argc master_ip` for the IP address) and follow the [WireGuard docs](./docs/wireguard.md) to set up the initial peer. 55 | 5. Deploy jobs with Nomad. Use `argc nomad apply`. 56 | - This will apply the terraform workspace in the `nomad` directory. 57 | 58 | ### Local environment setup 59 | 60 | To access the cluster from your local machine: 61 | 62 | 1. Install the generated CA at `bootstrap/data/ca.crt` to configure SSL. You can install it using Keychain Access.app into the login keychain, but you will need to manually trust the certificate, which is done from the certificate info window under "Trust". 63 | - This also enables UI access in the browser: [Consul](https://172.30.0.1:8501/) | [Vault](https://172.30.0.1:8200/) | [Nomad](https://172.30.0.1:4646/) 64 | 2. Configure WireGuard and connect it. 65 | 3. Set up local DNS to use Consul. 66 | 4. Use `eval $(argc env)` to get the necessary environment variables. 67 | 68 | ### Accessing Consul DNS over Wireguard 69 | 70 | The generated WireGuard configuration does not specify DNS servers for the tunnel. If you want to resolve `service.consul` addresses through the tunnel, you need to either route all DNS through the tunnel, or configure your machine to only route the desired queries through the tunnel. 71 | 72 | **macOS** 73 | 74 | You can configure your macOS system DNS to use the tunnel for the `.consul` TLD only using this snippet. [This StackExchange answer](https://apple.stackexchange.com/a/385218/14873) has more information and debugging tips. 75 | 76 | ```bash 77 | sudo scutil <config </dev/null 2>&1; then 54 | vault write kv/cluster/consul_config gossip_key=$(consul keygen) 55 | else 56 | echo "Nothing to do" 57 | fi 58 | register: prepare_consul_secrets 59 | changed_when: "'Nothing to do' not in prepare_consul_secrets.stdout" 60 | 61 | - name: consul encryption 62 | blockinfile: 63 | path: /etc/vault-agent.d/vault-agent.hcl 64 | marker: "# {mark} CONSUL GOSSIP CONFIG" 65 | block: | 66 | template { 67 | destination = "/etc/consul.d/gossip.hcl" 68 | perms = "0640" 69 | error_on_missing_key = true 70 | contents = < /usr/local/share/ca-certificates/vault.global.crt 15 | update-ca-certificates 16 | vault secrets tune -max-lease-ttl=8760h pki 17 | vault write pki/config/urls \ 18 | issuing_certificates="https://vault.consul.service:8200/v1/pki/ca" \ 19 | crl_distribution_points="https://vault.consul.service:8200/v1/pki/crl" 20 | vault write pki/roles/server-${DC} \ 21 | allowed_domains=server.${DC}.consul,server.${DC}.nomad,server.${DC}.vault,service.consul,${BASE_DOMAIN} \ 22 | allow_bare_domains=true \ 23 | allow_subdomains=true \ 24 | generate_lease=true \ 25 | max_ttl=1440h 26 | 27 | # Set up the authentication for vault-agent 28 | vault policy write vault-agent - </dev/null 2>&1; then 8 | echo "Printing existing configuration for $peer" 9 | else 10 | num_peers=$(vault list -format=json wireguard/groups/default | jq -r '. | length') 11 | next_ip={{ wireguard_ip | splitext | first }}.$((num_peers + 1)) 12 | echo "Allocating $next_ip for $peer" 13 | 14 | vault write wireguard/groups/default/$peer allowed_ips=$next_ip/32 15 | fi 16 | vault write -force wireguard/groups/default/$peer "$@" 17 | service vault-agent restart 18 | 19 | template=$(mktemp) 20 | result=$(mktemp) 21 | cat >>$template < global.vault.crt 39 | vault write auth/cert/certs/server-${DC} \ 40 | name=server-${DC} \ 41 | certificate=@global.vault.crt \ 42 | allowed_common_names=server.${DC}.vault 43 | ``` 44 | 45 | To get the new root certificate where it's desired, you can use curl: 46 | 47 | ```bash 48 | VAULT_ADDR=https://127.0.0.1:8200 49 | curl -sS --insecure $VAULT_ADDR/v1/pki/ca/pem > /usr/local/share/ca-certificates/global.vault.crt 50 | update-ca-certificates 51 | ``` 52 | 53 | As of Nomad v1.2.2, it seems to require a full service restart to detect the new CA certificate. -------------------------------------------------------------------------------- /docs/wireguard.md: -------------------------------------------------------------------------------- 1 | # Wireguard 2 | 3 | ## Runbook 4 | 5 | ### Adding a new client 6 | 7 | The WireGuard playbook installs a script called `wg-add-peer` to the master node. This script will add a new peer or print the configuration for an existing one. 8 | 9 | Usage: `wg-add-peer peername` 10 | 11 | This script automatically assigns an IP to the node and generates keys, then outputs the configuration as well as a QR code. 12 | 13 | Note that to actually use most of the services, you will have to add the CA certificate to the peer's trust root. This can be downloaded from . On iPhone, you will need to follow the following procedure: 14 | 15 | 1. [Download the certificate](https://vault.service.consul:8200/v1/pki/ca/pem). 16 | 2. In Files, manually rename it to `ca.crt` then tap to install. 17 | 3. In Settings, install it using the "Profile Downloaded" option in the main menu. 18 | 4. Trust it in Settings > General > About > Certificate Trust Settings. 19 | 5. Verify it worked by visiting [Nomad](https://nomad.service.consul:4646/). 20 | 21 | -------------------------------------------------------------------------------- /nomad/.gitignore: -------------------------------------------------------------------------------- 1 | ca.crt 2 | -------------------------------------------------------------------------------- /nomad/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/local" { 5 | version = "2.4.0" 6 | constraints = "2.4.0" 7 | hashes = [ 8 | "h1:ZUEYUmm2t4vxwzxy1BvN1wL6SDWrDxfH7pxtzX8c6d0=", 9 | "zh:53604cd29cb92538668fe09565c739358dc53ca56f9f11312b9d7de81e48fab9", 10 | "zh:66a46e9c508716a1c98efbf793092f03d50049fa4a83cd6b2251e9a06aca2acf", 11 | "zh:70a6f6a852dd83768d0778ce9817d81d4b3f073fab8fa570bff92dcb0824f732", 12 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 13 | "zh:82a803f2f484c8b766e2e9c32343e9c89b91997b9f8d2697f9f3837f62926b35", 14 | "zh:9708a4e40d6cc4b8afd1352e5186e6e1502f6ae599867c120967aebe9d90ed04", 15 | "zh:973f65ce0d67c585f4ec250c1e634c9b22d9c4288b484ee2a871d7fa1e317406", 16 | "zh:c8fa0f98f9316e4cfef082aa9b785ba16e36ff754d6aba8b456dab9500e671c6", 17 | "zh:cfa5342a5f5188b20db246c73ac823918c189468e1382cb3c48a9c0c08fc5bf7", 18 | "zh:e0e2b477c7e899c63b06b38cd8684a893d834d6d0b5e9b033cedc06dd7ffe9e2", 19 | "zh:f62d7d05ea1ee566f732505200ab38d94315a4add27947a60afa29860822d3fc", 20 | "zh:fa7ce69dde358e172bd719014ad637634bbdabc49363104f4fca759b4b73f2ce", 21 | ] 22 | } 23 | 24 | provider "registry.terraform.io/hashicorp/nomad" { 25 | version = "2.0.0" 26 | constraints = "2.0.0" 27 | hashes = [ 28 | "h1:t5Su9weDo4qtIMQ+dTnfB9Nv/MnjzeCiOOJ3z7ys4as=", 29 | "zh:09b897d64db293f9a904a4a0849b11ec1e3fff5c638f734d82ae36d8dc044b72", 30 | "zh:435cc106799290f64078ec24b6c59cb32b33784d609088638ed32c6d12121199", 31 | "zh:7073444bd064e8c4ec115ca7d9d7f030cc56795c0a83c27f6668bba519e6849a", 32 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 33 | "zh:79d238c35d650d2d83a439716182da63f3b2767e72e4cbd0b69cb13d9b1aebfc", 34 | "zh:7ef5f49344278fe0bbc5447424e6aa5425ff1821d010d944a444d7fa2c751acf", 35 | "zh:92179091638c8ba03feef371c4361a790190f9955caea1fa59de2055c701a251", 36 | "zh:a8a34398851761368eb8e7c171f24e55efa6e9fdbb5c455f6dec34dc17f631bc", 37 | "zh:b38fd5338625ebace5a4a94cea1a28b11bd91995d834e318f47587cfaf6ec599", 38 | "zh:b71b273a2aca7ad5f1e07c767b25b5a888881ba9ca93b30044ccc39c2937f03c", 39 | "zh:cd14357e520e0f09fb25badfb4f2ee37d7741afdc3ed47c7bcf54c1683772543", 40 | "zh:e05e025f4bb95138c3c8a75c636e97cd7cfd2fc1525b0c8bd097db8c5f02df6e", 41 | ] 42 | } 43 | 44 | provider "registry.terraform.io/hashicorp/vault" { 45 | version = "3.20.1" 46 | constraints = "3.20.1" 47 | hashes = [ 48 | "h1:PEhptXPSrIoezSkCiKsPShibWrcOH/xErqRKuQUPLIA=", 49 | "zh:28623d9254a8869c5717cef8f4ad35f13688d40194c5e2f44c0f49161edf77da", 50 | "zh:3358d6ec273632c13ffd2c0f0588ca6eb6e82a786c194f3863dcfb57712d3455", 51 | "zh:3eaca2f2198ebbd4a0860800ce63a507cec54e6bc82a89b0cb679e01bb848a95", 52 | "zh:415c6e20fb0a1a330380af72a692e1a89923bc5ca7721fbd57e5289435938def", 53 | "zh:76ce29be387abb4c47a664d8af011bb9ff8a9877487ce7edf56311082bcf4603", 54 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 55 | "zh:8724f824ad93004d5aabf54f2971d5a6326c7827cca1a4177befafbbc816f570", 56 | "zh:c1d02a2f8e40a01ecfd16e36b5b1bbbabd7dcdc553d1122c07f59ca6f7844ab4", 57 | "zh:c718d9973344a9c1306760118717f81f68a6c7235e7d3e6c8597fdffdcb94f0d", 58 | "zh:d423cfdce7bc331b70aef6dc61861afa3b96cb423eedd8af0dc3b3f948248bac", 59 | "zh:e8d3abdf6ee23d51c361407b8d7e858edfa60c468614da8b26a66f64d3c75312", 60 | "zh:eec1cd1614bd2b49f4a00dccdf5cc2ac2875fbc53f59d4e628e4abbf0e62ea53", 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /nomad/backup/README.md: -------------------------------------------------------------------------------- 1 | # Automated backups with Restic 2 | 3 | This job is designed to back up a single-node cluster to a remote [Restic](https://restic.net) repository. 4 | 5 | ## Installation 6 | 7 | The single node needs to have restic installed on it manually: 8 | 9 | ```bash 10 | apt install restic 11 | ``` 12 | 13 | The single node also needs to have the `raw_exec` task driver enabled, which is done through the node's config in `/etc/nomad.d/client.hcl`: 14 | 15 | ```hcl 16 | plugin "raw_exec" { 17 | config { 18 | enabled = true 19 | } 20 | } 21 | ``` 22 | 23 | You need to prepare the restic repository that you will be using. I am using an S3 bucket for this, so my process looks like this. You should probably create a limited IAM user who only has access to the configured bucket; see below for a sample IAM policy to use. 24 | 25 | ```bash 26 | export AWS_ACCESS_KEY_ID=... 27 | export AWS_SECRET_ACCESS_KEY=... 28 | export RESTIC_REPOSITORY=s3:s3.amazonaws.com/$S3_BUCKET_NAME 29 | export RESTIC_PASSWORD=$(openssl rand -base64 32) 30 | restic init 31 | ``` 32 | 33 | Store the secret in Vault: 34 | 35 | ```bash 36 | vault secrets enable -version=1 kv 37 | vault kv put kv/backup/repository \ 38 | aws_access_key_id=$AWS_ACCESS_KEY_ID \ 39 | aws_secret_access_key=$AWS_SECRET_ACCESS_KEY \ 40 | restic_repository=$RESTIC_REPOSITORY \ 41 | restic_password=$RESTIC_PASSWORD 42 | ``` 43 | 44 | **Important:** you should save the RESTIC_PASSWORD in the same place that you store your Vault unseal key, because you will need both of them to recover from a server failure. 45 | 46 | ### Sample IAM Policy 47 | 48 | ```json 49 | { 50 | "Version": "2012-10-17", 51 | "Statement": [ 52 | { 53 | "Sid": "VisualEditor0", 54 | "Effect": "Allow", 55 | "Action": [ 56 | "s3:PutObject", 57 | "s3:GetObject", 58 | "s3:ListBucket", 59 | "s3:DeleteObject", 60 | "s3:GetBucketLocation" 61 | ], 62 | "Resource": [ 63 | "arn:aws:s3:::MY_BUCKET/*", 64 | "arn:aws:s3:::MY_BUCKET" 65 | ] 66 | } 67 | ] 68 | } 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /nomad/backup/main.tf: -------------------------------------------------------------------------------- 1 | resource "vault_policy" "backup" { 2 | name = "backup-cluster" 3 | policy = <<-EOT 4 | path "kv/backup/repository" { 5 | capabilities = ["read"] 6 | } 7 | EOT 8 | } 9 | 10 | resource "nomad_job" "backup" { 11 | jobspec = file("${path.module}/nomad.hcl") 12 | } 13 | -------------------------------------------------------------------------------- /nomad/backup/nomad.hcl: -------------------------------------------------------------------------------- 1 | job "backup" { 2 | datacenters = ["nbg1"] 3 | type = "batch" 4 | priority = 90 5 | 6 | periodic { 7 | // Run once a month at midnight in the morning of the first of the month 8 | cron = "@monthly" 9 | } 10 | 11 | group "main" { 12 | restart { 13 | delay = "1h" 14 | } 15 | 16 | ephemeral_disk { 17 | sticky = true 18 | migrate = true 19 | } 20 | 21 | task "restic" { 22 | driver = "raw_exec" 23 | config { 24 | command = "bash" 25 | args = ["secrets/do-backup.sh"] 26 | } 27 | 28 | vault { 29 | policies = ["backup-cluster"] 30 | } 31 | 32 | template { 33 | destination = "secrets/env" 34 | env = true 35 | data = <<-EOF 36 | {{ with secret "kv/backup/repository" }} 37 | AWS_ACCESS_KEY_ID={{ .Data.aws_access_key_id }} 38 | AWS_SECRET_ACCESS_KEY={{ .Data.aws_secret_access_key }} 39 | RESTIC_REPOSITORY={{ .Data.restic_repository }} 40 | RESTIC_PASSWORD={{ .Data.restic_password }} 41 | RESTIC_CACHE_DIR={{ env "NOMAD_ALLOC_DIR" }} 42 | {{ end }} 43 | EOF 44 | } 45 | 46 | template { 47 | destination = "local/excludes.txt" 48 | data = <<-EOF 49 | # Ignore allocation ephemeral storage 50 | /opt/nomad/alloc 51 | EOF 52 | } 53 | 54 | template { 55 | destination = "secrets/do-backup.sh" 56 | perms = "755" 57 | data = <<-EOF 58 | #!/bin/bash 59 | exec 2>&1 60 | set -uexo pipefail 61 | restic version 62 | wg-quick save /etc/wireguard/wg0.conf 63 | restic backup --verbose /opt /etc /home/ubuntu --exclude-file=local/excludes.txt --exclude-caches -o s3.storage-class=STANDARD_IA 64 | # This is pressently commented out because it has no effect yet. 65 | # Ideally, restic 0.12 will be installed when uncommenting it, since 66 | # that has substantial prune performance improvements. 67 | # restic forget --keep-monthly=12 --prune 68 | EOF 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /nomad/dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Dashboard 2 | 3 | This simple job publishes a page on the base domain which contains links to Nomad, Consul, and Vault. The page is publicly accessible, but the admin interfaces are not. This job also serves as an example for publishing a static website. -------------------------------------------------------------------------------- /nomad/dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Own Cluster 7 | 8 | 9 | 10 |
11 |
12 |

13 | My Own Cluster 14 |

15 | 54 |
55 |
56 |
57 |
58 |

In order to access these dashboards, you need to be connected to the WireGuard VPN, have configured the system DNS, and added the Vault certificate authority to your browser's trusted store.

59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /nomad/dashboard/main.tf: -------------------------------------------------------------------------------- 1 | data "local_file" "index_html" { 2 | filename = "${path.module}/index.html" 3 | } 4 | 5 | variable "base_domain" { 6 | type = string 7 | } 8 | 9 | resource "nomad_job" "dashboard" { 10 | jobspec = templatefile("${path.module}/nomad.hcl.tpl", { 11 | base_domain = var.base_domain 12 | index_html = data.local_file.index_html.content 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /nomad/dashboard/nomad.hcl.tpl: -------------------------------------------------------------------------------- 1 | job "dashboard" { 2 | datacenters = ["nbg1"] 3 | type = "service" 4 | 5 | update { 6 | canary = 1 7 | auto_promote = true 8 | } 9 | 10 | group "main" { 11 | network { 12 | mode = "bridge" 13 | port "http" { 14 | to = 8080 15 | } 16 | } 17 | 18 | service { 19 | name = NOMAD_JOB_NAME 20 | port = "http" 21 | 22 | tags = [ 23 | "traefik.enable=true", 24 | "traefik.http.routers.$${NOMAD_JOB_NAME}.tls.certresolver=le", 25 | "traefik.http.routers.$${NOMAD_JOB_NAME}.rule=Host(`${base_domain}`)", 26 | ] 27 | 28 | check { 29 | type = "http" 30 | path = "/" 31 | interval = "30s" 32 | timeout = "2s" 33 | } 34 | } 35 | 36 | task "server" { 37 | driver = "docker" 38 | 39 | config { 40 | image = "halverneus/static-file-server" 41 | ports = ["http"] 42 | 43 | volumes = [ 44 | "local/site:/web" 45 | ] 46 | } 47 | 48 | template { 49 | change_mode = "noop" 50 | destination = "$${NOMAD_TASK_DIR}/site/index.html" 51 | data = <<-EOF 52 | ${index_html} 53 | EOF 54 | } 55 | 56 | template { 57 | change_mode = "noop" 58 | destination = "$${NOMAD_TASK_DIR}/site/ca.crt" 59 | data = <<-EOF 60 | {{- with secret "pki/cert/ca" -}} 61 | {{- .Data.certificate -}} 62 | {{- end -}} 63 | EOF 64 | } 65 | 66 | resources { 67 | memory = 10 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /nomad/joplin/README.md: -------------------------------------------------------------------------------- 1 | # Joplin Server 2 | 3 | Joplin Server is the native sync solution for [Joplin](https://joplinapp.org). Joplin natively support WebDAV sync engines like Seafile, and even S3, but according to the developer this native server is able to sync much faster, particularly on mobile clients. 4 | 5 | ## Installation 6 | 7 | After submitting the job to Nomad, open the page and log in using the default username (`admin@localhost`) and password (`admin`). Change these to something else, then configure Joplin to sync. -------------------------------------------------------------------------------- /nomad/joplin/joplin.nomad: -------------------------------------------------------------------------------- 1 | [[/* This is jobspec should be rendered with levant. */]] 2 | job "joplin" { 3 | datacenters = ["nbg1"] 4 | type = "service" 5 | priority = 60 6 | 7 | group "main" { 8 | network { 9 | mode = "bridge" 10 | port "http" { 11 | to = 22300 12 | } 13 | } 14 | 15 | service { 16 | name = "${NOMAD_JOB_NAME}" 17 | port = "http" 18 | 19 | tags = [ 20 | "traefik.enable=true", 21 | "traefik.http.routers.${NOMAD_JOB_NAME}.tls.certresolver=le", 22 | ] 23 | 24 | check { 25 | type = "http" 26 | path = "/api/ping" 27 | interval = "30s" 28 | timeout = "2s" 29 | header { 30 | Host = ["joplin.[[ consulKey "traefik/config/domain" ]]"] 31 | } 32 | } 33 | } 34 | 35 | task "server" { 36 | driver = "docker" 37 | config { 38 | image = "joplin/server:2.7.3-beta" 39 | ports = ["http"] 40 | } 41 | 42 | template { 43 | destination = "secrets/env" 44 | env = true 45 | data = <<-EOF 46 | DB_CLIENT=pg 47 | APP_BASE_URL=https://{{ env "NOMAD_JOB_NAME" }}.{{ key "traefik/config/domain" }} 48 | EOF 49 | } 50 | 51 | resources { 52 | memory = 128 53 | } 54 | } 55 | 56 | task "postgres" { 57 | driver = "docker" 58 | config { 59 | image = "postgres:13-alpine" 60 | 61 | volumes = [ 62 | "/opt/joplin/postgres:/var/lib/postgresql/data", 63 | ] 64 | } 65 | 66 | env { 67 | POSTGRES_USER = "joplin" 68 | POSTGRES_PASSWORD = "joplin" 69 | POSTGRES_DB = "joplin" 70 | } 71 | 72 | resources { 73 | memory = 32 74 | } 75 | 76 | lifecycle { 77 | hook = "prestart" 78 | sidecar = true 79 | } 80 | } 81 | } 82 | } 83 | 84 | 85 | -------------------------------------------------------------------------------- /nomad/joplin/tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "{{ job_name }}" 3 | nomad_job: 4 | name: "{{ job_name }}" 5 | job_file: "{{ job_dir }}/{{ job_name }}.nomad" 6 | state: absent 7 | ignore_errors: yes 8 | -------------------------------------------------------------------------------- /nomad/jupyter/README.md: -------------------------------------------------------------------------------- 1 | # Jupyter Lab 2 | 3 | This provides a [Jupyter](https://jupyter.org) environment, configured for a single user. 4 | 5 | ## Installation 6 | 7 | Create an empty configuration in Vault: 8 | 9 | ```bash 10 | vault secrets enable -version=1 kv 11 | vault kv put kv/jupyter/config password_hash="" 12 | ``` 13 | 14 | Start the job. You could extract the token from the allocation logs, but to set a password, log into the instance: 15 | 16 | ```bash 17 | nomad alloc exec -i -t -task server $ALLOCATION_ID /bin/bash 18 | jupyter server password --config=$(pwd)/temp.json 19 | cat temp.json 20 | exit 21 | ``` 22 | 23 | Copy the entire "password" value and assign it to the previously-created Vault key: 24 | 25 | ```bash 26 | # Make sure to use single quotes to properly escape the string 27 | PASSWORD_HASH='argon2:$argon2id$v=19$m=10240,t=10,p=8$C+Srb6yq+TBQL6CcjaAehA$SylbTqzEA6HKc5vE4UpXmDGwyEYsyLlv6jDkcOsaw+4' 28 | vault kv put kv/jupyter/config password_hash=$PASSWORD_HASH 29 | ``` 30 | 31 | Finally, stop the old jupyter job and deploy again to update the template with the password. 32 | -------------------------------------------------------------------------------- /nomad/jupyter/jupyter.nomad: -------------------------------------------------------------------------------- 1 | job "jupyter" { 2 | datacenters = ["nbg1"] 3 | type = "service" 4 | priority = 50 5 | 6 | group "main" { 7 | network { 8 | mode = "bridge" 9 | port "http" { 10 | to = 8888 11 | } 12 | } 13 | 14 | service { 15 | name = "${NOMAD_JOB_NAME}" 16 | port = "http" 17 | 18 | tags = [ 19 | "traefik.enable=true", 20 | "traefik.http.routers.${NOMAD_JOB_NAME}.tls.certresolver=le", 21 | ] 22 | 23 | check { 24 | type = "http" 25 | path = "/" 26 | interval = "30s" 27 | timeout = "2s" 28 | } 29 | } 30 | 31 | task "server" { 32 | driver = "docker" 33 | config { 34 | image = "jupyter/tensorflow-notebook" 35 | ports = ["http"] 36 | 37 | volumes = [ 38 | "/opt/jupyter/data:/home/jovyan/work", 39 | "secrets/jupyter_server_config.json:/home/jovyan/.jupyter/jupyter_server_config.json" 40 | ] 41 | } 42 | 43 | vault { 44 | policies = ["jupyter"] 45 | } 46 | 47 | template { 48 | destination = "secrets/jupyter_server_config.json" 49 | # cull_idle_timeout - Kill idle kernels after 48 hours 50 | data = <<-EOF 51 | {{ with secret "kv/jupyter/config" }} 52 | { 53 | "ServerApp": { 54 | "password": "{{ .Data.password_hash }}", 55 | "quit_button": false, 56 | "notebook_dir": "work" 57 | }, 58 | "MappingKernelManager": { 59 | "cull_idle_timeout": 172800 60 | } 61 | } 62 | {{ end }} 63 | EOF 64 | } 65 | 66 | resources { 67 | memory = 500 68 | } 69 | } 70 | } 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /nomad/jupyter/tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "{{ job_name }} vault policy" 3 | hashivault_policy: 4 | name: jupyter 5 | rules: | 6 | path "kv/jupyter/config" { 7 | capabilities = ["read"] 8 | } 9 | 10 | - name: "{{ job_name }}" 11 | nomad_job: 12 | name: "{{ job_name }}" 13 | job_file: "{{ job_dir }}/{{ job_name }}.nomad" 14 | state: present 15 | ignore_errors: yes 16 | -------------------------------------------------------------------------------- /nomad/librechat/README.md: -------------------------------------------------------------------------------- 1 | # LibreChat 2 | 3 | LibreChat is an open-source alternative to ChatGPT that supports multiple LLM providers. 4 | 5 | ## Deployment 6 | 7 | This service includes: 8 | - LibreChat API 9 | - MongoDB database 10 | 11 | ## Configuration 12 | 13 | ``` 14 | vault kv put kv/librechat/env \ 15 | OPENAI_API_KEY=$OPENAI_API_KEY \ 16 | ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ 17 | GOOGLE_SEARCH_API_KEY=$GOOGLE_SEARCH_API_KEY \ 18 | GOOGLE_CSE_ID=$GOOGLE_CSE_ID \ 19 | TAVILY_API_KEY=$TAVILY_API_KEY \ 20 | CREDS_KEY=$(openssl rand -hex 32) \ 21 | CREDS_IV=$(openssl rand -hex 16) \ 22 | JWT_SECRET=$(openssl rand -base64 32) \ 23 | JWT_REFRESH_SECRET=$(openssl rand -base64 32) 24 | ``` 25 | -------------------------------------------------------------------------------- /nomad/librechat/main.tf: -------------------------------------------------------------------------------- 1 | resource "vault_policy" "librechat" { 2 | name = "librechat" 3 | policy = <<-EOT 4 | path "kv/librechat/env" { 5 | capabilities = ["read"] 6 | } 7 | EOT 8 | } 9 | 10 | resource "nomad_job" "librechat" { 11 | jobspec = file("${path.module}/nomad.hcl") 12 | } -------------------------------------------------------------------------------- /nomad/librechat/nomad.hcl: -------------------------------------------------------------------------------- 1 | variable "api_image_tag" { 2 | description = "Docker tag to use for LibreChat API" 3 | default = "latest" 4 | } 5 | 6 | job "librechat" { 7 | datacenters = ["nbg1"] 8 | type = "service" 9 | priority = 50 10 | 11 | group "main" { 12 | network { 13 | mode = "bridge" 14 | port "http" { 15 | to = 3080 16 | } 17 | } 18 | 19 | service { 20 | name = NOMAD_JOB_NAME 21 | port = "http" 22 | 23 | tags = [ 24 | "traefik.enable=true", 25 | "traefik.http.routers.${NOMAD_JOB_NAME}.tls.certresolver=le", 26 | ] 27 | 28 | check { 29 | type = "http" 30 | path = "/" 31 | interval = "30s" 32 | timeout = "10s" 33 | } 34 | } 35 | 36 | task "api" { 37 | driver = "docker" 38 | config { 39 | image = "ghcr.io/danny-avila/librechat-dev-api:${var.api_image_tag}" 40 | ports = ["http"] 41 | 42 | volumes = [ 43 | "/opt/librechat/config:/app/librechat.yaml", 44 | "/opt/librechat/images:/app/client/public/images", 45 | "/opt/librechat/uploads:/app/uploads", 46 | "/opt/librechat/logs:/app/api/logs" 47 | ] 48 | } 49 | 50 | vault { 51 | policies = ["librechat"] 52 | } 53 | 54 | template { 55 | destination = "secrets/env" 56 | env = true 57 | data = <<-EOF 58 | HOST=0.0.0.0 59 | NODE_ENV=production 60 | PORT=3080 61 | SEARCH=false 62 | ALLOW_EMAIL_LOGIN=true 63 | ALLOW_REGISTRATION=false 64 | {{ with secret "kv/librechat/env" }} 65 | OPENAI_API_KEY={{ .Data.OPENAI_API_KEY }} 66 | ANTHROPIC_API_KEY={{ .Data.ANTHROPIC_API_KEY }} 67 | GOOGLE_SEARCH_API_KEY={{ .Data.GOOGLE_SEARCH_API_KEY }} 68 | GOOGLE_CSE_ID={{ .Data.GOOGLE_CSE_ID }} 69 | TAVILY_API_KEY={{ .Data.TAVILY_API_KEY }} 70 | CREDS_KEY={{ .Data.CREDS_KEY }} 71 | CREDS_IV={{ .Data.CREDS_IV }} 72 | JWT_SECRET={{ .Data.JWT_SECRET }} 73 | JWT_REFRESH_SECRET={{ .Data.JWT_REFRESH_SECRET }} 74 | {{ end }} 75 | # MongoDB connection 76 | MONGO_URI=mongodb://127.0.0.1:27017/LibreChat 77 | EOF 78 | } 79 | 80 | resources { 81 | memory = 196 82 | memory_max = 256 83 | } 84 | } 85 | 86 | task "mongodb" { 87 | driver = "docker" 88 | config { 89 | image = "mongo" 90 | command = "mongod" 91 | args = ["--noauth"] 92 | volumes = [ 93 | "/opt/librechat/mongodb:/data/db" 94 | ] 95 | } 96 | 97 | resources { 98 | memory = 256 99 | memory_max = 384 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /nomad/lobechat/README.md: -------------------------------------------------------------------------------- 1 | # LobeChat 2 | 3 | [LobeChat](https://github.com/lobehub/lobe-chat) is a self-hosted frontend for LLMs. 4 | 5 | ## Installation 6 | 7 | Set up the necessary environment variables in Vault: 8 | 9 | ```bash 10 | vault secrets enable -version=1 kv 11 | vault kv put kv/lobechat/env \ 12 | OPENAI_API_KEY=$OPENAI_API_KEY \ 13 | ACCESS_CODE=$ACCESS_CODE 14 | ``` 15 | -------------------------------------------------------------------------------- /nomad/lobechat/main.tf: -------------------------------------------------------------------------------- 1 | resource "vault_policy" "lobechat" { 2 | name = "lobechat" 3 | policy = <<-EOT 4 | path "kv/lobechat/env" { 5 | capabilities = ["read"] 6 | } 7 | EOT 8 | } 9 | 10 | resource "nomad_job" "lobechat" { 11 | jobspec = file("${path.module}/nomad.hcl") 12 | } 13 | -------------------------------------------------------------------------------- /nomad/lobechat/nomad.hcl: -------------------------------------------------------------------------------- 1 | variable "image_tag" { 2 | description = "Docker tag to use for lobehub/lobe-chat" 3 | default = "v1.34.0" 4 | } 5 | 6 | job "lobechat" { 7 | datacenters = ["nbg1"] 8 | type = "service" 9 | priority = 50 10 | 11 | group "main" { 12 | network { 13 | mode = "bridge" 14 | port "http" { 15 | to = 3210 16 | } 17 | } 18 | 19 | service { 20 | name = NOMAD_JOB_NAME 21 | port = "http" 22 | 23 | tags = [ 24 | "traefik.enable=true", 25 | "traefik.http.routers.$${NOMAD_JOB_NAME}.tls.certresolver=le", 26 | ] 27 | 28 | check { 29 | type = "http" 30 | path = "/" 31 | interval = "30s" 32 | timeout = "2s" 33 | } 34 | } 35 | 36 | task "server" { 37 | driver = "docker" 38 | config { 39 | image = "lobehub/lobe-chat:${var.image_tag}" 40 | ports = ["http"] 41 | } 42 | 43 | vault { 44 | policies = ["lobechat"] 45 | } 46 | 47 | template { 48 | destination = "secrets/env" 49 | env = true 50 | data = <<-EOF 51 | {{ with secret "kv/lobechat/env" }} 52 | OPENAI_API_KEY={{ .Data.OPENAI_API_KEY }} 53 | ANTHROPIC_API_KEY={{ .Data.ANTHROPIC_API_KEY }} 54 | ACCESS_CODE={{ .Data.ACCESS_CODE }} 55 | {{ end }} 56 | 57 | OPENAI_MODEL_LIST=-gpt-4-turbo 58 | ENABLED_OLLAMA=0 59 | EOF 60 | } 61 | 62 | resources { 63 | memory = 64 64 | memory_max = 256 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /nomad/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | cloud { 3 | organization = "cgamesplay" 4 | 5 | workspaces { 6 | name = "nomad" 7 | } 8 | } 9 | 10 | 11 | required_providers { 12 | local = { 13 | source = "hashicorp/local" 14 | version = "2.4.0" 15 | } 16 | nomad = { 17 | source = "hashicorp/nomad" 18 | version = "2.0.0" 19 | } 20 | vault = { 21 | source = "hashicorp/vault" 22 | version = "3.20.1" 23 | } 24 | } 25 | 26 | required_version = ">= 0.14.9" 27 | } 28 | 29 | provider "nomad" { 30 | ca_file = "./ca.crt" 31 | } 32 | 33 | provider "vault" { 34 | ca_cert_file = "./ca.crt" 35 | } 36 | 37 | variable "datacenter" { 38 | type = string 39 | description = "internal name of the target data center" 40 | default = "nbg1" 41 | nullable = false 42 | } 43 | 44 | variable "base_domain" { 45 | type = string 46 | description = "main domain name for traefik rules" 47 | nullable = false 48 | default = "example.com" 49 | } 50 | 51 | resource "nomad_scheduler_config" "config" { 52 | scheduler_algorithm = "binpack" 53 | memory_oversubscription_enabled = true 54 | preemption_config = { 55 | batch_scheduler_enabled = false 56 | service_scheduler_enabled = false 57 | sysbatch_scheduler_enabled = false 58 | system_scheduler_enabled = false 59 | } 60 | } 61 | 62 | module "backup" { 63 | source = "./backup" 64 | } 65 | 66 | module "dashboard" { 67 | source = "./dashboard" 68 | base_domain = var.base_domain 69 | } 70 | 71 | module "lobechat" { 72 | source = "./lobechat" 73 | count = 0 74 | } 75 | 76 | module "open-webui" { 77 | source = "./open-webui" 78 | } 79 | 80 | module "librechat" { 81 | source = "./librechat" 82 | } 83 | 84 | module "seafile" { 85 | source = "./seafile" 86 | } 87 | 88 | module "traefik" { 89 | source = "./traefik" 90 | base_domain = var.base_domain 91 | } 92 | 93 | module "whoami" { 94 | source = "./whoami" 95 | count = 0 96 | } 97 | -------------------------------------------------------------------------------- /nomad/nextcloud/README.md: -------------------------------------------------------------------------------- 1 | # NextCloud 2 | 3 | I very briefly evaluated using NextCloud, but ended up opting against it in favor of Seafile. This code remains here in case I ever reconsider. 4 | 5 | **Why?** Nextcloud seems like a bloated security nightmare. It seems to want to keep all of its application code inside of the persistent volume, and keep its data directory in a subdirectory of that. Additionally, the default install comes with a ton of unnecessary features. The security features it does have are irrelevant to a setup behind a load balancer, but cannot be disabled. -------------------------------------------------------------------------------- /nomad/nextcloud/nextcloud.nomad: -------------------------------------------------------------------------------- 1 | [[/* This is jobspec should be rendered with levant. */]] 2 | job "nextcloud" { 3 | datacenters = ["nbg1"] 4 | type = "service" 5 | 6 | group "main" { 7 | network { 8 | mode = "bridge" 9 | port "http" { 10 | to = "80" 11 | } 12 | } 13 | 14 | service { 15 | name = "${NOMAD_JOB_NAME}" 16 | port = "http" 17 | 18 | tags = [ 19 | "traefik.enable=true", 20 | "traefik.http.routers.${NOMAD_JOB_NAME}.tls.certresolver=le", 21 | ] 22 | 23 | check { 24 | type = "http" 25 | path = "/" 26 | interval = "30s" 27 | timeout = "2s" 28 | } 29 | } 30 | 31 | task "nextcloud" { 32 | driver = "docker" 33 | config { 34 | image = "nextcloud:21-fpm-alpine" 35 | ports = ["api"] 36 | 37 | volumes = [ 38 | "../alloc/html:/var/www/html" 39 | ] 40 | } 41 | 42 | template { 43 | destination = "secrets/env" 44 | env = true 45 | data = <<-EOF 46 | SQLITE_DATABASE=nextcloud # This means: /var/www/html/data/nextcloud.db 47 | TRUSTED_PROXIES={{ env "NOMAD_HOST_IP_http" }} 48 | NEXTCLOUD_ADMIN_USER=admin 49 | NEXTCLOUD_ADMIN_PASSWORD=password 50 | # The IP address is required so the Consul service checks don't get a 51 | # 400 error when hitting the endpoint. 52 | NEXTCLOUD_TRUSTED_DOMAINS={{ env "NOMAD_JOB_NAME" }}.{{ key "traefik/config/domain" }} {{ env "NOMAD_HOST_IP_http" }} 53 | EOF 54 | } 55 | } 56 | 57 | task "nginx" { 58 | driver = "docker" 59 | config { 60 | image = "nginx:alpine" 61 | 62 | volumes = [ 63 | "local/nginx.conf:/etc/nginx/nginx.conf", 64 | "../alloc/html:/var/www/html:ro" 65 | ] 66 | } 67 | 68 | template { 69 | destination = "local/nginx.conf" 70 | data = <<-EOF 71 | [[fileContents "nginx.conf"]] 72 | EOF 73 | } 74 | } 75 | 76 | task "cron" { 77 | driver = "docker" 78 | config { 79 | image = "nextcloud:21-fpm-alpine" 80 | entrypoint = ["/cron.sh"] 81 | 82 | volumes = [ 83 | "../alloc/html:/var/www/html" 84 | ] 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /nomad/nextcloud/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | 12 | http { 13 | include /etc/nginx/mime.types; 14 | default_type application/octet-stream; 15 | 16 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 17 | '$status $body_bytes_sent "$http_referer" ' 18 | '"$http_user_agent" "$http_x_forwarded_for"'; 19 | 20 | access_log /var/log/nginx/access.log main; 21 | 22 | sendfile on; 23 | #tcp_nopush on; 24 | 25 | keepalive_timeout 65; 26 | 27 | #gzip on; 28 | 29 | upstream php-handler { 30 | server 127.0.0.1:9000; 31 | } 32 | 33 | server { 34 | listen 80; 35 | 36 | # Add headers to serve security related headers 37 | # Before enabling Strict-Transport-Security headers please read into this 38 | # topic first. 39 | #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always; 40 | # 41 | # WARNING: Only add the preload option once you read about 42 | # the consequences in https://hstspreload.org/. This option 43 | # will add the domain to a hardcoded list that is shipped 44 | # in all major browsers and getting removed from this list 45 | # could take several months. 46 | add_header Referrer-Policy "no-referrer" always; 47 | add_header X-Content-Type-Options "nosniff" always; 48 | add_header X-Download-Options "noopen" always; 49 | add_header X-Frame-Options "SAMEORIGIN" always; 50 | add_header X-Permitted-Cross-Domain-Policies "none" always; 51 | add_header X-Robots-Tag "none" always; 52 | add_header X-XSS-Protection "1; mode=block" always; 53 | 54 | # Remove X-Powered-By, which is an information leak 55 | fastcgi_hide_header X-Powered-By; 56 | 57 | # Path to the root of your installation 58 | root /var/www/html; 59 | 60 | location = /robots.txt { 61 | allow all; 62 | log_not_found off; 63 | access_log off; 64 | } 65 | 66 | # The following 2 rules are only needed for the user_webfinger app. 67 | # Uncomment it if you're planning to use this app. 68 | #rewrite ^/.well-known/host-meta /public.php?service=host-meta last; 69 | #rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last; 70 | 71 | # The following rule is only needed for the Social app. 72 | # Uncomment it if you're planning to use this app. 73 | #rewrite ^/.well-known/webfinger /public.php?service=webfinger last; 74 | 75 | location = /.well-known/carddav { 76 | return 301 $scheme://$host:$server_port/remote.php/dav; 77 | } 78 | 79 | location = /.well-known/caldav { 80 | return 301 $scheme://$host:$server_port/remote.php/dav; 81 | } 82 | 83 | # set max upload size 84 | client_max_body_size 10G; 85 | fastcgi_buffers 64 4K; 86 | 87 | # Enable gzip but do not remove ETag headers 88 | gzip on; 89 | gzip_vary on; 90 | gzip_comp_level 4; 91 | gzip_min_length 256; 92 | gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; 93 | gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; 94 | 95 | # Uncomment if your server is build with the ngx_pagespeed module 96 | # This module is currently not supported. 97 | #pagespeed off; 98 | 99 | location / { 100 | rewrite ^ /index.php; 101 | } 102 | 103 | location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ { 104 | deny all; 105 | } 106 | location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) { 107 | deny all; 108 | } 109 | 110 | location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) { 111 | fastcgi_split_path_info ^(.+?\.php)(\/.*|)$; 112 | set $path_info $fastcgi_path_info; 113 | try_files $fastcgi_script_name =404; 114 | include fastcgi_params; 115 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 116 | fastcgi_param PATH_INFO $path_info; 117 | # fastcgi_param HTTPS on; 118 | 119 | # Avoid sending the security headers twice 120 | fastcgi_param modHeadersAvailable true; 121 | 122 | # Enable pretty urls 123 | fastcgi_param front_controller_active true; 124 | fastcgi_pass php-handler; 125 | fastcgi_intercept_errors on; 126 | fastcgi_request_buffering off; 127 | } 128 | 129 | location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) { 130 | try_files $uri/ =404; 131 | index index.php; 132 | } 133 | 134 | # Adding the cache control header for js, css and map files 135 | # Make sure it is BELOW the PHP block 136 | location ~ \.(?:css|js|woff2?|svg|gif|map)$ { 137 | try_files $uri /index.php$request_uri; 138 | add_header Cache-Control "public, max-age=15778463"; 139 | # Add headers to serve security related headers (It is intended to 140 | # have those duplicated to the ones above) 141 | # Before enabling Strict-Transport-Security headers please read into 142 | # this topic first. 143 | #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always; 144 | # 145 | # WARNING: Only add the preload option once you read about 146 | # the consequences in https://hstspreload.org/. This option 147 | # will add the domain to a hardcoded list that is shipped 148 | # in all major browsers and getting removed from this list 149 | # could take several months. 150 | add_header Referrer-Policy "no-referrer" always; 151 | add_header X-Content-Type-Options "nosniff" always; 152 | add_header X-Download-Options "noopen" always; 153 | add_header X-Frame-Options "SAMEORIGIN" always; 154 | add_header X-Permitted-Cross-Domain-Policies "none" always; 155 | add_header X-Robots-Tag "none" always; 156 | add_header X-XSS-Protection "1; mode=block" always; 157 | 158 | # Optional: Don't log access to assets 159 | access_log off; 160 | } 161 | 162 | location ~ \.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$ { 163 | try_files $uri /index.php$request_uri; 164 | # Optional: Don't log access to other assets 165 | access_log off; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /nomad/nextcloud/tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "{{ job_name }}" 3 | nomad_job: 4 | name: "{{ job_name }}" 5 | job_file: "{{ job_dir }}/{{ job_name }}.nomad" 6 | state: absent 7 | ignore_errors: yes 8 | -------------------------------------------------------------------------------- /nomad/open-webui/README.md: -------------------------------------------------------------------------------- 1 | # Open WebUI 2 | 3 | [Open WebUI](https://openwebui.com) is a self-hosted frontend for LLMs. 4 | 5 | ## Installation 6 | 7 | Set up the necessary environment variables in Vault: 8 | 9 | ```bash 10 | vault secrets enable -version=1 kv 11 | vault kv put kv/open-webui/env \ 12 | OPENAI_API_KEY=$OPENAI_API_KEY \ 13 | ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ 14 | SERPLY_API_KEY=$SERPLY_API_KEY \ 15 | WEBUI_SECRET_KEY=$(openssl rand 32 | base32) 16 | ``` 17 | -------------------------------------------------------------------------------- /nomad/open-webui/main.tf: -------------------------------------------------------------------------------- 1 | resource "vault_policy" "open-webui" { 2 | name = "open-webui" 3 | policy = <<-EOT 4 | path "kv/open-webui/env" { 5 | capabilities = ["read"] 6 | } 7 | EOT 8 | } 9 | 10 | resource "nomad_job" "open-webui" { 11 | jobspec = file("${path.module}/nomad.hcl") 12 | } 13 | -------------------------------------------------------------------------------- /nomad/open-webui/nomad.hcl: -------------------------------------------------------------------------------- 1 | variable "image_tag" { 2 | description = "Docker tag to use for open-webui/open-webui" 3 | default = "0.5.18" 4 | } 5 | 6 | job "open-webui" { 7 | datacenters = ["nbg1"] 8 | type = "service" 9 | priority = 50 10 | 11 | group "main" { 12 | network { 13 | mode = "bridge" 14 | port "http" { 15 | to = 8080 16 | } 17 | } 18 | 19 | service { 20 | name = NOMAD_JOB_NAME 21 | port = "http" 22 | 23 | tags = [ 24 | "traefik.enable=true", 25 | "traefik.http.routers.$${NOMAD_JOB_NAME}.tls.certresolver=le", 26 | ] 27 | 28 | check { 29 | type = "http" 30 | path = "/health" 31 | interval = "30s" 32 | timeout = "10s" 33 | } 34 | } 35 | 36 | task "server" { 37 | driver = "docker" 38 | config { 39 | image = "ghcr.io/open-webui/open-webui:${var.image_tag}" 40 | ports = ["http"] 41 | 42 | volumes = [ 43 | "/opt/open-webui:/app/backend/data" 44 | ] 45 | } 46 | 47 | vault { 48 | policies = ["open-webui"] 49 | } 50 | 51 | template { 52 | destination = "secrets/env" 53 | env = true 54 | data = <<-EOF 55 | {{ with secret "kv/open-webui/env" }} 56 | WEBUI_SECRET_KEY={{ .Data.WEBUI_SECRET_KEY }} 57 | ANTHROPIC_API_KEY={{ .Data.ANTHROPIC_API_KEY }} 58 | # All of the following are PersistentConfig, so they theoretically 59 | # have no effect on existing containers. 60 | WEBUI_URL=https://${NOMAD_JOB_NAME}.cluster.cgamesplay.com/ 61 | ENABLE_OLLAMA_API=False 62 | OPENAI_API_KEY={{ .Data.OPENAI_API_KEY }} 63 | RAG_EMBEDDING_ENGINE=openai 64 | AUDIO_STT_ENGINE=openai 65 | 66 | ENABLE_RAG_WEB_SEARCH=True 67 | RAG_WEB_SEARCH_ENGINE=google_pse 68 | RAG_WEB_SEARCH_RESULT_COUNT=3 69 | RAG_WEB_SEARCH_CONCURRENT_REQUESTS=10 70 | GOOGLE_PSE_API_KEY={{ .Data.GOOGLE_PSE_API_KEY }} 71 | GOOGLE_PSE_ENGINE_ID={{ .Data.GOOGLE_PSE_ENGINE_ID }} 72 | {{ end }} 73 | EOF 74 | } 75 | 76 | resources { 77 | memory = 512 78 | memory_max = 1024 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /nomad/seafile/README.md: -------------------------------------------------------------------------------- 1 | # Seafile 2 | 3 | [Seafile](https://www.seafile.com/en/home/) is a self-hosted Dropbox alternative. This configuration uses the Pro version of Seafile, which is free for up to 3 users without a license. 4 | 5 | ## Installation 6 | 7 | After starting the container for the first time, you should be able to access the login page, but won't be able to log in. You need to create an admin user account, and make some changes to the configuration. 8 | 9 | ```bash 10 | # Access the container 11 | nomad alloc exec -i -t -task seafile -job seafile /bin/bash 12 | # Create the admin user 13 | /opt/seafile/seafile-server-latest/reset-admin.sh 14 | # The server name of the memcached server is hard-coded, and must be updated 15 | sed -i 's/memcached:11211/localhost:11211/' conf/seahub_settings.py 16 | # This is optional, but it removes about 150MB of RAM usage in the container. 17 | sed -i '/OFFICE CONVERTER/,/^$/s/enabled.*/enabled = false/' conf/seafevents.conf 18 | ``` 19 | 20 | After doing that, restart the seafile task (this is doable from the Nomad UI or using `nomad alloc restart`). Then you can log in normally and set up the instance the way you like. Make sure to do these two things: 21 | 22 | - Set the `SERVICE_URL` and `FILE_SERVER_URL` in the system settings. 23 | - Delete the empty default user. 24 | 25 | -------------------------------------------------------------------------------- /nomad/seafile/main.tf: -------------------------------------------------------------------------------- 1 | resource "nomad_job" "seafile" { 2 | jobspec = file("${path.module}/nomad.hcl") 3 | } 4 | -------------------------------------------------------------------------------- /nomad/seafile/nomad.hcl: -------------------------------------------------------------------------------- 1 | job "seafile" { 2 | datacenters = ["nbg1"] 3 | type = "service" 4 | priority = 60 5 | 6 | group "main" { 7 | network { 8 | mode = "bridge" 9 | port "http" { 10 | to = 80 11 | } 12 | } 13 | 14 | task "seafile" { 15 | leader = true 16 | driver = "docker" 17 | config { 18 | image = "docker.seadrive.org/seafileltd/seafile-pro-mc" 19 | ports = ["http"] 20 | 21 | auth { 22 | username = "seafile" 23 | password = "zjkmid6rQibdZ=uJMuWS" 24 | } 25 | 26 | volumes = [ 27 | "/opt/seafile/seafile:/shared" 28 | ] 29 | } 30 | 31 | resources { 32 | memory = 768 33 | memory_max = 2048 34 | } 35 | 36 | service { 37 | name = NOMAD_JOB_NAME 38 | port = "http" 39 | 40 | tags = [ 41 | "traefik.enable=true", 42 | "traefik.http.routers.${NOMAD_JOB_NAME}.tls.certresolver=le", 43 | ] 44 | 45 | check { 46 | type = "http" 47 | path = "/api2/ping/" 48 | interval = "30s" 49 | timeout = "2s" 50 | failures_before_critical = 3 51 | } 52 | } 53 | } 54 | 55 | task "mariadb" { 56 | driver = "docker" 57 | config { 58 | image = "mariadb:10.5" 59 | args = ["--datadir=/shared/mariadb"] 60 | 61 | volumes = [ 62 | "/opt/seafile/mariadb:/shared/mariadb" 63 | ] 64 | } 65 | 66 | env { 67 | MYSQL_ALLOW_EMPTY_PASSWORD = true 68 | MYSQL_LOG_CONSOLE = true 69 | } 70 | 71 | resources { 72 | memory = 200 73 | } 74 | 75 | lifecycle { 76 | hook = "prestart" 77 | sidecar = true 78 | } 79 | } 80 | 81 | task "memcached" { 82 | driver = "docker" 83 | config { 84 | image = "memcached:1.5.6" 85 | entrypoint = ["memcached", "-m", "60"] 86 | } 87 | 88 | resources { 89 | memory = 64 90 | } 91 | 92 | lifecycle { 93 | hook = "prestart" 94 | sidecar = true 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /nomad/traefik/README.md: -------------------------------------------------------------------------------- 1 | # Traefik 2 | 3 | The Traefik load balancer is the main ingress point for all public-facing services in the cluster. It is responsible for terminating SSL using LetsEncrypt and forwarding connections to the proper services. 4 | 5 | ## Installation 6 | 7 | To set up Traefik to handle SSL for your cluster, first assign a DNS name to the nodes that will run Traefik. Ensure that all subdomains also point to the same IPs. Store in Consul the domain name as well as the contact email address for your SSL certificates: 8 | 9 | ```bash 10 | consul kv put traefik/config/domain example.com 11 | consul kv put traefik/config/email contact@example.com 12 | ``` 13 | 14 | No further configuration is necessary for Traefik. 15 | 16 | ## Usage 17 | 18 | Traefik will scan the Consul catalog for additional services and automatically configure SSL and forwarding for them. To use this, apply these tags: 19 | 20 | ```hcl 21 | service { 22 | tags = [ 23 | "traefik.enable=true", 24 | "traefik.http.routers.${NOMAD_JOB_NAME}.tls.certresolver=le", 25 | ] 26 | } 27 | ``` 28 | 29 | The usage of `${NOMAD_JOB_NAME}` means that the subdomain for the service will default to the job's name. If you want to customize the behavior, see [the Traefik documentation](https://doc.traefik.io/traefik/routing/routers/). 30 | 31 | **Note:** Traefik is configured to automatically redirect all HTTP traffic to the corresponding HTTPS endpoint, regardless of any dynamic configuration. 32 | 33 | ### Debugging 34 | 35 | The first place to look should be the [Traefik dashboard](https://172.30.0.1:8080). This will list all configured services and the rules required to access them. If something isn't listed there, check the [Consul dashboard](https://172.30.0.1:8501) to ensure that the service is properly registered and healthy. 36 | 37 | It may be helpful to enable the DEBUG log level in Traefik, which will cause it to log to stdout every change in configuration. 38 | 39 | ### Tunnel to a WireGuard peer 40 | 41 | It's possible to use Traefik to forward a specific subdomain to a WireGuard 42 | peer, for example a laptop. This serves as a very basic alternative to ngrok. 43 | In this configuration, Traefik will unwrap the SSL and forward the connection 44 | over WireGuard. 45 | 46 | Setting the `traefik/config/tunnel` key in Consul will cause Traefik to forward 47 | the "tunnel" subdomain to that address. 48 | 49 | ```bash 50 | consul kv put traefik/config/tunnel 172.30.15.1:3000 51 | # Later, remove with 52 | consul kv delete traefik/config/tunnel 53 | ``` 54 | -------------------------------------------------------------------------------- /nomad/traefik/main.tf: -------------------------------------------------------------------------------- 1 | variable "base_domain" { 2 | type = string 3 | } 4 | 5 | resource "nomad_job" "traefik" { 6 | jobspec = templatefile("${path.module}/nomad.hcl.tpl", { 7 | base_domain = var.base_domain 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /nomad/traefik/nomad.hcl.tpl: -------------------------------------------------------------------------------- 1 | job "traefik" { 2 | datacenters = ["nbg1"] 3 | type = "service" 4 | priority = 90 5 | 6 | group "main" { 7 | network { 8 | port "http" { 9 | static = 80 10 | } 11 | port "https" { 12 | static = 443 13 | } 14 | port "dashboard" { 15 | static = 8080 16 | } 17 | } 18 | 19 | restart { 20 | mode = "delay" 21 | } 22 | 23 | ephemeral_disk { 24 | sticky = true 25 | migrate = true 26 | } 27 | 28 | service { 29 | name = NOMAD_JOB_NAME 30 | port = "https" 31 | 32 | check { 33 | type = "tcp" 34 | port = "http" 35 | interval = "30s" 36 | timeout = "2s" 37 | } 38 | } 39 | 40 | task "traefik" { 41 | driver = "docker" 42 | 43 | config { 44 | image = "traefik:v2.11.11" 45 | network_mode = "host" 46 | 47 | volumes = [ 48 | "local/ca.crt:/etc/traefik/ca.crt", 49 | "local/traefik.toml:/etc/traefik/traefik.toml", 50 | "local/acme:/etc/traefik/acme", 51 | "local/static.toml:/etc/traefik/static.toml" 52 | ] 53 | } 54 | 55 | template { 56 | destination = "local/traefik.toml" 57 | data = <<-EOF 58 | [entryPoints.http] 59 | address = ":{{env "NOMAD_PORT_http"}}" 60 | [entryPoints.http.http.redirections.entryPoint] 61 | to = "https" 62 | scheme = "https" 63 | 64 | [entryPoints.https] 65 | address = ":{{env "NOMAD_PORT_https"}}" 66 | 67 | [entryPoints.traefik] 68 | address = "172.30.0.1:{{env "NOMAD_PORT_dashboard"}}" 69 | 70 | [certificatesResolvers.le.acme] 71 | email = "{{ key "traefik/config/email" }}" 72 | storage = "/etc/traefik/acme/acme.json" 73 | preferredChain = "ISRG Root X1" 74 | [certificatesResolvers.le.acme.tlsChallenge] 75 | 76 | [api] 77 | # Expose the API directly. Note that we bind to the Wireguard IP directly for 78 | # the traefik entryPoint. 79 | insecure = true 80 | 81 | # Enable some hard-coded configuration options 82 | [providers.file] 83 | filename = "/etc/traefik/static.toml" 84 | watch = false 85 | 86 | # Enable Consul Catalog configuration backend. 87 | [providers.consulCatalog] 88 | exposedByDefault = false 89 | defaultRule = "Host(`{{`{{normalize .Name}}`}}.${base_domain}`)" 90 | 91 | [providers.consulCatalog.endpoint] 92 | address = "127.0.0.1:8501" 93 | scheme = "https" 94 | [providers.consulCatalog.endpoint.tls] 95 | ca = "/etc/traefik/ca.crt" 96 | 97 | [log] 98 | #level = "DEBUG" 99 | 100 | [accesslog] 101 | EOF 102 | } 103 | 104 | template { 105 | destination = "local/ca.crt" 106 | data = <<-EOF 107 | {{ with secret "pki/cert/ca"}} 108 | {{ .Data.certificate }} 109 | {{ end }} 110 | EOF 111 | } 112 | 113 | template { 114 | destination = "local/static.toml" 115 | data = <<-EOF 116 | [tcp.routers.nomad] 117 | service = "nomad" 118 | rule = "HostSNI(`nomad.${base_domain}`)" 119 | tls.passthrough = true 120 | 121 | [[tcp.services.nomad.loadBalancer.servers]] 122 | address = "127.0.0.1:4646" 123 | 124 | [tcp.routers.vault] 125 | service = "vault" 126 | rule = "HostSNI(`vault.${base_domain}`)" 127 | tls.passthrough = true 128 | 129 | [[tcp.services.vault.loadBalancer.servers]] 130 | address = "127.0.0.1:8200" 131 | 132 | {{ if keyExists "traefik/config/tunnel" }} 133 | [http.routers.tunnel] 134 | entryPoints = [ "https" ] 135 | service = "tunnel" 136 | rule = "Host(`tunnel.${base_domain}`)" 137 | tls.certresolver = "le" 138 | 139 | [[http.services.tunnel.loadBalancer.servers]] 140 | url = "http://{{ key "traefik/config/tunnel" }}/" 141 | {{ end }} 142 | EOF 143 | } 144 | 145 | resources { 146 | memory = 64 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /nomad/whoami/README.md: -------------------------------------------------------------------------------- 1 | # Whoami Service 2 | 3 | This service is the simplest test case of a web service. It does not require any configuration, and is the ideal test case for Traefik. 4 | -------------------------------------------------------------------------------- /nomad/whoami/main.tf: -------------------------------------------------------------------------------- 1 | resource "nomad_job" "whoami" { 2 | jobspec = file("${path.module}/nomad.hcl") 3 | } 4 | -------------------------------------------------------------------------------- /nomad/whoami/nomad.hcl: -------------------------------------------------------------------------------- 1 | job "whoami" { 2 | datacenters = ["nbg1"] 3 | type = "service" 4 | 5 | update { 6 | canary = 1 7 | auto_promote = true 8 | } 9 | 10 | group "main" { 11 | network { 12 | mode = "bridge" 13 | port "http" { 14 | to = 80 15 | } 16 | } 17 | 18 | service { 19 | name = NOMAD_JOB_NAME 20 | port = "http" 21 | 22 | tags = [ 23 | "traefik.enable=true", 24 | "traefik.http.routers.${NOMAD_JOB_NAME}.tls.certresolver=le", 25 | ] 26 | 27 | check { 28 | type = "http" 29 | path = "/" 30 | interval = "30s" 31 | timeout = "2s" 32 | } 33 | } 34 | 35 | task "server" { 36 | driver = "docker" 37 | 38 | config { 39 | image = "containous/whoami" 40 | ports = ["http"] 41 | } 42 | 43 | resources { 44 | memory = 10 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "ansible" 5 | version = "8.4.0" 6 | description = "Radically simple IT automation" 7 | optional = false 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "ansible-8.4.0-py3-none-any.whl", hash = "sha256:d601d89a4306934e7c0aae05195fd72c0719287fde165982d0ebac282b4280f1"}, 11 | {file = "ansible-8.4.0.tar.gz", hash = "sha256:f33c492690592fad12684e9897f6de2da15c9f6e1ecb79137703a06470af2ce6"}, 12 | ] 13 | 14 | [package.dependencies] 15 | ansible-core = ">=2.15.4,<2.16.0" 16 | 17 | [[package]] 18 | name = "ansible-core" 19 | version = "2.15.4" 20 | description = "Radically simple IT automation" 21 | optional = false 22 | python-versions = ">=3.9" 23 | files = [ 24 | {file = "ansible-core-2.15.4.tar.gz", hash = "sha256:c1a8aaede985f79e5932ba2163639379f7d8025bfd9b28378db1649a4ef541ed"}, 25 | {file = "ansible_core-2.15.4-py3-none-any.whl", hash = "sha256:5c57089405406f3004e948127b518b65509e280d524f61f91cc6360303fc388b"}, 26 | ] 27 | 28 | [package.dependencies] 29 | cryptography = "*" 30 | jinja2 = ">=3.0.0" 31 | packaging = "*" 32 | PyYAML = ">=5.1" 33 | resolvelib = ">=0.5.3,<1.1.0" 34 | 35 | [[package]] 36 | name = "ansible-modules-hashivault" 37 | version = "4.7.1" 38 | description = "Ansible Modules for Hashicorp Vault" 39 | optional = false 40 | python-versions = "*" 41 | files = [ 42 | {file = "ansible-modules-hashivault-4.7.1.tar.gz", hash = "sha256:83d29bb7fd49ef2eb1e9a00865bdfe49a30845f4bb9b6c2bbfbe64a6f34fa096"}, 43 | ] 44 | 45 | [package.dependencies] 46 | ansible = ">=4.0.0" 47 | hvac = ">=0.11.2,<1" 48 | requests = "*" 49 | 50 | [[package]] 51 | name = "certifi" 52 | version = "2023.7.22" 53 | description = "Python package for providing Mozilla's CA Bundle." 54 | optional = false 55 | python-versions = ">=3.6" 56 | files = [ 57 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 58 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 59 | ] 60 | 61 | [[package]] 62 | name = "cffi" 63 | version = "1.16.0" 64 | description = "Foreign Function Interface for Python calling C code." 65 | optional = false 66 | python-versions = ">=3.8" 67 | files = [ 68 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 69 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 70 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 71 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 72 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 73 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 74 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 75 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 76 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 77 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 78 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 79 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 80 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 81 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 82 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 83 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 84 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 85 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 86 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 87 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 88 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 89 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 90 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 91 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 92 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 93 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 94 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 95 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 96 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 97 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 98 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 99 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 100 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 101 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 102 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 103 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 104 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 105 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 106 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 107 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 108 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 109 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 110 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 111 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 112 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 113 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 114 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 115 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 116 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 117 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 118 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 119 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 120 | ] 121 | 122 | [package.dependencies] 123 | pycparser = "*" 124 | 125 | [[package]] 126 | name = "charset-normalizer" 127 | version = "3.3.0" 128 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 129 | optional = false 130 | python-versions = ">=3.7.0" 131 | files = [ 132 | {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, 133 | {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, 134 | {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, 135 | {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, 136 | {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, 137 | {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, 138 | {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, 139 | {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, 140 | {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, 141 | {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, 142 | {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, 143 | {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, 144 | {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, 145 | {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, 146 | {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, 147 | {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, 148 | {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, 149 | {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, 150 | {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, 151 | {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, 152 | {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, 153 | {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, 154 | {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, 155 | {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, 156 | {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, 157 | {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, 158 | {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, 159 | {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, 160 | {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, 161 | {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, 162 | {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, 163 | {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, 164 | {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, 165 | {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, 166 | {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, 167 | {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, 168 | {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, 169 | {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, 170 | {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, 171 | {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, 172 | {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, 173 | {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, 174 | {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, 175 | {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, 176 | {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, 177 | {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, 178 | {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, 179 | {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, 180 | {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, 181 | {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, 182 | {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, 183 | {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, 184 | {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, 185 | {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, 186 | {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, 187 | {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, 188 | {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, 189 | {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, 190 | {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, 191 | {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, 192 | {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, 193 | {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, 194 | {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, 195 | {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, 196 | {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, 197 | {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, 198 | {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, 199 | {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, 200 | {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, 201 | {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, 202 | {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, 203 | {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, 204 | {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, 205 | {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, 206 | {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, 207 | {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, 208 | {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, 209 | {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, 210 | {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, 211 | {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, 212 | {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, 213 | {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, 214 | {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, 215 | {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, 216 | {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, 217 | {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, 218 | {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, 219 | {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, 220 | {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, 221 | {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, 222 | ] 223 | 224 | [[package]] 225 | name = "cryptography" 226 | version = "41.0.4" 227 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 228 | optional = false 229 | python-versions = ">=3.7" 230 | files = [ 231 | {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, 232 | {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, 233 | {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, 234 | {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, 235 | {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, 236 | {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, 237 | {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, 238 | {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, 239 | {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, 240 | {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, 241 | {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, 242 | {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, 243 | {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, 244 | {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, 245 | {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, 246 | {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, 247 | {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, 248 | {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, 249 | {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, 250 | {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, 251 | {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, 252 | {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, 253 | {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, 254 | ] 255 | 256 | [package.dependencies] 257 | cffi = ">=1.12" 258 | 259 | [package.extras] 260 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 261 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 262 | nox = ["nox"] 263 | pep8test = ["black", "check-sdist", "mypy", "ruff"] 264 | sdist = ["build"] 265 | ssh = ["bcrypt (>=3.1.5)"] 266 | test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 267 | test-randomorder = ["pytest-randomly"] 268 | 269 | [[package]] 270 | name = "hvac" 271 | version = "0.11.2" 272 | description = "HashiCorp Vault API client" 273 | optional = false 274 | python-versions = ">=2.7" 275 | files = [ 276 | {file = "hvac-0.11.2-py2.py3-none-any.whl", hash = "sha256:3e8a34804b1e20954a2b4991cc13ed9c09b32e50dadd9d3438224481150f6568"}, 277 | {file = "hvac-0.11.2.tar.gz", hash = "sha256:f905c59d32d88d3f67571fe5a8a78de4659e04798ad809de439f667247d13626"}, 278 | ] 279 | 280 | [package.dependencies] 281 | requests = ">=2.21.0" 282 | six = ">=1.5.0" 283 | 284 | [package.extras] 285 | parser = ["pyhcl (>=0.3.10)"] 286 | 287 | [[package]] 288 | name = "idna" 289 | version = "3.4" 290 | description = "Internationalized Domain Names in Applications (IDNA)" 291 | optional = false 292 | python-versions = ">=3.5" 293 | files = [ 294 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 295 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 296 | ] 297 | 298 | [[package]] 299 | name = "jinja2" 300 | version = "3.1.2" 301 | description = "A very fast and expressive template engine." 302 | optional = false 303 | python-versions = ">=3.7" 304 | files = [ 305 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 306 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 307 | ] 308 | 309 | [package.dependencies] 310 | MarkupSafe = ">=2.0" 311 | 312 | [package.extras] 313 | i18n = ["Babel (>=2.7)"] 314 | 315 | [[package]] 316 | name = "markupsafe" 317 | version = "2.1.3" 318 | description = "Safely add untrusted strings to HTML/XML markup." 319 | optional = false 320 | python-versions = ">=3.7" 321 | files = [ 322 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, 323 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, 324 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, 325 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, 326 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, 327 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, 328 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, 329 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, 330 | {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, 331 | {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, 332 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, 333 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, 334 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, 335 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, 336 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, 337 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, 338 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, 339 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, 340 | {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, 341 | {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, 342 | {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, 343 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, 344 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, 345 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, 346 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, 347 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, 348 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, 349 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, 350 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, 351 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, 352 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, 353 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, 354 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, 355 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, 356 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, 357 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, 358 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, 359 | {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, 360 | {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, 361 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, 362 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, 363 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, 364 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, 365 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, 366 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, 367 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, 368 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, 369 | {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, 370 | {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, 371 | {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, 372 | ] 373 | 374 | [[package]] 375 | name = "packaging" 376 | version = "23.1" 377 | description = "Core utilities for Python packages" 378 | optional = false 379 | python-versions = ">=3.7" 380 | files = [ 381 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 382 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 383 | ] 384 | 385 | [[package]] 386 | name = "pycparser" 387 | version = "2.21" 388 | description = "C parser in Python" 389 | optional = false 390 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 391 | files = [ 392 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 393 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 394 | ] 395 | 396 | [[package]] 397 | name = "pyyaml" 398 | version = "6.0.1" 399 | description = "YAML parser and emitter for Python" 400 | optional = false 401 | python-versions = ">=3.6" 402 | files = [ 403 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 404 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 405 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 406 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 407 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 408 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 409 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 410 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 411 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 412 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 413 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 414 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 415 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 416 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 417 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 418 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 419 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 420 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 421 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 422 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 423 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 424 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 425 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 426 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 427 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 428 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 429 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 430 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 431 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 432 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 433 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 434 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 435 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 436 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 437 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 438 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 439 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 440 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 441 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 442 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 443 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 444 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 445 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 446 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 447 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 448 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 449 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 450 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 451 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 452 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 453 | ] 454 | 455 | [[package]] 456 | name = "requests" 457 | version = "2.31.0" 458 | description = "Python HTTP for Humans." 459 | optional = false 460 | python-versions = ">=3.7" 461 | files = [ 462 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 463 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 464 | ] 465 | 466 | [package.dependencies] 467 | certifi = ">=2017.4.17" 468 | charset-normalizer = ">=2,<4" 469 | idna = ">=2.5,<4" 470 | urllib3 = ">=1.21.1,<3" 471 | 472 | [package.extras] 473 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 474 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 475 | 476 | [[package]] 477 | name = "resolvelib" 478 | version = "1.0.1" 479 | description = "Resolve abstract dependencies into concrete ones" 480 | optional = false 481 | python-versions = "*" 482 | files = [ 483 | {file = "resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf"}, 484 | {file = "resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309"}, 485 | ] 486 | 487 | [package.extras] 488 | examples = ["html5lib", "packaging", "pygraphviz", "requests"] 489 | lint = ["black", "flake8", "isort", "mypy", "types-requests"] 490 | release = ["build", "towncrier", "twine"] 491 | test = ["commentjson", "packaging", "pytest"] 492 | 493 | [[package]] 494 | name = "six" 495 | version = "1.16.0" 496 | description = "Python 2 and 3 compatibility utilities" 497 | optional = false 498 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 499 | files = [ 500 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 501 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 502 | ] 503 | 504 | [[package]] 505 | name = "urllib3" 506 | version = "2.0.5" 507 | description = "HTTP library with thread-safe connection pooling, file post, and more." 508 | optional = false 509 | python-versions = ">=3.7" 510 | files = [ 511 | {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, 512 | {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, 513 | ] 514 | 515 | [package.extras] 516 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 517 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 518 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 519 | zstd = ["zstandard (>=0.18.0)"] 520 | 521 | [metadata] 522 | lock-version = "2.0" 523 | python-versions = "^3.11" 524 | content-hash = "cf577a5979ecf3bdc537e95be65515773a83cbdce8cbeb46ce6dd485ad395109" 525 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "infra" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Your Name "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | PyYAML = "^6.0.1" 11 | requests = "^2.31.0" 12 | ansible = "^8.4.0" 13 | ansible-modules-hashivault = "^4.7.0" 14 | 15 | 16 | [build-system] 17 | requires = ["poetry-core"] 18 | build-backend = "poetry.core.masonry.api" 19 | -------------------------------------------------------------------------------- /share/Deploy.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # This is a gitlab-ci configuration file that will build a Docker image and 2 | # deploy it to the cluster. 3 | # 4 | # This template is from: 5 | # https://gitlab.com/CGamesPlay/infra/-/blob/master/share/Deploy.gitlab-ci.yml 6 | 7 | build-image: 8 | image: docker:latest 9 | stage: build 10 | services: 11 | - docker:dind 12 | interruptible: true 13 | before_script: 14 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 15 | - docker buildx create --use --bootstrap 16 | script: 17 | # Images are tagged with the commit short sha, and pushes to the default 18 | # branch are additionally tagged with "latest". 19 | - | 20 | tags="-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" 21 | if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then 22 | tags="$tags -t $CI_REGISTRY_IMAGE:latest" 23 | fi 24 | # Provenance is disabled because gitlab doesn't support it. We use a 25 | # separate image to store the cache layers. 26 | - docker buildx build 27 | --cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/cache 28 | --cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/cache,mode=max 29 | --provenance false 30 | $tags --push . 31 | 32 | # To use the deploy job, create a new SecretID and store it in a protected 33 | # variable named VAULT_SECRET_ID. 34 | # 35 | # To create a new SecretID: 36 | # vault write -f auth/approle/role/deploy/secret-id 37 | deploy: 38 | stage: deploy 39 | image: registry.gitlab.com/cgamesplay/infra/deploy:latest 40 | variables: 41 | VAULT_ROLE_ID: 133cccb6-eb2f-5674-f0fc-cb7f755384cb 42 | script: 43 | - set -euo pipefail 44 | - export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id="$VAULT_ROLE_ID" secret_id="$VAULT_SECRET_ID") || exit $? 45 | - export NOMAD_TOKEN=$(vault read -field=secret_id nomad/creds/deploy) || exit $? 46 | - nomad job run -var tag=$CI_COMMIT_SHORT_SHA myjob.nomad 47 | # Environment configuration is just for reference in gitlab, it doesn't 48 | # affect anything in the job itself. 49 | environment: 50 | name: production 51 | url: https://myjob.cluster.cgamesplay.com/ 52 | rules: 53 | # Automatically deploy on pushes to the main branch. 54 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 55 | # ALternatively, you can use a manual job. 56 | # - when: manual 57 | -------------------------------------------------------------------------------- /share/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM alpine:latest 3 | ENV VAULT_ADDR=https://vault.cluster.cgamesplay.com/ 4 | ENV NOMAD_ADDR=https://nomad.cluster.cgamesplay.com/ 5 | 6 | ADD ca.crt /usr/local/share/vault.crt 7 | 8 | RUN <> /etc/ssl/certs/ca-certificates.crt 12 | EOF 13 | -------------------------------------------------------------------------------- /share/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDxDCCAqygAwIBAgIUROEYa+HW6Hf4xaJLGZwpaZfOXBYwDQYJKoZIhvcNAQEL 3 | BQAwFzEVMBMGA1UEAxMMZ2xvYmFsLnZhdWx0MB4XDTIyMDUwODE2NTcxMVoXDTMy 4 | MDUwNTE2NTc0MVowFzEVMBMGA1UEAxMMZ2xvYmFsLnZhdWx0MIIBIjANBgkqhkiG 5 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwZDoCPVspnb8r+rOQ95zkZSTftowORrQETVE 6 | ifDAsOTog4wdHxoJ7e/ckzlYwyygO/2lB1akGGeLqCdGzvhvkj/A1GDZxCOpaXD5 7 | eJPUObj5S7ItCF3LTGva01u6AXxEo005hsce4t+KM4xArfZu1bczoZFIaVyWMZH0 8 | 11WpRtx8OVjzB7SPOfhbCJo0JZMAGv55PwYbAhdRUr9A5CQQVLqA9O7GqfJarch/ 9 | lDJZxiBIoFazYFwu3wqIXFVgc2KVB3BKx3hDHBDLlLbeZ73TDH2EoyyV8QgjzT4T 10 | nMZ/r6XY2jDNhFM7NMbxcdhftTSNPz42FAkaY/ALuhpzL0uucwIDAQABo4IBBjCC 11 | AQIwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMH8 12 | l1Zltsp7IM0cQix9M+57waGrMB8GA1UdIwQYMBaAFMH8l1Zltsp7IM0cQix9M+57 13 | waGrMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcwAoYraHR0cHM6Ly92YXVsdC5j 14 | b25zdWwuc2VydmljZTo4MjAwL3YxL3BraS9jYTAXBgNVHREEEDAOggxnbG9iYWwu 15 | dmF1bHQwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cHM6Ly92YXVsdC5jb25zdWwuc2Vy 16 | dmljZTo4MjAwL3YxL3BraS9jcmwwDQYJKoZIhvcNAQELBQADggEBALq89Hmr5GrL 17 | PTa3v2Q9o+t6N8sdR/zCcmpo91KzXVDqwoPDkT84Cr2X1NW4gOMFIWi6iqFiHLce 18 | pRu0bih8S6zuaxK0Ufy5R48En5f7FBV8hOWkSCHe2O9I2BPJXvsgWSzwGDUCwalE 19 | eWcrdH0/ZJvE7TbHfU5ThPpJeVa9VKALXh9ui9dTKkT/QmOgTP1lMiL6cy6qocpP 20 | XdYq2A4AFtytWSZK0ZfySxtH8wUcyc5cpgq91fCXzXhRR2aRTG5m6OwnBcAs1GBI 21 | FPAYdLjNoj5LsL98Zh0Kn97Jv1pH0W5y1qx0Kw/llZNZHXD90hFoUskX/YV3QYSg 22 | cIT+oIj6eoQ= 23 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/external" { 5 | version = "2.2.2" 6 | hashes = [ 7 | "h1:BKQ5f5ijzeyBSnUr+j0wUi+bYv6KBQVQNDXNRVEcfJE=", 8 | "h1:e7RpnZ2PbJEEPnfsg7V0FNwbfSk0/Z3FdrLsXINBmDY=", 9 | "zh:0b84ab0af2e28606e9c0c1289343949339221c3ab126616b831ddb5aaef5f5ca", 10 | "zh:10cf5c9b9524ca2e4302bf02368dc6aac29fb50aeaa6f7758cce9aa36ae87a28", 11 | "zh:56a016ee871c8501acb3f2ee3b51592ad7c3871a1757b098838349b17762ba6b", 12 | "zh:719d6ef39c50e4cffc67aa67d74d195adaf42afcf62beab132dafdb500347d39", 13 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 14 | "zh:7fbfc4d37435ac2f717b0316f872f558f608596b389b895fcb549f118462d327", 15 | "zh:8ac71408204db606ce63fe8f9aeaf1ddc7751d57d586ec421e62d440c402e955", 16 | "zh:a4cacdb06f114454b6ed0033add28006afa3f65a0ea7a43befe45fc82e6809fb", 17 | "zh:bb5ce3132b52ae32b6cc005bc9f7627b95259b9ffe556de4dad60d47d47f21f0", 18 | "zh:bb60d2976f125ffd232a7ccb4b3f81e7109578b23c9c6179f13a11d125dca82a", 19 | "zh:f9540ecd2e056d6e71b9ea5f5a5cf8f63dd5c25394b9db831083a9d4ea99b372", 20 | "zh:ffd998b55b8a64d4335a090b6956b4bf8855b290f7554dd38db3302de9c41809", 21 | ] 22 | } 23 | 24 | provider "registry.terraform.io/hetznercloud/hcloud" { 25 | version = "1.35.1" 26 | constraints = "1.35.1" 27 | hashes = [ 28 | "h1:FgSVN8CkqWt+iHhTYPPVQgoltoO8FGI+quB0PZucfj4=", 29 | "h1:TKBg6bTAbfqxD1RLJ32wQquMjHR9696oCEH7z3cKUqI=", 30 | "zh:055161a3bec0b09db32b2488ac9036e46e7867c3319af182329157a1ff72ca00", 31 | "zh:08f0d5b31dfac682df21a3f193aac93522a05e83e8eca26c547d2baa2858238b", 32 | "zh:16d4c4a194d056947820680a116bf23227d4ee527d33831d7a7df52c5c0c3c4b", 33 | "zh:46b528a76968599e1a6c45d8264b86fe9602070a42fd2d2db32899b5161e44dc", 34 | "zh:502b16a56bb6780b86913ad3f4f573ae3f29f7a3d99335d7fd120c1b607537e8", 35 | "zh:5fa5114d101e9d7c1915b1f136cc2b48a83c9ace7c994545940f11ccabf1f036", 36 | "zh:6ac8ff28f145ef20c595faf81ff9c478be4d469cdd5b7aeaf2feefcc80a3dd36", 37 | "zh:8ced6aec0546784eea6a9e56082af3af5c9917459351ef2951a9742125d4aab9", 38 | "zh:927b0c39de0b368e52c7491859948082aaa84d877f0fed7ef483892c844875bf", 39 | "zh:9d9c0fb5e862e47d24cdb007afad0215ccff9da65cf8a6cfa66030e844f5403c", 40 | "zh:ae5475cae11806a93bb4adb3c87007ce9c0211d16c9c7a87ae5e9d58a68fcc0b", 41 | "zh:d01600e67abc7ce7c59bc8567b7a650bc5ce817723a354f401a803d421610641", 42 | "zh:f3487f1c49145b560fd19c8c681cb9eaaa85fc3700ea9b675f649f5f5d8b1e3c", 43 | "zh:f5257b83287156effecb0f43fe80b6cbcc02c89f35ceda1b845d4e3dcf757dca", 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | cloud { 3 | organization = "cgamesplay" 4 | 5 | workspaces { 6 | tags = ["core"] 7 | } 8 | } 9 | 10 | 11 | required_providers { 12 | hcloud = { 13 | source = "hetznercloud/hcloud" 14 | version = "1.35.1" 15 | } 16 | } 17 | 18 | required_version = ">= 0.14.9" 19 | } 20 | 21 | provider "hcloud" { 22 | } 23 | 24 | variable "datacenter" { 25 | type = string 26 | description = "internal name of the target data center" 27 | default = "nbg1" 28 | nullable = false 29 | } 30 | 31 | variable "public_ssh" { 32 | type = bool 33 | description = "enable SSH via public IP" 34 | default = true 35 | nullable = false 36 | } 37 | 38 | variable "delete_protection" { 39 | type = bool 40 | description = "enable delete protection on important resources" 41 | default = false 42 | nullable = false 43 | } 44 | 45 | data "external" "master_user_data" { 46 | program = ["${path.module}/master_user_data.py"] 47 | 48 | query = { 49 | # arbitrary map from strings to strings, passed 50 | # to the external program as the data query. 51 | } 52 | } 53 | 54 | resource "hcloud_network" "network" { 55 | name = "network" 56 | ip_range = "172.31.0.0/16" 57 | } 58 | 59 | resource "hcloud_network_subnet" "network_subnet" { 60 | type = "cloud" 61 | network_id = hcloud_network.network.id 62 | network_zone = "eu-central" 63 | ip_range = "172.31.0.0/20" 64 | } 65 | 66 | resource "hcloud_firewall" "firewall" { 67 | name = "firewall" 68 | rule { 69 | direction = "in" 70 | protocol = "icmp" 71 | source_ips = ["0.0.0.0/0", "::/0"] 72 | } 73 | dynamic "rule" { 74 | for_each = var.public_ssh ? [1] : [] 75 | content { 76 | direction = "in" 77 | protocol = "tcp" 78 | port = "22" 79 | source_ips = ["0.0.0.0/0", "::/0"] 80 | } 81 | } 82 | rule { 83 | direction = "in" 84 | protocol = "tcp" 85 | port = "80" 86 | source_ips = ["0.0.0.0/0", "::/0"] 87 | } 88 | rule { 89 | direction = "in" 90 | protocol = "tcp" 91 | port = "443" 92 | source_ips = ["0.0.0.0/0", "::/0"] 93 | } 94 | rule { 95 | direction = "in" 96 | protocol = "udp" 97 | port = "51820" 98 | source_ips = ["0.0.0.0/0", "::/0"] 99 | } 100 | } 101 | 102 | resource "hcloud_server" "master" { 103 | name = "master" 104 | image = "ubuntu-20.04" 105 | location = "nbg1" 106 | server_type = "cx21" 107 | user_data = data.external.master_user_data.result.rendered 108 | firewall_ids = [hcloud_firewall.firewall.id] 109 | rebuild_protection = var.delete_protection 110 | delete_protection = var.delete_protection 111 | 112 | network { 113 | network_id = hcloud_network.network.id 114 | ip = "172.31.0.2" 115 | } 116 | 117 | # Note: the depends_on is important when directly attaching the server to a 118 | # network. Otherwise Terraform will attempt to create server and sub-network 119 | # in parallel. This may result in the server creation failing randomly. 120 | depends_on = [ 121 | hcloud_network_subnet.network_subnet 122 | ] 123 | 124 | lifecycle { 125 | ignore_changes = [ssh_keys, user_data, image] 126 | } 127 | } 128 | 129 | resource "hcloud_volume" "master_drive" { 130 | name = "master-drive" 131 | size = 20 132 | server_id = hcloud_server.master.id 133 | automount = false 134 | format = "ext4" 135 | delete_protection = var.delete_protection 136 | } 137 | 138 | output "master_ip" { 139 | description = "Public IP of the master node" 140 | value = hcloud_server.master.ipv4_address 141 | } 142 | -------------------------------------------------------------------------------- /terraform/master_user_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This script generates the user data used to spin up the master instance. It 3 | # is called by Terraform. 4 | 5 | import json 6 | import yaml 7 | import requests 8 | import subprocess 9 | import os 10 | import sys 11 | import gzip 12 | 13 | ssh_keys = subprocess.run(["ssh-add", "-L"], capture_output=True, check=True).stdout.decode('utf-8').splitlines() 14 | 15 | drive_script = """ 16 | set -ex 17 | mkdir -p /opt 18 | echo "UUID=$(lsblk /dev/sdb -no uuid) /opt ext4 discard,defaults,errors=remount-ro 0 2" >> /etc/fstab 19 | mount -a 20 | """ 21 | 22 | user_data = { 23 | 'disable_root': True, 24 | 'user': { 25 | 'name': 'ubuntu', 26 | 'groups': ['adm', 'docker'], 27 | 'ssh_authorized_keys': ssh_keys, 28 | 'sudo': 'ALL=(ALL) NOPASSWD:ALL', 29 | }, 30 | 'users': { 31 | 'name': 'root', 32 | 'lock_passwd': True, 33 | }, 34 | 'ntp': { 35 | 'enabled': True, 36 | }, 37 | 'timezone': 'UTC', 38 | 'package_update': True, 39 | 'packages': ['docker.io', 'wireguard', 'net-tools', 'jq'], 40 | 'runcmd': [ drive_script, 'chage -E -1 -M -1 -d -1 root' ], 41 | } 42 | 43 | result = "#cloud-config\n" + yaml.dump(user_data) 44 | if sys.stdout.isatty(): 45 | print(result, end='') 46 | else: 47 | print(json.dumps({ "rendered": result })) 48 | -------------------------------------------------------------------------------- /terraform/prod-control.tfvars: -------------------------------------------------------------------------------- 1 | public_ssh = false 2 | delete_protection = true 3 | -------------------------------------------------------------------------------- /terraform/test-control.tfvars: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGamesPlay/infra/35120ca5e04795cad60536bc5f91c0c6f89f4d15/terraform/test-control.tfvars --------------------------------------------------------------------------------