Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ jobs:
cat stderr.log | grep "in.*line.*column" -B1 -A1 --no-group-separator | sed -z 's/\([^\n]*\)\n\s*in "\([^"]*\)", line \([[:digit:]]\+\), column \([[:digit:]]\+\):\s*/::error file=cloud-init.yaml,title=\1 in \2,line=\3,col=\4::/g'
exit 1
fi
- name: Lint dev-test-bootstrap2-iroh cloud-init file
run: |
if ! cloud-init schema -c dev-test-bootstrap2-iroh/cloud-init.yaml 2> >(tee stderr.log) >> $GITHUB_STEP_SUMMARY
then
# Print errors as such in GitHub logs.
cat stderr.log | grep "in.*line.*column" -B1 -A1 --no-group-separator | sed -z 's/\([^\n]*\)\n\s*in "\([^"]*\)", line \([[:digit:]]\+\), column \([[:digit:]]\+\):\s*/::error file=dev-test-bootstrap2-iroh\/cloud-init.yaml,title=\1 in \2,line=\3,col=\4::/g'
exit 1
fi
- name: Lint hc-auth-iroh-unyt cloud-init file
run: |
if ! cloud-init schema -c hc-auth-iroh-unyt/cloud-init.yaml.tmpl 2> >(tee stderr.log) >> $GITHUB_STEP_SUMMARY
Expand Down
3 changes: 3 additions & 0 deletions Pulumi.network-services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ config:
secure: AAABANh3F2nDgAfmPhJ5WfY6zWkRR42P8SvqvKk9K22urvqYiXJSgCjnDau1oUqzV42nTn3CGHiDlwza9YQ4XBEpPKJgxA81
hc-auth-iroh-unyt:session-secret:
secure: AAABADk48DM2ZdxAXP0bqKocsoRrVSiS9aXgDULVL5jsc+TT8up+zivzSyNb0XriQvnYIz+o1VfyyYwVh8YQFqxTDdn4hNJSyh7rW2sYovD6rN3y+GuWhnVBdRNNflhNcGERk5k1nvkM8jDA7yvfkPzzUZaWgL1I
cloudflare:apiToken:
secure: AAABAFPh7LffxJ9S2r0u9GTA2M5dKok33wA3EsEtRrHD5WALAIDju1ANuvtjsaoxtZ7UpojMifDF7Ndk49zxFXhZozwvzn+/kQJWRhdsNqWmSsr9NA==
dns:cloudflare-zone-id: 10a5dbacf3bed1ccc671f04e90f4423f
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,75 @@ podman compose up -d

Note that this will restart the service, which will close any open connections!

## Cloudflare DNS

DNS records for newer deployments are managed by Pulumi using the Cloudflare provider. This requires a Cloudflare API
token and the zone ID for the `holochain.org` DNS zone.

Set the zone ID:

```shell
pulumi config set dns:cloudflare-zone-id <holochain.org-zone-id>
```

Set the API token as a secret:

```shell
pulumi config set --secret cloudflare:apiToken <cloudflare-api-token>
```

The API token needs `Zone:DNS:Edit` permission for the `holochain.org` zone.

## Iroh bootstrap server (dev-test)

A standalone Iroh relay bootstrap server without authentication, intended for development testing. DNS is provisioned
automatically via Cloudflare, and TLS certificates are obtained via certbot on first boot.

The service is deployed as a Podman Quadlet container managed by systemd. On first boot, cloud-init provisions the TLS
certificate (retrying until DNS propagates) and starts the `bootstrap` systemd service.

### First deploy

No manual setup is required beyond the Pulumi configuration. The deployment creates:
- A DigitalOcean droplet
- Cloudflare A and AAAA records for `dev-test-bootstrap2-iroh.holochain.org`

Cloud-init handles certbot and service startup automatically.

### Managing the service

SSH into the server and use systemd to manage the bootstrap service:

```sh
ssh root@dev-test-bootstrap2-iroh.holochain.org

# Check service status
systemctl status bootstrap

# View logs
journalctl -u bootstrap

# Restart
systemctl restart bootstrap
```

### Updating the container

Edit `dev-test-bootstrap2-iroh/cloud-init.yaml` locally and open a PR. Once merged, the cloud-init change will take effect on the
next droplet recreation. To update a running instance, SSH in and update the Quadlet container file:

```sh
scp dev-test-bootstrap2-iroh/cloud-init.yaml root@dev-test-bootstrap2-iroh.holochain.org:/tmp/cloud-init.yaml
```

Then on the server, extract the updated container definition from the cloud-init and reload:

