// A reference machine image for a Fadebox elastic runtime on Google Compute Engine.
//
// Fadebox publishes an image for Compute Engine, the family
// projects/fadebox/global/images/family/fadebox-runtime, built from this template; you need it
// only for an image of your own. It is the shortest image that satisfies the contract documented
// at https://fadebox.io/docs/guides/elastic-runtimes — read that page first. Where the two
// disagree, the page is the contract and this file is out of date.
//
//   packer init fadebox-gce.pkr.hcl
//   packer build -var project=acme-dev-7f2c -var zone=europe-west3-a fadebox-gce.pkr.hcl
//
// Packer authenticates the way gcloud does (`gcloud auth application-default login`). The
// identity that *builds* the image needs to create instances, disks and images in the build
// project — that is a person's grant, and nothing to do with the service account Fadebox
// authenticates as at deploy time.

packer {
  required_plugins {
    googlecompute = {
      source  = "github.com/hashicorp/googlecompute"
      version = "~> 1"
    }
  }
}

variable "project" {
  type        = string
  description = "The project id the image is built in and published to."
}

variable "zone" {
  type        = string
  default     = "europe-west3-a"
  description = "Where the short-lived build instance runs. The image itself is global."
}

// Ubuntu LTS carries cloud-init and its Compute Engine datasource already, which is why it is
// the default here. Debian works the same way — set source_image_project_id to "debian-cloud"
// and source_image_family to "debian-12" or "debian-13"; the provisioner below installs
// cloud-init where the base image has none.
variable "source_image_project_id" {
  type    = string
  default = "ubuntu-os-cloud"
}

variable "source_image_family" {
  type    = string
  default = "ubuntu-2404-lts-amd64"
}

variable "image_family" {
  type        = string
  default     = "fadebox-machine"
  description = "The family a machine profile's `image` parameter names."
}

// The two images every machine's ingress router runs. Baking them in is what keeps a cold start
// off a registry entirely. The tags are Fadebox's own pins and move with each release — take
// them from the version of Fadebox you run, not from here. An installation that rewrites system
// images through a registry prefix must list the *prefixed* references instead, exactly as
// Fadebox will ask for them, and log the build instance into that registry first.
variable "router_images" {
  type    = list(string)
  default = ["traefik:v3.7", "tecnativa/docker-socket-proxy:v0.5.0"]
}

variable "machine_type" {
  type    = string
  default = "e2-standard-2"
}

variable "disk_size" {
  type        = number
  default     = 20
  description = "The build disk in GB. Instances get their own size from the machine profile."
}

locals {
  image_name = "fadebox-machine-${formatdate("YYYYMMDD-hhmmss", timestamp())}"
}

source "googlecompute" "fadebox" {
  project_id              = var.project
  zone                    = var.zone
  source_image_project_id = [var.source_image_project_id]
  source_image_family     = var.source_image_family

  image_name        = local.image_name
  image_family      = var.image_family
  image_description = "Fadebox elastic-runtime machine: Docker Engine, cloud-init, ingress router images"

  machine_type = var.machine_type
  disk_size    = var.disk_size
  ssh_username = "packer"

  // The build instance needs a route out to Docker's apt repository and to the registry the
  // router images come from. If your policy forbids external addresses, set omit_external_ip
  // and use_internal_ip to true and give the build subnet Cloud NAT.
}

build {
  sources = ["source.googlecompute.fadebox"]

  provisioner "shell" {
    execute_command = "sudo -E bash -c '{{ .Vars }} {{ .Path }}'"

    environment_vars = [
      "DEBIAN_FRONTEND=noninteractive",
      "ROUTER_IMAGES=${join(" ", var.router_images)}",
    ]

    inline_shebang = "/bin/bash -eux -o pipefail"
    inline = [
      // The base image's own first boot has to finish before apt is usable.
      "cloud-init status --wait || true",

      // --- what the bootstrap script calls, and cloud-init to run it --------------------
      "apt-get update",
      "apt-get install -y ca-certificates curl gnupg cloud-init openssl iproute2 iptables",

      // Nothing else may manage the firewall: anything that rewrites INPUT takes Fadebox's
      // jump with it, and nothing puts it back before the next boot.
      "apt-get purge -y ufw firewalld || true",

      // Pin the datasource. Compute Engine hands cloud-init the user-data metadata key, and
      // saying so keeps the first boot from probing for anything else.
      "printf 'datasource_list: [ GCE ]\\n' > /etc/cloud/cloud.cfg.d/90-fadebox-datasource.cfg",

      // --- Docker Engine, from Docker's own repository ---------------------------------
      "install -m 0755 -d /etc/apt/keyrings",
      ". /etc/os-release",
      "curl -fsSL \"https://download.docker.com/linux/${ID}/gpg\" -o /etc/apt/keyrings/docker.asc",
      "chmod a+r /etc/apt/keyrings/docker.asc",
      "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable\" > /etc/apt/sources.list.d/docker.list",
      "apt-get update",
      "apt-get install -y docker-ce docker-ce-cli containerd.io",
      "systemctl enable docker",

      // --- the two things the image must NOT do ----------------------------------------
      // A `hosts` key in daemon.json and the -H flags Fadebox's drop-in passes are mutually
      // exclusive: dockerd refuses both together and the machine never comes up. A baked-in
      // TCP listener would be an open daemon for the length of every boot, before cloud-init
      // has written the firewall.
      "if [ -f /etc/docker/daemon.json ] && grep -q '\"hosts\"' /etc/docker/daemon.json; then echo 'daemon.json sets hosts; remove it' >&2; exit 1; fi",
      "if grep -rl -- '-H tcp://' /etc/systemd/system/docker.service.d /etc/default/docker 2>/dev/null | grep .; then echo 'the image starts dockerd on TCP; remove that override' >&2; exit 1; fi",

      // --- bake the ingress router's images in ------------------------------------------
      "systemctl start docker",
      "for image in $ROUTER_IMAGES; do docker pull \"$image\"; done",

      // --- prove the contract before the image is published ------------------------------
      "test -x /usr/bin/dockerd",
      // the drop-in runs /usr/bin/systemctl before every start of the daemon, and the boot
      // document enables docker.service itself, which needs the packaged unit's [Install] section
      "test -x /usr/bin/systemctl",
      "systemctl cat docker.service | grep -q '^\\[Install\\]'",
      "for binary in openssl ip iptables iptables-restore sysctl awk cut date; do command -v \"$binary\" >/dev/null || { echo \"missing $binary\" >&2; exit 1; }; done",
      "openssl version",
      "iptables -m conntrack --help >/dev/null",

      // --- leave it as if it had never booted --------------------------------------------
      "systemctl stop docker docker.socket containerd || true",
      "apt-get clean",
      "rm -rf /var/lib/apt/lists/*",
      "rm -rf /root/.docker /home/packer/.docker",
      // Without these two the machine keeps the build's identity and cloud-init decides it has
      // already run, so no instance would ever receive its user-data.
      "cloud-init clean --logs --seed",
      "truncate -s 0 /etc/machine-id",
      "rm -f /var/lib/dbus/machine-id",
    ]
  }
}
