Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Drop the Terraform configurations; cloud-init is the deployment path

51624dc663738e70cc37b8ac5e232841fd49aaf8 · Keenan Nemetz · 3d ago · parent e21f6c5e

Drop the Terraform configurations; cloud-init is the deployment path

A Vole instance is one package, one configuration file, and one directory.
The first-boot file that installs the package and writes the configuration
is that same work described once, and it needs no provider, no state file,
and no cloud token with write access to an account. What Terraform did
outside the machine — firewall, address, DNS record — is three fields in a
console the operator already has open.

So the three configurations go, and with them what existed only to check
them: the pinned Terraform download in the verify image, the `fmt -check`
step, the .terraform entries in the ignore file, and the directory skip in
the link checker. The deployment check now needs python3 and PyYAML and
nothing else — no network, no credentials.

deploy/README.md keeps the reasoning, so the next person tempted to add a
provider back sees what it would buy and what it would cost.

The templates' header comments no longer name a renderer that is gone:
render.sh fills them in, and check-deploy.py renders them the same way.

deploy/terraform/digitalocean/main.tf −217

@@ -1,217 +0,0 @@
1 # A Vole instance on DigitalOcean, configured by cloud-init.
2 #
3 # Run it from a clone of the Vole repository, because the machine's user data is
4 # deploy/cloud-init/vole.yaml and is read by its path:
5 #
6 # cd deploy/terraform/digitalocean
7 # terraform init
8 # terraform apply \
9 # -var domain=forge.example.com \
10 # -var "admin_ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
11 # -var vole_package_url=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb
12 #
13 # DIGITALOCEAN_TOKEN must be set. See docs/deploy/digitalocean.md.
14
15 terraform {
16 required_version = ">= 1.9"
17 required_providers {
18 digitalocean = {
19 source = "digitalocean/digitalocean"
20 version = ">= 2.40"
21 }
22 }
23 }
24
25 provider "digitalocean" {}
26
27 locals {
28 labels = ["app:vole"]
29
30 user_data = templatefile(
31 var.cloud_init_file != "" ? var.cloud_init_file : "${path.module}/../../cloud-init/vole.yaml",
32 {
33 hostname = var.domain
34 admin_account = var.admin_account
35 admin_ssh_key = trimspace(var.admin_ssh_public_key)
36 admin_email = var.admin_email
37 instance_name = var.instance_name
38 tls_mode = var.tls_mode
39 vole_package_url = var.vole_package_url
40 }
41 )
42
43 # A runner is a second machine with its own job: it leases work from the
44 # instance, runs each job in a container, and reports the result. It does not
45 # need a name of its own, an address that stays put, or backups.
46 runner_user_data = templatefile(
47 var.runner_cloud_init_file != "" ? var.runner_cloud_init_file : "${path.module}/../../cloud-init/runner.yaml",
48 {
49 runner_hostname = "${var.name}-runner"
50 forge_host = var.domain
51 runner_runtime = "podman"
52 runner_network = var.runner_network
53 runner_dir = "/var/lib/vole-runner"
54 runner_labels = join(" ", [for label in var.runner_labels : "--label ${label}"])
55 vole_package_url = var.vole_package_url
56 }
57 )
58 }
59
60 resource "digitalocean_ssh_key" "admin" {
61 name = "${var.name}-admin"
62 public_key = trimspace(var.admin_ssh_public_key)
63 }
64
65 resource "digitalocean_firewall" "vole" {
66 name = var.name
67 droplet_ids = [digitalocean_droplet.vole.id]
68
69 inbound_rule {
70 protocol = "tcp"
71 port_range = "22"
72 source_addresses = var.ssh_admin_cidrs
73 }
74
75 # Git over SSH. Restricting this to your team's addresses is the cheapest way
76 # to take the write surface off the internet.
77 inbound_rule {
78 protocol = "tcp"
79 port_range = "2222"
80 source_addresses = var.ssh_admin_cidrs
81 }
82
83 inbound_rule {
84 protocol = "tcp"
85 port_range = "80"
86 source_addresses = ["0.0.0.0/0", "::/0"]
87 }
88
89 inbound_rule {
90 protocol = "tcp"
91 port_range = "443"
92 source_addresses = ["0.0.0.0/0", "::/0"]
93 }
94
95 inbound_rule {
96 protocol = "icmp"
97 source_addresses = ["0.0.0.0/0", "::/0"]
98 }
99
100 outbound_rule {
101 protocol = "tcp"
102 port_range = "1-65535"
103 destination_addresses = ["0.0.0.0/0", "::/0"]
104 }
105
106 outbound_rule {
107 protocol = "udp"
108 port_range = "1-65535"
109 destination_addresses = ["0.0.0.0/0", "::/0"]
110 }
111
112 outbound_rule {
113 protocol = "icmp"
114 destination_addresses = ["0.0.0.0/0", "::/0"]
115 }
116 }
117
118 resource "digitalocean_droplet" "vole" {
119 name = var.name
120 image = var.image
121 size = var.size
122 region = var.region
123 ssh_keys = [digitalocean_ssh_key.admin.fingerprint]
124 tags = local.labels
125 ipv6 = true
126
127 user_data = local.user_data
128
129 # Free: the agent reports CPU and disk to DigitalOcean's graphs, which is the
130 # quickest way to see that a build ate the disk.
131 monitoring = true
132
133 # Weekly droplet backups. Not a substitute for a copy you keep elsewhere; see
134 # docs/deploy/README.md.
135 backups = var.enable_backups
136
137 lifecycle {
138 ignore_changes = [user_data]
139 }
140 }
141
142 resource "digitalocean_domain" "this" {
143 count = var.create_dns ? 1 : 0
144
145 name = var.dns_zone
146 }
147
148 resource "digitalocean_record" "forge" {
149 count = var.create_dns ? 1 : 0
150
151 domain = digitalocean_domain.this[0].name
152 type = "A"
153 name = "@"
154 value = digitalocean_droplet.vole.ipv4_address
155 ttl = 300
156 }
157
158 resource "digitalocean_firewall" "runner" {
159 count = var.runner ? 1 : 0
160
161 name = "${var.name}-runner"
162 droplet_ids = [digitalocean_droplet.runner[0].id]
163
164 # Nothing arrives except the operator's own shell. The runner dials out to the
165 # instance and to the registries its images come from.
166 inbound_rule {
167 protocol = "tcp"
168 port_range = "22"
169 source_addresses = var.ssh_admin_cidrs
170 }
171
172 inbound_rule {
173 protocol = "icmp"
174 source_addresses = ["0.0.0.0/0", "::/0"]
175 }
176
177 outbound_rule {
178 protocol = "tcp"
179 port_range = "1-65535"
180 destination_addresses = ["0.0.0.0/0", "::/0"]
181 }
182
183 outbound_rule {
184 protocol = "udp"
185 port_range = "1-65535"
186 destination_addresses = ["0.0.0.0/0", "::/0"]
187 }
188
189 outbound_rule {
190 protocol = "icmp"
191 destination_addresses = ["0.0.0.0/0", "::/0"]
192 }
193 }
194
195 resource "digitalocean_droplet" "runner" {
196 count = var.runner ? 1 : 0
197
198 name = "${var.name}-runner"
199 image = var.image
200 size = var.runner_type
201 region = var.region
202 ssh_keys = [digitalocean_ssh_key.admin.fingerprint]
203 tags = local.labels
204 ipv6 = true
205
206 user_data = local.runner_user_data
207
208 monitoring = true
209
210 # A runner holds no data worth a backup: its checkouts and caches are copies of
211 # what the repositories have.
212 backups = false
213
214 lifecycle {
215 ignore_changes = [user_data]
216 }
217 }