```sh
# Edit /etc/containers/systemd/bootstrap.container with the new image/config
systemctl daemon-reload
systemctl restart bootstrap
```

## Iroh-flavored bootstrap service with authentication for Unyt

Configured an OAuth application as directed by the [hc-auth-iroh-unyt README](https://github.com/holochain/hc-auth-server).
Expand Down
59 changes: 59 additions & 0 deletions dev-test-bootstrap2-iroh/cloud-init.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#cloud-config

snap:
commands:
0: [install, core]
1: [refresh, core]
2: [install, --classic, certbot]

write_files:
- content: |
[Container]
Image=ghcr.io/holochain/kitsune2_bootstrap_srv_iroh_relay:v0.4.0-dev.6
Exec=kitsune2-bootstrap-srv --production --listen [::]:443 --tls-cert /etc/letsencrypt/live/dev-test-bootstrap2-iroh.holochain.org/fullchain.pem --tls-key /etc/letsencrypt/live/dev-test-bootstrap2-iroh.holochain.org/privkey.pem
Environment=RUST_LOG=info
Network=host
Volume=/etc/letsencrypt:/etc/letsencrypt:ro

[Service]
Restart=always

[Install]
WantedBy=multi-user.target default.target
path: /etc/containers/systemd/bootstrap.container
permissions: "0644"

- content: |
#!/bin/bash
set -euo pipefail

# Restart journald so its storage directory matches the machine ID
# that cloud-init may have reinitialized.
systemctl restart systemd-journald

apt-get update -y
apt-get install -y podman

max_attempts=30
delay=20

for attempt in $(seq 1 $max_attempts); do
echo "Certbot attempt $attempt of $max_attempts..."
if certbot certonly --standalone -d dev-test-bootstrap2-iroh.holochain.org --non-interactive --agree-tos -m contact@holochain.org; then
echo "Certificate obtained successfully."
systemctl daemon-reload
systemctl enable --now bootstrap
exit 0
fi
sleep_for=$((delay * attempt))
echo "Certbot failed. Retrying in ${sleep_for}s..."
sleep "$sleep_for"
done

echo "Failed to obtain certificate after $max_attempts attempts."
exit 1
path: /opt/bootstrap_srv/provision-cert.sh
permissions: "0755"

runcmd:
- /opt/bootstrap_srv/provision-cert.sh
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ require (
github.com/pkg/term v1.1.0 // indirect
github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect
github.com/pulumi/esc v0.12.0 // indirect
github.com/pulumi/pulumi-cloudflare/sdk/v5 v5.49.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435
github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE=
github.com/pulumi/esc v0.12.0 h1:lB9ZohZ1Bo1XfhSHjFnk4/y42UMLkmZxz9klH6Jh6Ps=
github.com/pulumi/esc v0.12.0/go.mod h1:iCs5bP1xvleSzcMDwfNc1Ym/6EJ2P6xmrjy0iqb0ATs=
github.com/pulumi/pulumi-cloudflare/sdk/v5 v5.49.1 h1:/9v4yIix3+V1jljWJQalrJvGYEH8MQN9AED3kqvduD8=
github.com/pulumi/pulumi-cloudflare/sdk/v5 v5.49.1/go.mod h1:8VTpp8gXQWlJbW8XcdZKmp+HHAlrbiC0d9e4TrWgCfE=
github.com/pulumi/pulumi-digitalocean/sdk/v4 v4.40.1 h1:e5KmmDROUE64PpXblZqShs4Slp0ljPsczm4sa6EYDSo=
github.com/pulumi/pulumi-digitalocean/sdk/v4 v4.40.1/go.mod h1:9lOuOhYZO/JVysszZwk7xGab+dKmToxSd9ROm/m7gWY=
github.com/pulumi/pulumi/sdk/v3 v3.153.1 h1:qlkttqvoPcuxbMZd1ZfwairuYAZ68izqRnCWmpA9p84=
Expand Down
75 changes: 70 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import (
"os"
"text/template"

"github.com/pulumi/pulumi-cloudflare/sdk/v5/go/cloudflare"
"github.com/pulumi/pulumi-digitalocean/sdk/v4/go/digitalocean"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
pulumiConfig "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)

func main() {
devTestCloudInitYaml, err := os.ReadFile("dev-test/cloud-init.yaml")
devTestBootstrap2IrohCloudInitYaml, err := os.ReadFile("dev-test-bootstrap2-iroh/cloud-init.yaml")
if err != nil {
log.Fatalf("failed to load cloud-init.yaml: %s", err)
log.Fatalf("failed to load dev-test-bootstrap2-iroh/cloud-init.yaml: %s", err)
}

hcAuthIrohUnytCloudInitBytes, err := os.ReadFile("hc-auth-iroh-unyt/cloud-init.yaml.tmpl")
Expand All @@ -27,7 +28,11 @@ func main() {
}

pulumi.Run(func(ctx *pulumi.Context) error {
if err := configureDevTestBootstrapSrv(ctx, string(devTestCloudInitYaml)); err != nil {
if err := configureDevTestBootstrapSrv(ctx); err != nil {
return err
}

if err := configureDevTestBootstrap2Iroh(ctx, string(devTestBootstrap2IrohCloudInitYaml)); err != nil {
return err
}

Expand All @@ -39,7 +44,12 @@ func main() {
})
}

func configureDevTestBootstrapSrv(ctx *pulumi.Context, devTestCloudInitYaml string) error {
func configureDevTestBootstrapSrv(ctx *pulumi.Context) error {
devTestCloudInitYaml, err := os.ReadFile("dev-test/cloud-init.yaml")
if err != nil {
return err
}

getSshKeysResult, err := digitalocean.GetSshKeys(ctx, &digitalocean.GetSshKeysArgs{}, nil)
if err != nil {
return err
Expand All @@ -58,7 +68,7 @@ func configureDevTestBootstrapSrv(ctx *pulumi.Context, devTestCloudInitYaml stri
Ipv6: pulumi.Bool(true),
Tags: pulumi.StringArray{pulumi.String("network-services")},
SshKeys: pulumi.ToStringArray(sshFingerprints),
UserData: pulumi.String(devTestCloudInitYaml),
UserData: pulumi.String(string(devTestCloudInitYaml)),
}, pulumi.IgnoreChanges([]string{"sshKeys", "userData"}))
if err != nil {
return err
Expand All @@ -67,6 +77,61 @@ func configureDevTestBootstrapSrv(ctx *pulumi.Context, devTestCloudInitYaml stri
return nil
}

func configureDevTestBootstrap2Iroh(ctx *pulumi.Context, devTestBootstrap2IrohCloudInitYaml string) error {
cfg := pulumiConfig.New(ctx, "dns")
zoneId := cfg.Require("cloudflare-zone-id")

getSshKeysResult, err := digitalocean.GetSshKeys(ctx, &digitalocean.GetSshKeysArgs{}, nil)
if err != nil {
return err
}

var sshFingerprints []string
for _, key := range getSshKeysResult.SshKeys {
sshFingerprints = append(sshFingerprints, key.Fingerprint)
}

droplet, err := digitalocean.NewDroplet(ctx, "dev-test-bootstrap2-iroh", &digitalocean.DropletArgs{
Image: pulumi.String("ubuntu-24-04-x64"),
Name: pulumi.String("dev-test-bootstrap2-iroh"),
Region: pulumi.String(digitalocean.RegionFRA1),
Size: pulumi.String(digitalocean.DropletSlugDropletS2VCPU2GB),
Ipv6: pulumi.Bool(true),
Tags: pulumi.StringArray{pulumi.String("network-services")},
SshKeys: pulumi.ToStringArray(sshFingerprints),
UserData: pulumi.String(devTestBootstrap2IrohCloudInitYaml),
}, pulumi.IgnoreChanges([]string{"sshKeys"}))
if err != nil {
return err
}

_, err = cloudflare.NewRecord(ctx, "dev-test-bootstrap2-iroh-A", &cloudflare.RecordArgs{
ZoneId: pulumi.String(zoneId),
Name: pulumi.String("dev-test-bootstrap2-iroh"),
Type: pulumi.String("A"),
Content: droplet.Ipv4Address,
Ttl: pulumi.Int(300),
Proxied: pulumi.Bool(false),
})
if err != nil {
return err
}

_, err = cloudflare.NewRecord(ctx, "dev-test-bootstrap2-iroh-AAAA", &cloudflare.RecordArgs{
ZoneId: pulumi.String(zoneId),
Name: pulumi.String("dev-test-bootstrap2-iroh"),
Type: pulumi.String("AAAA"),
Content: droplet.Ipv6Address,
Ttl: pulumi.Int(300),
Proxied: pulumi.Bool(false),
})
if err != nil {
return err
}

return nil
}

func configureHcAuthIrohUnyt(ctx *pulumi.Context, cloudInitTmpl *template.Template) error {
getSshKeysResult, err := digitalocean.GetSshKeys(ctx, &digitalocean.GetSshKeysArgs{}, nil)
if err != nil {
Expand Down
Loading