Files
JMR-devandClaude Opus 5.5 973c1bbcd3 Keep the ACME contact email in Secret Manager
gitea:acmeEmail sat in Pulumi.prod.yaml, which this public repository
publishes, and was then copied into instance metadata. It now lives in a
gitea-acme-email secret instead, read by the VM when it renders the
Caddyfile.

- scripts/bootstrap.sh creates the secret empty and prints how to set
  it, the same as github-pat: the address is chosen, not generated.
- Pulumi grants the VM secretAccessor on it and nothing more. It is kept
  out of secrets.Names, whose members also get secretVersionAdder and are
  mapped to `gitea generate secret` by vm/bootstrap.sh.
- The gitea:acmeEmail config key and the acme-email metadata entry are
  gone.
- vm/bootstrap.sh renders the whole `email` directive. If the secret is
  unreadable it renders a comment instead and warns: Caddy still issues
  certificates under an account with no contact address, whereas an
  empty `email` would fail to parse and leave nothing serving TLS. Same
  directive-or-comment pattern as CADDY_PUBLISH_PORTS and CADDY_SYSCTL.

README setup gains the secret step, plus two that were missing: ADC
login (Pulumi's GCS backend and provider do not use the gcloud login),
and exporting PULUMI_CONFIG_PASSPHRASE from Secret Manager before
`stack init`. Without the latter, init prompts for a new passphrase and
the stack is encrypted with a key Cloud Build's infra trigger never sees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:04:18 -05:00

213 lines
7.5 KiB
Go

// Package compute builds the data disk, its snapshot schedule, and the instance.
package compute
import (
"fmt"
"os"
"strconv"
gcpcompute "github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/serviceaccount"
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/storage"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"gitea-infra/pkg/config"
"gitea-infra/pkg/network"
"gitea-infra/pkg/registry"
)
// DataDiskDeviceName is what the disk shows up as under /dev/disk/by-id/google-*
// inside the guest. vm/bootstrap.sh mounts it by UUID, but it needs this name to
// find the device the first time, before there is a filesystem to have a UUID.
const DataDiskDeviceName = "gitea-data"
type Instance struct {
VM *gcpcompute.Instance
Disk *gcpcompute.Disk
}
func New(
ctx *pulumi.Context,
cfg *config.Config,
net *network.Network,
sa *serviceaccount.Account,
buckets *storage.Bucket,
backupBucket *storage.Bucket,
configHash string,
bootstrapPath string,
deps []pulumi.Resource,
) (*Instance, error) {
opts := pulumi.DependsOn(deps)
// A separate disk from the boot disk, so replacing the instance -- for a
// machine-type change, an image refresh, anything -- never touches the
// repositories. Protect(true) makes an accidental delete a two-step mistake.
//
// This is the disk that holds everything you would miss: repositories, the
// SQLite database, and LFS objects. gitea:dataDiskGb sizes it. The boot disk
// is separate and sized by gitea:bootDiskGb; growing that one buys nothing.
// A pd-balanced disk can be grown in place later, never shrunk.
disk, err := gcpcompute.NewDisk(ctx, "gitea-data", &gcpcompute.DiskArgs{
Name: pulumi.String("gitea-data"),
Zone: pulumi.String(cfg.Zone),
Size: pulumi.Int(cfg.DataDiskGB),
Type: pulumi.String("pd-balanced"),
Description: pulumi.String("Gitea repositories, SQLite database, and LFS objects"),
}, opts, pulumi.Protect(true))
if err != nil {
return nil, err
}
// The safety net for the weekly unattended-reboot window, and for everything
// else that goes wrong at 3am.
policy, err := gcpcompute.NewResourcePolicy(ctx, "gitea-snapshots", &gcpcompute.ResourcePolicyArgs{
Name: pulumi.String("gitea-daily-snapshots"),
Region: pulumi.String(cfg.Region),
SnapshotSchedulePolicy: &gcpcompute.ResourcePolicySnapshotSchedulePolicyArgs{
Schedule: &gcpcompute.ResourcePolicySnapshotSchedulePolicyScheduleArgs{
DailySchedule: &gcpcompute.ResourcePolicySnapshotSchedulePolicyScheduleDailyScheduleArgs{
DaysInCycle: pulumi.Int(1),
StartTime: pulumi.String("03:00"),
},
},
RetentionPolicy: &gcpcompute.ResourcePolicySnapshotSchedulePolicyRetentionPolicyArgs{
MaxRetentionDays: pulumi.Int(14),
// Keep the history even if the disk itself is deleted.
OnSourceDiskDelete: pulumi.String("KEEP_AUTO_SNAPSHOTS"),
},
SnapshotProperties: &gcpcompute.ResourcePolicySnapshotSchedulePolicySnapshotPropertiesArgs{
StorageLocations: pulumi.String(cfg.Region),
Labels: pulumi.StringMap{"app": pulumi.String("gitea")},
},
},
}, opts)
if err != nil {
return nil, err
}
if _, err := gcpcompute.NewDiskResourcePolicyAttachment(ctx, "gitea-data-snapshots", &gcpcompute.DiskResourcePolicyAttachmentArgs{
Name: policy.Name,
Disk: disk.Name,
Zone: pulumi.String(cfg.Zone),
}, opts); err != nil {
return nil, err
}
startupScript, err := os.ReadFile(bootstrapPath)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", bootstrapPath, err)
}
imageGitea := fmt.Sprintf("%s/%s/%s/gitea:prod", cfg.ARHost(), cfg.Project, registry.RepoID)
imageCaddy := fmt.Sprintf("%s/%s/%s/caddy:prod", cfg.ARHost(), cfg.Project, registry.RepoID)
vm, err := gcpcompute.NewInstance(ctx, "gitea-vm", &gcpcompute.InstanceArgs{
Name: pulumi.String("gitea-vm"),
Zone: pulumi.String(cfg.Zone),
MachineType: pulumi.String(cfg.MachineType),
Tags: pulumi.ToStringArray([]string{network.Tag}),
Description: pulumi.String("Gitea, Caddy, and podman quadlets on AlmaLinux 10"),
BootDisk: &gcpcompute.InstanceBootDiskArgs{
InitializeParams: &gcpcompute.InstanceBootDiskInitializeParamsArgs{
// Image family, not a pinned image: new VMs pick up the monthly
// refreshed AlmaLinux build automatically.
Image: pulumi.String("almalinux-cloud/almalinux-10"),
Size: pulumi.Int(cfg.BootDiskGB),
Type: pulumi.String("pd-balanced"),
},
},
AttachedDisks: gcpcompute.InstanceAttachedDiskArray{
&gcpcompute.InstanceAttachedDiskArgs{
Source: disk.ID(),
DeviceName: pulumi.String(DataDiskDeviceName),
Mode: pulumi.String("READ_WRITE"),
},
},
NetworkInterfaces: gcpcompute.InstanceNetworkInterfaceArray{
&gcpcompute.InstanceNetworkInterfaceArgs{
Network: net.Network.ID(),
Subnetwork: net.Subnetwork.ID(),
AccessConfigs: gcpcompute.InstanceNetworkInterfaceAccessConfigArray{
&gcpcompute.InstanceNetworkInterfaceAccessConfigArgs{
NatIp: net.Address.Address,
},
},
},
},
ServiceAccount: &gcpcompute.InstanceServiceAccountArgs{
Email: sa.Email,
// cloud-platform plus per-resource IAM, rather than a hand-rolled
// scope list: scopes are a coarse legacy filter and IAM is where the
// real restrictions live.
Scopes: pulumi.ToStringArray([]string{"cloud-platform"}),
},
ShieldedInstanceConfig: &gcpcompute.InstanceShieldedInstanceConfigArgs{
EnableSecureBoot: pulumi.Bool(true),
EnableVtpm: pulumi.Bool(true),
EnableIntegrityMonitoring: pulumi.Bool(true),
},
Metadata: pulumi.StringMap{
// OS Login means SSH access follows IAM instead of metadata keys.
"enable-oslogin": pulumi.String("TRUE"),
"google-logging-enabled": pulumi.String("true"),
"config-bucket": buckets.Name,
"backup-bucket": backupBucket.Name,
"gcp-project": pulumi.String(cfg.Project),
"gcp-region": pulumi.String(cfg.Region),
"ar-host": pulumi.String(cfg.ARHost()),
// Floating :prod tags -- AutoUpdate=registry has nothing to poll if
// these are digests.
"image-gitea": pulumi.String(imageGitea),
"image-caddy": pulumi.String(imageCaddy),
"domain": pulumi.String(cfg.Domain),
"app-name": pulumi.String(cfg.AppName),
"require-signin-view": pulumi.String(strconv.FormatBool(cfg.RequireSigninView)),
// Drives Coraza's SecRuleEngine and, from the same value, whether
// the fail2ban jail acting on WAF verdicts is enabled.
"waf-mode": pulumi.String(cfg.WAFMode),
"podman-subnet": pulumi.String(cfg.PodmanCIDR),
"podman-gateway": pulumi.String(gatewayFor(cfg.PodmanCIDR)),
"data-disk-device": pulumi.String("/dev/disk/by-id/google-" + DataDiskDeviceName),
// Not read by the guest -- it exists so `gcloud compute instances
// describe` shows which revision of vm/ the box was built against.
"config-hash": pulumi.String(configHash),
},
MetadataStartupScript: pulumi.String(string(startupScript)),
// Lets Pulumi change machine type or metadata in place instead of
// refusing the update.
AllowStoppingForUpdate: pulumi.Bool(true),
Labels: pulumi.StringMap{"app": pulumi.String("gitea")},
}, opts)
if err != nil {
return nil, err
}
return &Instance{VM: vm, Disk: disk}, nil
}
// gatewayFor returns the .1 address of a /24. The podman network's gateway has
// to be stated explicitly alongside the pinned subnet.
func gatewayFor(cidr string) string {
var a, b, c, d, bits int
if _, err := fmt.Sscanf(cidr, "%d.%d.%d.%d/%d", &a, &b, &c, &d, &bits); err != nil {
return ""
}
return fmt.Sprintf("%d.%d.%d.1", a, b, c)
}