From 412c5c1ef7d4363507beeb4495bcec064d3344b3 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Mon, 26 Oct 2026 21:20:44 -0300 Subject: [PATCH 1/5] reference: term. is an installation's own name, and modules/terminal goes spin-terraform-aws no longer has modules/terminal: the proxy serves term. from its image, and the name is one of the installation's records. The prerequisites list it with the others, the optional modules are the identity issuer alone, and refgen writes no page for a module that is gone. Co-Authored-By: Claude Opus 5.5 --- src/content/docs/aws/prerequisites.md | 10 +-- .../docs/reference/terraform/terminal.md | 81 ------------------- tools/refgen/terraform.go | 1 - tools/refgen/terraform_test.go | 18 ++--- .../testdata/tf/modules/controlplane/main.tf | 8 ++ .../testdata/tf/modules/terminal/main.tf | 11 --- 6 files changed, 22 insertions(+), 107 deletions(-) delete mode 100644 src/content/docs/reference/terraform/terminal.md delete mode 100644 tools/refgen/testdata/tf/modules/terminal/main.tf diff --git a/src/content/docs/aws/prerequisites.md b/src/content/docs/aws/prerequisites.md index 272608f..5f3179d 100644 --- a/src/content/docs/aws/prerequisites.md +++ b/src/content/docs/aws/prerequisites.md @@ -26,6 +26,7 @@ zone in Route 53, and you point the domain's nameservers at it. The installation |---|---| | `app.` | The dashboard and the API | | `tunnel.app.` | The relay runners connect to | +| `term.` | The browser terminal: an SSH client for a workspace, in the browser, that signs in with a passkey | | `*.ws.` | Workspaces' ports | Workspaces are under a name of their own so that a workspace is never a subdomain of the @@ -33,16 +34,15 @@ dashboard: nothing a workspace serves can set a cookie the dashboard reads. ### Optional modules -Two modules of spin-terraform-aws are not part of an installation unless you add them beside -it. Each serves one more name, on its own CloudFront origin, and takes a provider in `us-east-1` -for its certificate: +One module of spin-terraform-aws is not part of an installation unless you add it beside it. It +serves one more name, on its own CloudFront origin, and takes a provider in `us-east-1` for its +certificate: | Name | Module | What | |---|---|---| -| `term.` | [`modules/terminal`](/reference/terraform/terminal/) | The browser terminal: an SSH client for a workspace, in the browser, that signs in with a passkey | | `id.` | [`modules/identity-issuer`](/reference/terraform/identity-issuer/) | The identity issuer: the documents a system outside Spinbox verifies a workspace's identity token with | -Each module's page says what it takes, and its README what it needs and how to add it. +Its page says what it takes, and its README what it needs and how to add it. ## Room for runners diff --git a/src/content/docs/reference/terraform/terminal.md b/src/content/docs/reference/terraform/terminal.md deleted file mode 100644 index f28aceb..0000000 --- a/src/content/docs/reference/terraform/terminal.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: "modules/terminal" -description: "spin-terraform-aws, the optional browser terminal at term., added beside an installation." -source: "spin-terraform-aws:modules/terminal/terminal.tf" -editUrl: false -sidebar: - order: 5 ---- - - - -Not part of an installation unless added beside it: the installation module does not call it. Its [README](https://github.com/spin-stack/spin-terraform-aws/tree/v20261025.01/modules/terminal) says what it needs and how to add it. - -Source: `github.com/spin-stack/spin-terraform-aws//modules/terminal?ref=v20261025.01`. Generated from its variables and outputs. - -## Variables - -| Variable | Type | Default | -|---|---|---| -| [`name`](#name) | `string` | **required** | -| [`iam_name`](#iam_name) | `string` | **required** | -| [`domain`](#domain) | `string` | **required** | -| [`zone_id`](#zone_id) | `string` | **required** | -| [`release`](#release) | `string` | **required** | -| [`files`](#files) | `string` | **required** | -| [`tags`](#tags) | `map(string)` | `{}` | - -### name - -The installation's name, as modules/controlplane's name output says it. - -- Type: `string` -- **Required** - -### iam_name - -The prefix of the installation's roles (modules/controlplane's iam_name): every one is refused a write here. - -- Type: `string` -- **Required** - -### domain - -The installation's domain: the terminal is term.<domain>, and asks app.<domain>. - -- Type: `string` -- **Required** - -### zone_id - -The domain's public zone (modules/controlplane's public_zone_id). - -- Type: `string` -- **Required** - -### release - -The spin release whose terminal is published, which `files` must be: the terminal holds a launch claim to the release window around it. - -- Type: `string` -- **Required** - -### files - -The directory `task term:fetch RELEASE=<release>` unpacked the release's verified spin-term.tar.gz into: index.html, term.wasm, wasm_exec.js, assets/, and the RELEASE it is. - -- Type: `string` -- **Required** - -### tags - -Tags every resource carries. - -- Type: `map(string)` -- Default: `{}` - -## Outputs - -| Output | What it is | -|---|---| -| `url` | The browser terminal's origin, which the dashboard opens a workspace's terminal at. | diff --git a/tools/refgen/terraform.go b/tools/refgen/terraform.go index f9ca221..216fd2a 100644 --- a/tools/refgen/terraform.go +++ b/tools/refgen/terraform.go @@ -26,7 +26,6 @@ var tfModules = []tfModule{ {"modules/controlplane", "controlplane", "modules/controlplane", "the network, bucket, database, control plane and proxy, used on its own", "modules/controlplane/variables.tf", false}, {"modules/runners", "runners", "modules/runners", "the runners' autoscaling group, used on its own", "modules/runners/variables.tf", false}, {"bootstrap", "bootstrap", "bootstrap", "the once-per-account state bucket and key, and CI's roles", "bootstrap/variables.tf", false}, - {"modules/terminal", "terminal", "modules/terminal", "the optional browser terminal at term., added beside an installation", "modules/terminal/terminal.tf", true}, {"modules/identity-issuer", "identity-issuer", "modules/identity-issuer", "the optional identity issuer at id., added beside an installation", "modules/identity-issuer/issuer.tf", true}, } diff --git a/tools/refgen/terraform_test.go b/tools/refgen/terraform_test.go index 074f62e..bbfe1c2 100644 --- a/tools/refgen/terraform_test.go +++ b/tools/refgen/terraform_test.go @@ -48,19 +48,19 @@ func TestTheNamesAModuleServesAreItsRecords(t *testing.T) { if err != nil { t.Fatal(err) } - want := []string{"*.ws.", "app.", "tunnel.app."} + want := []string{"*.ws.", "app.", "term.", "tunnel.app."} if !slices.Equal(got, want) { t.Errorf("installationNames = %q, want %q", got, want) } - if got, _ := servedNames(filepath.Join(fixtureTF, "modules", "terminal")); !slices.Equal(got, []string{"term."}) { - t.Errorf("the terminal serves %q", got) + if got, _ := servedNames(filepath.Join(fixtureTF, "modules", "identity-issuer")); !slices.Equal(got, []string{"id."}) { + t.Errorf("the identity issuer serves %q", got) } } const prerequisites = "---\ntitle: Before you start\n---\n\n## A domain\n\n| Name | What |\n|---|---|\n" + - "| `app.` | The dashboard |\n| `tunnel.app.` | The relay |\n| `*.ws.` | Ports |\n\n" + + "| `app.` | The dashboard |\n| `tunnel.app.` | The relay |\n| `term.` | The terminal |\n" + + "| `*.ws.` | Ports |\n\n" + "### Optional modules\n\n| Name | Module | What |\n|---|---|---|\n" + - "| `term.` | [`modules/terminal`](/reference/terraform/terminal/) | The terminal |\n" + "| `id.` | [`modules/identity-issuer`](/reference/terraform/identity-issuer/) | The issuer |\n\n## Next\n" // The page's names are the modules', both ways, and each optional one beside its module. @@ -72,7 +72,7 @@ func TestThePrerequisitesNameWhatTheModulesServe(t *testing.T) { wantBad(t, bad) wrong := strings.NewReplacer("| `tunnel.app.` | The relay |\n", "| `old.` | Gone |\n", - "[`modules/identity-issuer`](/reference/terraform/identity-issuer/)", "[`modules/terminal`](/reference/terraform/terminal/)").Replace(prerequisites) + "[`modules/identity-issuer`](/reference/terraform/identity-issuer/)", "[`modules/runners`](/reference/terraform/runners/)").Replace(prerequisites) bad, err = checkDomains(writePage(t, map[string]string{"aws/prerequisites.md": wrong}), fixtureTF) if err != nil { t.Fatal(err) @@ -81,7 +81,7 @@ func TestThePrerequisitesNameWhatTheModulesServe(t *testing.T) { "the installation serves tunnel.app., and the page does not name it", "names old., which the installation does not serve", "modules/identity-issuer serves id., and the page does not name it", - "names id., which modules/terminal does not serve") + `under "modules/runners", which is no optional module`) } // What an aws/ page tells people to type and to set is what the module has. @@ -183,7 +183,7 @@ func TestPagesPinTheModuleAndTheRelease(t *testing.T) { page := "source = \"github.com/spin-stack/spin-terraform-aws?ref=\"\n" + "spin_version = \"v20261019.02\"\n" + "pin one (`?ref=v.`)\n" + - "`//modules/terminal?ref=v20261001.01` and v20261001.01\n" + "`//modules/identity-issuer?ref=v20261001.01` and v20261001.01\n" // The dashboard's page names its screenshots' release, the documented one. shotsLine := "These screenshots are of spin v20261024.01.\n" docs := writePage(t, map[string]string{"aws/overview.mdx": page, shotsPageDoc: shotsLine}) @@ -202,7 +202,7 @@ func TestPagesPinTheModuleAndTheRelease(t *testing.T) { want := "source = \"github.com/spin-stack/spin-terraform-aws?ref=v20261024.01\"\n" + "spin_version = \"v20261019.02\"\n" + "pin one (`?ref=v.`)\n" + - "`//modules/terminal?ref=v20261024.01` and v20261019.02\n" + "`//modules/identity-issuer?ref=v20261024.01` and v20261019.02\n" if string(raw) != want { t.Errorf("rewritten:\n%s\nwant:\n%s", raw, want) } diff --git a/tools/refgen/testdata/tf/modules/controlplane/main.tf b/tools/refgen/testdata/tf/modules/controlplane/main.tf index 4e07cab..af10dc2 100644 --- a/tools/refgen/testdata/tf/modules/controlplane/main.tf +++ b/tools/refgen/testdata/tf/modules/controlplane/main.tf @@ -16,6 +16,14 @@ resource "aws_route53_record" "app" { name = each.value } +locals { + term = "term.${var.domain}" +} + +resource "aws_route53_record" "terminal" { + name = local.term +} + # Named by the certificate AWS issues, which no page can say. resource "aws_route53_record" "validation" { for_each = { for o in aws_acm_certificate.app.domain_validation_options : o.domain_name => o } diff --git a/tools/refgen/testdata/tf/modules/terminal/main.tf b/tools/refgen/testdata/tf/modules/terminal/main.tf deleted file mode 100644 index e428e57..0000000 --- a/tools/refgen/testdata/tf/modules/terminal/main.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "domain" { - type = string -} - -locals { - host = "term.${var.domain}" -} - -resource "aws_route53_record" "terminal" { - name = local.host -} From a21fdba75c17496b7f16e125639dcf81c86735ed Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Mon, 26 Oct 2026 21:45:47 -0300 Subject: [PATCH 2/5] reference: id. is an installation's own name too, and no module is optional spin-terraform-aws drops modules/identity-issuer: the proxy writes the issuer's documents itself and serves id.. The prerequisites list it with the installation's names, and refgen loses the optional modules - a page note, a table and its check - since none is left. Co-Authored-By: Claude Opus 5.5 --- src/content/docs/aws/prerequisites.md | 13 +--- .../reference/terraform/identity-issuer.md | 65 ------------------- tools/refgen/dns.go | 33 ++-------- tools/refgen/terraform.go | 19 ++---- tools/refgen/terraform_test.go | 22 ++----- .../testdata/tf/modules/controlplane/main.tf | 4 ++ .../tf/modules/identity-issuer/main.tf | 7 -- 7 files changed, 21 insertions(+), 142 deletions(-) delete mode 100644 src/content/docs/reference/terraform/identity-issuer.md delete mode 100644 tools/refgen/testdata/tf/modules/identity-issuer/main.tf diff --git a/src/content/docs/aws/prerequisites.md b/src/content/docs/aws/prerequisites.md index 5f3179d..de3045f 100644 --- a/src/content/docs/aws/prerequisites.md +++ b/src/content/docs/aws/prerequisites.md @@ -27,23 +27,12 @@ zone in Route 53, and you point the domain's nameservers at it. The installation | `app.` | The dashboard and the API | | `tunnel.app.` | The relay runners connect to | | `term.` | The browser terminal: an SSH client for a workspace, in the browser, that signs in with a passkey | +| `id.` | The identity issuer: what a system outside Spinbox verifies a workspace's identity token, or a commit it signed, with | | `*.ws.` | Workspaces' ports | Workspaces are under a name of their own so that a workspace is never a subdomain of the dashboard: nothing a workspace serves can set a cookie the dashboard reads. -### Optional modules - -One module of spin-terraform-aws is not part of an installation unless you add it beside it. It -serves one more name, on its own CloudFront origin, and takes a provider in `us-east-1` for its -certificate: - -| Name | Module | What | -|---|---|---| -| `id.` | [`modules/identity-issuer`](/reference/terraform/identity-issuer/) | The identity issuer: the documents a system outside Spinbox verifies a workspace's identity token with | - -Its page says what it takes, and its README what it needs and how to add it. - ## Room for runners Each runner is a large spot machine, of one of the types diff --git a/src/content/docs/reference/terraform/identity-issuer.md b/src/content/docs/reference/terraform/identity-issuer.md deleted file mode 100644 index 8274803..0000000 --- a/src/content/docs/reference/terraform/identity-issuer.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: "modules/identity-issuer" -description: "spin-terraform-aws, the optional identity issuer at id., added beside an installation." -source: "spin-terraform-aws:modules/identity-issuer/issuer.tf" -editUrl: false -sidebar: - order: 6 ---- - - - -Not part of an installation unless added beside it: the installation module does not call it. Its [README](https://github.com/spin-stack/spin-terraform-aws/tree/v20261025.01/modules/identity-issuer) says what it needs and how to add it. - -Source: `github.com/spin-stack/spin-terraform-aws//modules/identity-issuer?ref=v20261025.01`. Generated from its variables and outputs. - -## Variables - -| Variable | Type | Default | -|---|---|---| -| [`name`](#name) | `string` | **required** | -| [`domain`](#domain) | `string` | **required** | -| [`zone_id`](#zone_id) | `string` | **required** | -| [`documents`](#documents) | `string` | **required** | -| [`tags`](#tags) | `map(string)` | `{}` | - -### name - -The installation's name, as modules/controlplane's name output says it. - -- Type: `string` -- **Required** - -### domain - -The installation's domain: the issuer is id.<domain>. - -- Type: `string` -- **Required** - -### zone_id - -The domain's public zone (modules/controlplane's public_zone_id). - -- Type: `string` -- **Required** - -### documents - -The directory `spin-controlplane identity documents --out` wrote: it holds .well-known/openid-configuration, .well-known/jwks.json, .well-known/allowed_signers and .well-known/spiffe-bundle. - -- Type: `string` -- **Required** - -### tags - -Tags every resource carries. - -- Type: `map(string)` -- Default: `{}` - -## Outputs - -| Output | What it is | -|---|---| -| `issuer` | The issuer a workspace's identity token names, which a relying party is configured with. | diff --git a/tools/refgen/dns.go b/tools/refgen/dns.go index 02b1646..9584ab6 100644 --- a/tools/refgen/dns.go +++ b/tools/refgen/dns.go @@ -146,14 +146,12 @@ func installationNames(tf string) ([]string, error) { return slices.Compact(names), nil } -// domainRow is a row of a table of names: the name, and the module that serves it when the -// table says. -var domainRow = regexp.MustCompile("(?m)^\\| `([^`]+)` \\|(?: \\[`(modules/[\\w-]+)`\\])?") +// domainRow is a row of a table of names: the name. +var domainRow = regexp.MustCompile("(?m)^\\| `([^`]+)` \\|") // checkDomains holds aws/prerequisites.md's names to the modules: the table under "A domain" is -// every name the installation serves and nothing else, and the one under "Optional modules" is -// every name each optional module serves, beside that module. A name the module adds is one a -// reader must point the domain at, and the page is where they learn it. +// every name the installation serves and nothing else. A name the module adds is one a reader +// must point the domain at, and the page is where they learn it. func checkDomains(docs, tf string) ([]string, error) { page := filepath.Join(docs, "aws", "prerequisites.md") raw, err := os.ReadFile(page) @@ -173,29 +171,6 @@ func checkDomains(docs, tf string) ([]string, error) { have = append(have, string(m[1])) } bad := diffNames(page, "the installation", want, have) - - optional := section(raw, "### Optional modules") - if optional == nil { - return append(bad, page+`: no "### Optional modules" section`), nil - } - byModule := map[string][]string{} - for _, m := range domainRow.FindAllSubmatch(optional, -1) { - byModule[string(m[2])] = append(byModule[string(m[2])], string(m[1])) - } - for _, m := range tfModules { - if !m.optional { - continue - } - names, err := servedNames(filepath.Join(tf, m.dir)) - if err != nil { - return nil, err - } - bad = append(bad, diffNames(page, m.dir, names, byModule[m.dir])...) - delete(byModule, m.dir) - } - for mod, names := range byModule { - bad = append(bad, fmt.Sprintf("%s: the optional modules name %v under %q, which is no optional module of spin-terraform-aws", page, names, mod)) - } sort.Strings(bad) return bad, nil } diff --git a/tools/refgen/terraform.go b/tools/refgen/terraform.go index 216fd2a..d0615f3 100644 --- a/tools/refgen/terraform.go +++ b/tools/refgen/terraform.go @@ -14,19 +14,14 @@ type tfModule struct { dir, slug, title, what string // source is the file the page links as its source: the one its variables are declared in. source string - // optional is a module an installation does not have unless it is added beside it: the root - // does not call it, so what it serves is not what an installation needs. - optional bool } -// The modules an installation is made of, in the order a reader meets them, then the ones added -// beside it. +// The modules an installation is made of, in the order a reader meets them. var tfModules = []tfModule{ - {".", "root", "The installation module", "the module an installation uses: every variable it takes and every output it gives", "variables.tf", false}, - {"modules/controlplane", "controlplane", "modules/controlplane", "the network, bucket, database, control plane and proxy, used on its own", "modules/controlplane/variables.tf", false}, - {"modules/runners", "runners", "modules/runners", "the runners' autoscaling group, used on its own", "modules/runners/variables.tf", false}, - {"bootstrap", "bootstrap", "bootstrap", "the once-per-account state bucket and key, and CI's roles", "bootstrap/variables.tf", false}, - {"modules/identity-issuer", "identity-issuer", "modules/identity-issuer", "the optional identity issuer at id., added beside an installation", "modules/identity-issuer/issuer.tf", true}, + {".", "root", "The installation module", "the module an installation uses: every variable it takes and every output it gives", "variables.tf"}, + {"modules/controlplane", "controlplane", "modules/controlplane", "the network, bucket, database, control plane and proxy, used on its own", "modules/controlplane/variables.tf"}, + {"modules/runners", "runners", "modules/runners", "the runners' autoscaling group, used on its own", "modules/runners/variables.tf"}, + {"bootstrap", "bootstrap", "bootstrap", "the once-per-account state bucket and key, and CI's roles", "bootstrap/variables.tf"}, } // terraformPages reads each module's variables and outputs as OpenTofu would, so a variable @@ -45,10 +40,6 @@ func terraformPages(tf, release string) (map[string][]byte, error) { if m.dir != "." { src += "//" + m.dir } - if m.optional { - fmt.Fprintf(&b, "Not part of an installation unless added beside it: the installation module does not call it. "+ - "Its [README](https://github.com/spin-stack/spin-terraform-aws/tree/%s/%s) says what it needs and how to add it.\n\n", release, m.dir) - } fmt.Fprintf(&b, "Source: `%s?ref=%s`. Generated from its variables and outputs.\n\n", src, release) vars := make([]*tfconfig.Variable, 0, len(mod.Variables)) diff --git a/tools/refgen/terraform_test.go b/tools/refgen/terraform_test.go index bbfe1c2..1c96c33 100644 --- a/tools/refgen/terraform_test.go +++ b/tools/refgen/terraform_test.go @@ -48,22 +48,17 @@ func TestTheNamesAModuleServesAreItsRecords(t *testing.T) { if err != nil { t.Fatal(err) } - want := []string{"*.ws.", "app.", "term.", "tunnel.app."} + want := []string{"*.ws.", "app.", "id.", "term.", "tunnel.app."} if !slices.Equal(got, want) { t.Errorf("installationNames = %q, want %q", got, want) } - if got, _ := servedNames(filepath.Join(fixtureTF, "modules", "identity-issuer")); !slices.Equal(got, []string{"id."}) { - t.Errorf("the identity issuer serves %q", got) - } } const prerequisites = "---\ntitle: Before you start\n---\n\n## A domain\n\n| Name | What |\n|---|---|\n" + "| `app.` | The dashboard |\n| `tunnel.app.` | The relay |\n| `term.` | The terminal |\n" + - "| `*.ws.` | Ports |\n\n" + - "### Optional modules\n\n| Name | Module | What |\n|---|---|---|\n" + - "| `id.` | [`modules/identity-issuer`](/reference/terraform/identity-issuer/) | The issuer |\n\n## Next\n" + "| `id.` | The issuer |\n| `*.ws.` | Ports |\n\n## Next\n" -// The page's names are the modules', both ways, and each optional one beside its module. +// The page's names are the modules', both ways. func TestThePrerequisitesNameWhatTheModulesServe(t *testing.T) { bad, err := checkDomains(writePage(t, map[string]string{"aws/prerequisites.md": prerequisites}), fixtureTF) if err != nil { @@ -71,17 +66,14 @@ func TestThePrerequisitesNameWhatTheModulesServe(t *testing.T) { } wantBad(t, bad) - wrong := strings.NewReplacer("| `tunnel.app.` | The relay |\n", "| `old.` | Gone |\n", - "[`modules/identity-issuer`](/reference/terraform/identity-issuer/)", "[`modules/runners`](/reference/terraform/runners/)").Replace(prerequisites) + wrong := strings.NewReplacer("| `tunnel.app.` | The relay |\n", "| `old.` | Gone |\n").Replace(prerequisites) bad, err = checkDomains(writePage(t, map[string]string{"aws/prerequisites.md": wrong}), fixtureTF) if err != nil { t.Fatal(err) } wantBad(t, bad, "the installation serves tunnel.app., and the page does not name it", - "names old., which the installation does not serve", - "modules/identity-issuer serves id., and the page does not name it", - `under "modules/runners", which is no optional module`) + "names old., which the installation does not serve") } // What an aws/ page tells people to type and to set is what the module has. @@ -183,7 +175,7 @@ func TestPagesPinTheModuleAndTheRelease(t *testing.T) { page := "source = \"github.com/spin-stack/spin-terraform-aws?ref=\"\n" + "spin_version = \"v20261019.02\"\n" + "pin one (`?ref=v.`)\n" + - "`//modules/identity-issuer?ref=v20261001.01` and v20261001.01\n" + "`//modules/runners?ref=v20261001.01` and v20261001.01\n" // The dashboard's page names its screenshots' release, the documented one. shotsLine := "These screenshots are of spin v20261024.01.\n" docs := writePage(t, map[string]string{"aws/overview.mdx": page, shotsPageDoc: shotsLine}) @@ -202,7 +194,7 @@ func TestPagesPinTheModuleAndTheRelease(t *testing.T) { want := "source = \"github.com/spin-stack/spin-terraform-aws?ref=v20261024.01\"\n" + "spin_version = \"v20261019.02\"\n" + "pin one (`?ref=v.`)\n" + - "`//modules/identity-issuer?ref=v20261024.01` and v20261019.02\n" + "`//modules/runners?ref=v20261024.01` and v20261019.02\n" if string(raw) != want { t.Errorf("rewritten:\n%s\nwant:\n%s", raw, want) } diff --git a/tools/refgen/testdata/tf/modules/controlplane/main.tf b/tools/refgen/testdata/tf/modules/controlplane/main.tf index af10dc2..c079d8f 100644 --- a/tools/refgen/testdata/tf/modules/controlplane/main.tf +++ b/tools/refgen/testdata/tf/modules/controlplane/main.tf @@ -24,6 +24,10 @@ resource "aws_route53_record" "terminal" { name = local.term } +resource "aws_route53_record" "issuer" { + name = "id.${var.domain}" +} + # Named by the certificate AWS issues, which no page can say. resource "aws_route53_record" "validation" { for_each = { for o in aws_acm_certificate.app.domain_validation_options : o.domain_name => o } diff --git a/tools/refgen/testdata/tf/modules/identity-issuer/main.tf b/tools/refgen/testdata/tf/modules/identity-issuer/main.tf deleted file mode 100644 index 51b6bc5..0000000 --- a/tools/refgen/testdata/tf/modules/identity-issuer/main.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "domain" { - type = string -} - -resource "aws_route53_record" "issuer" { - name = "id.${var.domain}" -} From 4ce20e967ef8cdd6e933532be08543fe269b80f2 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Mon, 26 Oct 2026 22:15:20 -0300 Subject: [PATCH 3/5] reference: the images' page follows the modules' pages, two fewer of them Co-Authored-By: Claude Opus 5.5 --- src/content/docs/reference/terraform/images.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/reference/terraform/images.md b/src/content/docs/reference/terraform/images.md index a585b9c..8ff02a9 100644 --- a/src/content/docs/reference/terraform/images.md +++ b/src/content/docs/reference/terraform/images.md @@ -4,7 +4,7 @@ description: "The Spin OS images the module boots for the release this site docu source: "spin-terraform-aws:images.json" editUrl: false sidebar: - order: 7 + order: 5 --- From 03f00f5aed476a5045387fdfb09f399d48cdb76a Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Mon, 26 Oct 2026 22:33:42 -0300 Subject: [PATCH 4/5] pricing: a runner is every type the module's patterns match, within its vCPUs and on its disk spin-terraform-aws's runners now take instance_types as EC2's allowed_instance_types does - names or patterns, c8id.* - within vcpus, with a local NVMe while data_volume_gb is 0, and the price list has no row for a pattern: task pricing failed on c8i.*. The record keeps those defaults, as -check compares them, and beside them the types they match that every region prices, the cheapest first; the cost page's runner is that first one. Co-Authored-By: Claude Opus 5.5 --- src/lib/pricing.ts | 5 +- tools/pricing/pricing.go | 166 ++++++++++++++++++++++++++-------- tools/pricing/pricing_test.go | 126 ++++++++++++++++++++------ 3 files changed, 230 insertions(+), 67 deletions(-) diff --git a/src/lib/pricing.ts b/src/lib/pricing.ts index c524977..4aabf46 100644 --- a/src/lib/pricing.ts +++ b/src/lib/pricing.ts @@ -36,8 +36,9 @@ function price(region: string, type: string): number { /** The three scenarios, each a month in dollars per region. */ export function scenarios(): Scenario[] { - const { controlPlane, proxy, runners, maxRunners } = machines; - const runner = runners[0]; + const { controlPlane, proxy, maxRunners } = machines; + // The cheapest type a runner may be: what the group's on-demand price is at the least. + const runner = pricing.runners[0]; const perRegion = (f: (region: string) => number) => Object.fromEntries(regions.map((r) => [r, f(r)])); const idle = (r: string) => (price(r, controlPlane) + price(r, proxy) + ipv4Hourly) * hours + zones * zoneMonthly; const n = maxRunners; diff --git a/tools/pricing/pricing.go b/tools/pricing/pricing.go index 980189d..f28a414 100644 --- a/tools/pricing/pricing.go +++ b/tools/pricing/pricing.go @@ -17,8 +17,10 @@ import ( "io" "net/http" "os" + "path" "path/filepath" "regexp" + "slices" "sort" "strconv" "strings" @@ -46,18 +48,25 @@ type Record struct { // Published is when AWS published the lists read, as each list's manifest says. Published string `json:"published"` // Source is where the lists come from, for a reader. - Source string `json:"source"` - Machines Machines `json:"machines"` - Regions map[string]Region `json:"regions"` + Source string `json:"source"` + Machines Machines `json:"machines"` + // Runners are the types Machines' runner defaults match in every region's list, the cheapest + // first in the first region: what a runner may be. + Runners []string `json:"runners"` + Regions map[string]Region `json:"regions"` } // Machines is what the module starts by default, at the commit the site pins: the control -// plane's and the proxy's machine, the types a runner may be, and the most runners at once. +// plane's and the proxy's machine, what a runner's type is drawn from - the names and patterns +// of instance_types, within vcpus, with a local NVMe while data_volume_gb is 0 - and the most +// runners at once. type Machines struct { - ControlPlane string `json:"controlPlane"` - Proxy string `json:"proxy"` - Runners []string `json:"runners"` - MaxRunners int `json:"maxRunners"` + ControlPlane string `json:"controlPlane"` + Proxy string `json:"proxy"` + RunnerTypes []string `json:"runnerTypes"` + RunnerVCPUs [2]int `json:"runnerVcpus"` + RunnerLocalDisk bool `json:"runnerLocalDisk"` + MaxRunners int `json:"maxRunners"` } // Region is each machine's price in one region. @@ -113,12 +122,13 @@ var ( stringValue = regexp.MustCompile(`(?m)^\s*default\s*=\s*"([^"]+)"`) listValue = regexp.MustCompile(`(?m)^\s*default\s*=\s*\[([^\]]*)\]`) intValue = regexp.MustCompile(`(?m)^\s*default\s*=\s*(\d+)`) + rangeValue = regexp.MustCompile(`(?m)^\s*default\s*=\s*\{\s*min\s*=\s*(\d+)\s*,\s*max\s*=\s*(\d+)\s*\}`) ) // defaults reads what the module starts by default: the control plane's and the proxy's -// instance type from modules/controlplane, the runners' types and the most runners from -// modules/runners (max_hosts, which the root calls max_runners). A variable without a default -// is a mistake here, not a value to guess. +// instance type from modules/controlplane, and from modules/runners what a runner's type is +// drawn from and the most runners (max_hosts, which the root calls max_runners). A variable +// without a default is a mistake here, not a value to guess. func defaults(terraform string) (Machines, error) { cp, err := os.ReadFile(filepath.Join(terraform, "modules", "controlplane", "variables.tf")) if err != nil { @@ -145,12 +155,25 @@ func defaults(terraform string) (Machines, error) { } for _, item := range strings.Split(string(l[1]), ",") { if item = strings.Trim(strings.TrimSpace(item), `"`); item != "" { - m.Runners = append(m.Runners, item) + m.RunnerTypes = append(m.RunnerTypes, item) } } - if len(m.Runners) == 0 { + if len(m.RunnerTypes) == 0 { return Machines{}, errors.New("modules/runners instance_types: an empty default") } + block, err = variableBlock(runners, "vcpus") + if err != nil { + return Machines{}, err + } + v := rangeValue.FindSubmatch(block) + if v == nil { + return Machines{}, errors.New("modules/runners vcpus: no { min, max } default") + } + m.RunnerVCPUs[0], _ = strconv.Atoi(string(v[1])) // digits, by the expression + m.RunnerVCPUs[1], _ = strconv.Atoi(string(v[2])) + if m.RunnerLocalDisk, err = zeroDefault(runners, "data_volume_gb"); err != nil { + return Machines{}, err + } block, err = variableBlock(runners, "max_hosts") if err != nil { return Machines{}, err @@ -175,6 +198,19 @@ func stringDefault(src []byte, name string) (string, error) { return string(m[1]), nil } +// zeroDefault is whether variable name's number default is 0. +func zeroDefault(src []byte, name string) (bool, error) { + block, err := variableBlock(src, name) + if err != nil { + return false, err + } + m := intValue.FindSubmatch(block) + if m == nil { + return false, fmt.Errorf("%s: no number default", name) + } + return string(m[1]) == "0", nil +} + // variableBlock is the text of variable "name" up to the next variable. func variableBlock(src []byte, name string) ([]byte, error) { all := variable.FindAllSubmatchIndex(src, -1) @@ -191,23 +227,28 @@ func variableBlock(src []byte, name string) ([]byte, error) { return nil, fmt.Errorf("no variable %q", name) } +// row is one instance type of a region's list. +type row struct { + Price string `json:"price"` + VCPU string `json:"vCPU"` + Memory string `json:"Memory"` + Type string `json:"Instance Type"` + Storage string `json:"Storage"` +} + // list is one region's price list as the pricing page loads it: a manifest with its // publication date, and the region's prices keyed " Linux". type list struct { Manifest struct { Published string `json:"hawkFilePublicationDate"` } `json:"manifest"` - Regions map[string]map[string]struct { - Price string `json:"price"` - VCPU string `json:"vCPU"` - Memory string `json:"Memory"` - Type string `json:"Instance Type"` - } `json:"regions"` + Regions map[string]map[string]row `json:"regions"` } -// build reads each region's list and keeps the machines' prices. A machine the list does not -// price fails: a type the module starts that a region lacks is for the module to fix, not a -// blank on the page. +// build reads each region's list and keeps the machines' prices: the control plane's and the +// proxy's, and every type a runner may be there. A machine the list does not price fails, and so +// does a runner's defaults that match no type every region prices: a type the module starts that +// a region lacks is for the module to fix, not a blank on the page. func build(m Machines, lists string, now time.Time) (Record, error) { r := Record{Generated: now.UTC().Format("2006-01-02"), Source: fmt.Sprintf(listURL, ""), Machines: m, Regions: map[string]Region{}} codes := make([]string, 0, len(regionNames)) @@ -215,6 +256,7 @@ func build(m Machines, lists string, now time.Time) (Record, error) { codes = append(codes, code) } sort.Strings(codes) + rows := map[string]map[string]row{} for _, code := range codes { raw, err := fetch(code, lists) if err != nil { @@ -234,33 +276,80 @@ func build(m Machines, lists string, now time.Time) (Record, error) { if !ok { return Record{}, fmt.Errorf("%s's list holds no region %q", code, regionNames[code]) } + rows[code] = map[string]row{} + for _, p := range prices { + rows[code][p.Type] = p + } + } + runners, err := runnerTypes(m, codes, rows) + if err != nil { + return Record{}, err + } + r.Runners = runners + for _, code := range codes { region := Region{} - for _, t := range append([]string{m.ControlPlane, m.Proxy}, m.Runners...) { - p, ok := prices[key(t, regionNames[code])] + for _, t := range append([]string{m.ControlPlane, m.Proxy}, runners...) { + p, ok := rows[code][t] if !ok { return Record{}, fmt.Errorf("%s: no on-demand price for %s, which the module starts by default", code, t) } - hourly, err := strconv.ParseFloat(p.Price, 64) + price, err := priced(code, p) if err != nil { - return Record{}, fmt.Errorf("%s %s: price %q: %w", code, t, p.Price, err) + return Record{}, err } - vcpu, err := strconv.Atoi(p.VCPU) - if err != nil { - return Record{}, fmt.Errorf("%s %s: vCPU %q: %w", code, t, p.VCPU, err) - } - region[t] = Price{Hourly: hourly, VCPU: vcpu, Memory: p.Memory} + region[t] = price } r.Regions[code] = region } return r, nil } -// key is how the list names an instance type's row: the family and size apart, the region's -// name without its parentheses, then the operating system. -func key(instanceType, region string) string { - family, size, _ := strings.Cut(instanceType, ".") - clean := strings.NewReplacer("(", "", ")", "").Replace(region) - return family + " " + size + " " + clean + " Linux" +// runnerTypes is every type m's runner defaults match - a name or pattern of RunnerTypes, within +// RunnerVCPUs, with a local NVMe where RunnerLocalDisk says - that every region prices, the +// cheapest first in the first region. +func runnerTypes(m Machines, codes []string, rows map[string]map[string]row) ([]string, error) { + first := rows[codes[0]] + var types []string + for t, p := range first { + if !slices.ContainsFunc(m.RunnerTypes, func(pattern string) bool { ok, _ := path.Match(pattern, t); return ok }) { + continue + } + vcpu, err := strconv.Atoi(p.VCPU) + if err != nil || vcpu < m.RunnerVCPUs[0] || vcpu > m.RunnerVCPUs[1] { + continue + } + if m.RunnerLocalDisk && (p.Storage == "" || p.Storage == "EBS only") { + continue + } + if slices.ContainsFunc(codes, func(code string) bool { _, ok := rows[code][t]; return !ok }) { + continue + } + types = append(types, t) + } + if len(types) == 0 { + return nil, fmt.Errorf("no type every region prices matches the runners' defaults %v, %d to %d vCPUs", m.RunnerTypes, m.RunnerVCPUs[0], m.RunnerVCPUs[1]) + } + hourly := func(t string) float64 { h, _ := strconv.ParseFloat(first[t].Price, 64); return h } + sort.Slice(types, func(a, b int) bool { + if ha, hb := hourly(types[a]), hourly(types[b]); ha != hb { + return ha < hb + } + return types[a] < types[b] + }) + return types, nil +} + +// priced is a list's row as the record keeps it. +func priced(code string, p row) (Price, error) { + hourly, err := strconv.ParseFloat(p.Price, 64) + if err != nil { + return Price{}, fmt.Errorf("%s %s: price %q: %w", code, p.Type, p.Price, err) + } + vcpu, err := strconv.Atoi(p.VCPU) + if err != nil { + return Price{}, fmt.Errorf("%s %s: vCPU %q: %w", code, p.Type, p.VCPU, err) + } + return Price{Hourly: hourly, VCPU: vcpu, Memory: p.Memory}, nil } func fetch(code, lists string) ([]byte, error) { @@ -303,6 +392,9 @@ func checkRecord(path string, m Machines, now time.Time) error { if string(have) != string(want) { return fmt.Errorf("%s prices %s, and the module starts %s by default: run task pricing", path, have, want) } + if len(r.Runners) == 0 { + return fmt.Errorf("%s names no type a runner may be: run task pricing", path) + } for code := range regionNames { if _, ok := r.Regions[code]; !ok { return fmt.Errorf("%s has no prices for %s: run task pricing", path, code) diff --git a/tools/pricing/pricing_test.go b/tools/pricing/pricing_test.go index 36377fa..65e05e4 100644 --- a/tools/pricing/pricing_test.go +++ b/tools/pricing/pricing_test.go @@ -5,13 +5,15 @@ import ( "encoding/json" "os" "path/filepath" + "slices" "strings" "testing" "time" ) -// module is a spin-terraform-aws checkout with the three defaults the record prices. -func module(t *testing.T, runnerTypes string) string { +// module is a spin-terraform-aws checkout with the defaults the record prices: runnerTypes as +// instance_types' list, and dataVolume as data_volume_gb's. +func module(t *testing.T, runnerTypes, dataVolume string) string { t.Helper() root := t.TempDir() write := func(path, text string) { @@ -24,30 +26,44 @@ func module(t *testing.T, runnerTypes string) string { } } write("modules/controlplane/variables.tf", "variable \"domain\" {\n type = string\n}\n\nvariable \"instance_type\" {\n description = \"The machine.\"\n type = string\n default = \"t8i.medium\"\n}\n\nvariable \"proxy_instance_type\" {\n default = \"t8i.micro\"\n}\n") - write("modules/runners/variables.tf", "variable \"instance_types\" {\n type = list(string)\n default = ["+runnerTypes+"]\n}\n\nvariable \"max_hosts\" {\n type = number\n default = 3\n}\n") + write("modules/runners/variables.tf", "variable \"instance_types\" {\n type = list(string)\n default = ["+runnerTypes+"]\n}\n\n"+ + "variable \"vcpus\" {\n type = object({ min = number, max = number })\n default = { min = 32, max = 48 }\n}\n\n"+ + "variable \"data_volume_gb\" {\n type = number\n default = "+dataVolume+"\n}\n\n"+ + "variable \"max_hosts\" {\n type = number\n default = 3\n}\n") return root } // lists is a directory of each region's list as the pricing page loads it, with the machines' -// rows and others. -func lists(t *testing.T, published string) string { +// rows and others; extra are rows one region alone has. +func lists(t *testing.T, published string, extra map[string][4]string) string { t.Helper() dir := t.TempDir() - row := func(instanceType, price, vcpu, memory string) map[string]string { - return map[string]string{"price": price, "vCPU": vcpu, "Memory": memory, "Instance Type": instanceType} - } for code, name := range regionNames { clean := strings.NewReplacer("(", "", ")", "").Replace(name) - l := map[string]any{ - "manifest": map[string]string{"hawkFilePublicationDate": published}, - "regions": map[string]any{name: map[string]any{ - "t8i medium " + clean + " Linux": row("t8i.medium", "0.0499200000", "2", "4.0 GiB"), - "t8i micro " + clean + " Linux": row("t8i.micro", "0.0124800000", "2", "1.0 GiB"), - "m8id 8xlarge " + clean + " Linux": row("m8id.8xlarge", "2.0883200000", "32", "128 GiB"), - "g6 16xlarge " + clean + " Linux": row("g6.16xlarge", "9", "64", "256 GiB"), - }}, + prices := map[string]any{} + add := func(instanceType, price, vcpu, memory, storage string) { + family, size, _ := strings.Cut(instanceType, ".") + prices[family+" "+size+" "+clean+" Linux"] = map[string]string{ + "price": price, "vCPU": vcpu, "Memory": memory, "Instance Type": instanceType, "Storage": storage, + } + } + add("t8i.medium", "0.0499200000", "2", "4.0 GiB", "EBS only") + add("t8i.micro", "0.0124800000", "2", "1.0 GiB", "EBS only") + add("m8id.8xlarge", "2.0883200000", "32", "128 GiB", "1 x 1900 NVMe SSD") + add("c8id.12xlarge", "2.6611200000", "48", "96 GiB", "1 x 2850 NVMe SSD") + add("c8id.8xlarge", "1.7740800000", "32", "64 GiB", "1 x 1900 NVMe SSD") + add("c8id.16xlarge", "3.5481600000", "64", "128 GiB", "1 x 3800 NVMe SSD") + add("c8i.8xlarge", "1.4995200000", "32", "64 GiB", "EBS only") + add("g6.16xlarge", "9", "64", "256 GiB", "1 x 1900 NVMe SSD") + if code == "us-west-2" { + for t, r := range extra { + add(t, r[0], r[1], r[2], r[3]) + } } - b, err := json.Marshal(l) + b, err := json.Marshal(map[string]any{ + "manifest": map[string]string{"hawkFilePublicationDate": published}, + "regions": map[string]any{name: prices}, + }) if err != nil { t.Fatal(err) } @@ -58,13 +74,31 @@ func lists(t *testing.T, published string) string { return dir } +// record writes the record of tf's defaults against ls and reads it back. +func record(t *testing.T, tf, ls string) (Record, error) { + t.Helper() + out := filepath.Join(t.TempDir(), "pricing.json") + if err := run([]string{"-terraform", tf, "-lists", ls, "-out", out}, &bytes.Buffer{}); err != nil { + return Record{}, err + } + raw, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + var r Record + if err := json.Unmarshal(raw, &r); err != nil { + t.Fatal(err) + } + return r, nil +} + // The record prices the machines the module starts by default, in each region, dated by the // day it was written and by AWS's publication; a machine the lists do not price fails rather // than leaving a blank. func TestTheRecordPricesTheModulesDefaults(t *testing.T) { out := filepath.Join(t.TempDir(), "pricing.json") var stdout bytes.Buffer - if err := run([]string{"-terraform", module(t, `"m8id.8xlarge"`), "-lists", lists(t, "2026-10-23T22:13:55Z"), "-out", out}, &stdout); err != nil { + if err := run([]string{"-terraform", module(t, `"m8id.8xlarge"`, "0"), "-lists", lists(t, "2026-10-23T22:13:55Z", nil), "-out", out}, &stdout); err != nil { t.Fatal(err) } if got := stdout.String(); got != "pricing: 2 regions, lists published 2026-10-23T22:13:55Z\n" { @@ -81,7 +115,7 @@ func TestTheRecordPricesTheModulesDefaults(t *testing.T) { if r.Generated != time.Now().UTC().Format("2006-01-02") || r.Published != "2026-10-23T22:13:55Z" { t.Errorf("dated %s, lists %s", r.Generated, r.Published) } - want := Machines{ControlPlane: "t8i.medium", Proxy: "t8i.micro", Runners: []string{"m8id.8xlarge"}, MaxRunners: 3} + want := Machines{ControlPlane: "t8i.medium", Proxy: "t8i.micro", RunnerTypes: []string{"m8id.8xlarge"}, RunnerVCPUs: [2]int{32, 48}, RunnerLocalDisk: true, MaxRunners: 3} if got, _ := json.Marshal(r.Machines); string(got) != string(must(json.Marshal(want))) { t.Errorf("machines %s", got) } @@ -94,9 +128,41 @@ func TestTheRecordPricesTheModulesDefaults(t *testing.T) { } } - err = run([]string{"-terraform", module(t, `"m8id.8xlarge", "c8id.8xlarge"`), "-lists", lists(t, "2026-10-23T22:13:55Z"), "-out", out}, &stdout) - if err == nil || !strings.Contains(err.Error(), "no on-demand price for c8id.8xlarge") { - t.Errorf("a machine the list does not price: %v", err) + _, err = record(t, module(t, `"m9id.8xlarge"`, "0"), lists(t, "2026-10-23T22:13:55Z", nil)) + if err == nil || !strings.Contains(err.Error(), "no type every region prices matches") { + t.Errorf("a runner's type the list does not price: %v", err) + } +} + +// A runner's type is every one the module's names and patterns match within its vCPUs - with a +// local NVMe while the data is on the instance store, or any disk with a data volume - that every +// region prices, the cheapest first. +func TestTheRunnersAreTheTypesTheDefaultsMatch(t *testing.T) { + onlyWest := map[string][4]string{"c8id.10xlarge": {"2.2", "40", "80 GiB", "1 x 2400 NVMe SSD"}} + for _, tc := range []struct { + name, types, dataVolume string + want []string + }{ + {"on the instance store", `"c8i.*", "c8id.*", "m8id.*"`, "0", []string{"c8id.8xlarge", "m8id.8xlarge", "c8id.12xlarge"}}, + {"on a data volume", `"c8i.*", "c8id.*"`, "100", []string{"c8i.8xlarge", "c8id.8xlarge", "c8id.12xlarge"}}, + {"a name beside a pattern", `"m8id.8xlarge", "c8id.*"`, "0", []string{"c8id.8xlarge", "m8id.8xlarge", "c8id.12xlarge"}}, + } { + t.Run(tc.name, func(t *testing.T) { + r, err := record(t, module(t, tc.types, tc.dataVolume), lists(t, "2026-10-23T22:13:55Z", onlyWest)) + if err != nil { + t.Fatal(err) + } + if !slices.Equal(r.Runners, tc.want) { + t.Errorf("runners %v, want %v", r.Runners, tc.want) + } + for _, code := range []string{"us-east-1", "us-west-2"} { + for _, rt := range tc.want { + if _, ok := r.Regions[code][rt]; !ok { + t.Errorf("%s does not price %s", code, rt) + } + } + } + }) } } @@ -104,17 +170,21 @@ func TestTheRecordPricesTheModulesDefaults(t *testing.T) { // machines than the module starts now, is a record to rewrite. func TestAStaleOrMovedRecordFails(t *testing.T) { out := filepath.Join(t.TempDir(), "pricing.json") - tf := module(t, `"m8id.8xlarge"`) - if err := run([]string{"-terraform", tf, "-lists", lists(t, "2026-10-23T22:13:55Z"), "-out", out}, &bytes.Buffer{}); err != nil { + tf := module(t, `"m8id.*"`, "0") + if err := run([]string{"-terraform", tf, "-lists", lists(t, "2026-10-23T22:13:55Z", nil), "-out", out}, &bytes.Buffer{}); err != nil { t.Fatal(err) } if err := run([]string{"-terraform", tf, "-out", out, "-check"}, &bytes.Buffer{}); err != nil { t.Errorf("a record written today, of the module's machines: %v", err) } - moved := module(t, `"c8id.8xlarge"`) - err := run([]string{"-terraform", moved, "-out", out, "-check"}, &bytes.Buffer{}) - if err == nil || !strings.Contains(err.Error(), "the module starts") { - t.Errorf("the module's defaults moved: %v", err) + for name, moved := range map[string]string{ + "the types": module(t, `"c8id.*"`, "0"), + "the data disk": module(t, `"m8id.*"`, "100"), + } { + err := run([]string{"-terraform", moved, "-out", out, "-check"}, &bytes.Buffer{}) + if err == nil || !strings.Contains(err.Error(), "the module starts") { + t.Errorf("the module's defaults moved, %s: %v", name, err) + } } m, err := defaults(tf) From fd5c3dda5626e441fd7652f5ec61e8b8c1eac676 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Mon, 26 Oct 2026 22:33:42 -0300 Subject: [PATCH 5/5] sources: spin-terraform-aws v20261027.01, which serves term., agents. and id. itself The release that drops modules/terminal and modules/identity-issuer and gives the proxy agents. too: the prerequisites name agents. with the installation's other names, and the reference, the prices and the release's pages are what task reference and task pricing write from it. Co-Authored-By: Claude Opus 5.5 --- src/content/docs/aws/overview.mdx | 4 +- src/content/docs/aws/prerequisites.md | 3 +- src/content/docs/aws/setup.md | 2 +- src/content/docs/quickstart.md | 2 +- src/content/docs/reference/release.md | 14 +++---- .../docs/reference/terraform/bootstrap.md | 26 ++++++++++++- .../docs/reference/terraform/controlplane.md | 13 +++---- .../docs/reference/terraform/images.md | 6 +-- src/content/docs/reference/terraform/root.md | 14 +++++-- .../docs/reference/terraform/runners.md | 38 ++++++++++++------- src/content/docs/use/cli.md | 2 +- src/data/figures.json | 2 +- src/data/pricing.json | 38 +++++++++++++++---- src/data/reference.json | 8 ++-- versions.yaml | 4 +- 15 files changed, 122 insertions(+), 54 deletions(-) diff --git a/src/content/docs/aws/overview.mdx b/src/content/docs/aws/overview.mdx index 1a4f8c4..b70dab6 100644 --- a/src/content/docs/aws/overview.mdx +++ b/src/content/docs/aws/overview.mdx @@ -17,8 +17,8 @@ module, [spin-terraform-aws](https://github.com/spin-stack/spin-terraform-aws): ```hcl module "spin" { - source = "github.com/spin-stack/spin-terraform-aws?ref=v20261025.01" - spin_version = "v20261025.01" + source = "github.com/spin-stack/spin-terraform-aws?ref=v20261027.01" + spin_version = "v20261026.02" domain = "spin.example.com" } ``` diff --git a/src/content/docs/aws/prerequisites.md b/src/content/docs/aws/prerequisites.md index de3045f..19453e6 100644 --- a/src/content/docs/aws/prerequisites.md +++ b/src/content/docs/aws/prerequisites.md @@ -27,6 +27,7 @@ zone in Route 53, and you point the domain's nameservers at it. The installation | `app.` | The dashboard and the API | | `tunnel.app.` | The relay runners connect to | | `term.` | The browser terminal: an SSH client for a workspace, in the browser, that signs in with a passkey | +| `agents.` | The agent's client the browser terminal opens a workspace's agent in | | `id.` | The identity issuer: what a system outside Spinbox verifies a workspace's identity token, or a commit it signed, with | | `*.ws.` | Workspaces' ports | @@ -72,4 +73,4 @@ your own instead, name them with [`image_ids`](/reference/terraform/root/#image_ ## Pick a release Releases are named by date: `v.`, the Nth release of that day. This site documents -`v20261025.01`. See [github.com/spin-stack/spin/releases](https://github.com/spin-stack/spin/releases). +`v20261026.02`. See [github.com/spin-stack/spin/releases](https://github.com/spin-stack/spin/releases). diff --git a/src/content/docs/aws/setup.md b/src/content/docs/aws/setup.md index 8276e75..a80a527 100644 --- a/src/content/docs/aws/setup.md +++ b/src/content/docs/aws/setup.md @@ -35,7 +35,7 @@ $(tofu -chdir=../../bootstrap output -raw init) That runs `tofu init` against the state bucket. Then write `terraform.tfvars`: ```hcl -spin_version = "v20261025.01" +spin_version = "v20261026.02" domain = "spin.example.com" # name = "spin" # a prefix for every resource; one installation per name # region = "us-west-2" # the region bootstrap was applied in diff --git a/src/content/docs/quickstart.md b/src/content/docs/quickstart.md index cf6263d..bf6069d 100644 --- a/src/content/docs/quickstart.md +++ b/src/content/docs/quickstart.md @@ -25,7 +25,7 @@ spin version The Linux build is also in the release's public package: ```bash -oras pull ghcr.io/spin-stack/spin-release:v20261025.01-spin-linux-amd64 +oras pull ghcr.io/spin-stack/spin-release:v20261026.02-spin-linux-amd64 ``` ## 2. Sign in diff --git a/src/content/docs/reference/release.md b/src/content/docs/reference/release.md index 2c83508..043d47a 100644 --- a/src/content/docs/reference/release.md +++ b/src/content/docs/reference/release.md @@ -9,15 +9,15 @@ sidebar: -An installation runs Spinbox `v20261025.01`, the newest release spin-terraform-aws's `images.json` has images of. It is made of: +An installation runs Spinbox `v20261026.02`, the newest release spin-terraform-aws's `images.json` has images of. It is made of: | Part | Release | Read from | |---|---|---| -| spin: the control plane, the runner, the proxy and the CLI | `v20261025.01` | spin-terraform-aws's `images.json` | -| spin-machine: QEMU, the guest kernel, the firmware and the base image a workspace boots, as one artefact | [`v20261014.01`](https://github.com/spin-stack/spin-machine/releases/tag/v20261014.01) | spin's `versions.yaml` at `v20261025.01` | -| The OpenTofu module, spin-terraform-aws | [`v20261025.01`](https://github.com/spin-stack/spin-terraform-aws/releases/tag/v20261025.01) | its newest release tag | +| spin: the control plane, the runner, the proxy and the CLI | `v20261026.02` | spin-terraform-aws's `images.json` | +| spin-machine: QEMU, the guest kernel, the firmware and the base image a workspace boots, as one artefact | [`v20261023.01`](https://github.com/spin-stack/spin-machine/releases/tag/v20261023.01) | spin's `versions.yaml` at `v20261026.02` | +| The OpenTofu module, spin-terraform-aws | [`v20261027.01`](https://github.com/spin-stack/spin-terraform-aws/releases/tag/v20261027.01) | its newest release tag | -The versions of QEMU and the guest kernel in spin-machine `v20261014.01` are its own [`versions.yaml`](https://github.com/spin-stack/spin-machine/blob/v20261014.01/versions.yaml) at that tag. The host's kernel is the Spin OS image's: none of the sources this site is generated from records its version. +The versions of QEMU and the guest kernel in spin-machine `v20261023.01` are its own [`versions.yaml`](https://github.com/spin-stack/spin-machine/blob/v20261023.01/versions.yaml) at that tag. The host's kernel is the Spin OS image's: none of the sources this site is generated from records its version. ## Spin OS images @@ -25,5 +25,5 @@ Each machine boots the image of its role, in a region that has all three ([Spin | Region | Control plane | Proxy | Runner | |---|---|---|---| -| `us-east-1` | `ami-00cb2d0feffe89588` | `ami-0ac882e2869f704f9` | `ami-090fbe7203af7a8bd` | -| `us-west-2` | `ami-0ae9395569c631c81` | `ami-02b2c9c828e4db70d` | `ami-06f1c0680dcd7ea96` | +| `us-east-1` | `ami-0774cba716732d76f` | `ami-0d23fd135c2b1569a` | `ami-04dfd5df7d3bbaf5a` | +| `us-west-2` | `ami-0c0458b4218567425` | `ami-08808879d20e484cc` | `ami-0a2410b4fdd3f37e2` | diff --git a/src/content/docs/reference/terraform/bootstrap.md b/src/content/docs/reference/terraform/bootstrap.md index 227e278..0586f8a 100644 --- a/src/content/docs/reference/terraform/bootstrap.md +++ b/src/content/docs/reference/terraform/bootstrap.md @@ -9,7 +9,7 @@ sidebar: -Source: `github.com/spin-stack/spin-terraform-aws//bootstrap?ref=v20261025.01`. Generated from its variables and outputs. +Source: `github.com/spin-stack/spin-terraform-aws//bootstrap?ref=v20261027.01`. Generated from its variables and outputs. ## Variables @@ -18,8 +18,11 @@ Source: `github.com/spin-stack/spin-terraform-aws//bootstrap?ref=v20261025.01`. | [`cost_allocation_tags`](#cost_allocation_tags) | `bool` | `false` | | [`github_repositories`](#github_repositories) | `map(string)` | `{}` | | [`region`](#region) | `string` | `"us-west-2"` | +| [`tags`](#tags) | `map(string)` | `{}` | | [`image_publishers`](#image_publishers) | `map(string)` | `{}` | | [`publish_regions`](#publish_regions) | `list(string)` | `[]` | +| [`public_images`](#public_images) | `bool` | `false` | +| [`public_image_quota`](#public_image_quota) | `number` | `15` | ### cost_allocation_tags @@ -42,6 +45,13 @@ The region the installations are in. - Type: `string` - Default: `"us-west-2"` +### tags + +Tags on everything bootstrap creates: what the bill is read by. + +- Type: `map(string)` +- Default: `{}` + ### image_publishers Repositories that publish Spin OS from GitHub Actions, as owner/name => the prefix of their OIDC subject (github.tf): each gets a publish role. @@ -56,6 +66,20 @@ The regions a publish role writes snapshots and registers images in; none is thi - Type: `list(string)` - Default: `[]` +### public_images + +Whether the publish roles make their images public: unblocks public images and raises the quota in each publish region. + +- Type: `bool` +- Default: `false` + +### public_image_quota + +Public AMIs a region, when public_images: three a release, for the releases spin-stack/ami keeps public. + +- Type: `number` +- Default: `15` + ## Outputs | Output | What it is | diff --git a/src/content/docs/reference/terraform/controlplane.md b/src/content/docs/reference/terraform/controlplane.md index 1276236..2325a33 100644 --- a/src/content/docs/reference/terraform/controlplane.md +++ b/src/content/docs/reference/terraform/controlplane.md @@ -9,7 +9,7 @@ sidebar: -Source: `github.com/spin-stack/spin-terraform-aws//modules/controlplane?ref=v20261025.01`. Generated from its variables and outputs. +Source: `github.com/spin-stack/spin-terraform-aws//modules/controlplane?ref=v20261027.01`. Generated from its variables and outputs. ## Variables @@ -23,7 +23,7 @@ Source: `github.com/spin-stack/spin-terraform-aws//modules/controlplane?ref=v202 | [`internal_zone`](#internal_zone) | `string` | `"spin.internal"` | | [`acme_email`](#acme_email) | `string` | `""` | | [`vpc_cidr`](#vpc_cidr) | `string` | `"10.42.0.0/16"` | -| [`availability_zones`](#availability_zones) | `number` | `3` | +| [`availability_zones`](#availability_zones) | `number` | `8` | | [`instance_type`](#instance_type) | `string` | `"t8i.medium"` | | [`root_volume_gb`](#root_volume_gb) | `number` | `40` | | [`admin_email`](#admin_email) | `string` | `""` | @@ -104,10 +104,10 @@ The VPC's range, a /16. One /20 public subnet per availability zone is carved fr ### availability_zones -How many zones to make subnets in. The control plane, the proxy and the runners may each be started in any of them, which is what gives a group somewhere to go when a zone runs out. +How many zones to make subnets in, at most: a region with fewer gets one in each. The control plane, the proxy and the runners may each be started in any of them, which is what gives a group somewhere to go when a zone runs out - and a zone left out is spot pools a runner is never drawn from. A subnet is its zone's by index, so raising this adds subnets and moves none. - Type: `number` -- Default: `3` +- Default: `8` ### instance_type @@ -280,14 +280,13 @@ The builds of each role's image - control-plane, runner and proxy - oldest first | `standard_vcpus` | The on-demand standard vCPUs the control plane's and the proxy's machines take at most: two of each while an update replaces one. The runners module counts them against the quota on-demand runners share. | | `name_servers` | The domain's public zone's nameservers, which its registrar points at. | | `domain` | The base domain; the runners' relay is tunnel.app.<domain>, dialled at relay_dial. | -| `public_zone_id` | The domain's public zone, where modules/identity-issuer puts id.<domain>. | +| `public_zone_id` | The domain's public zone, where the installation's names are. | | `identity_key_arn` | The KMS key the installation's identity key is sealed under, which only an attested runner opens. | -| `identity_documents` | The commands that write the identity issuer's documents - discovery, key set, allowed signers and SPIFFE bundle - for modules/identity-issuer to publish: the identity key as the control plane keeps it in its bucket (its public half is all that is read) and the X.509 CA the first host to sign an SVID made beside it, then the documents from them. Run them with credentials that may read both, and commit what they write. | | `internal_zone_id` | The private zone the components reach each other by, cp.<internal_zone> and proxy.<internal_zone>. | | `relay_dial` | Where a runner dials the relay, host:port: the proxy inside the VPC, by the name the proxy of the moment points at itself. | | `collector` | Where every process of the installation pushes its telemetry, host:port. Where the collector sends it is set in the dashboard. | | `boundary_arn` | The permissions boundary every role of the installation carries; the runners module puts it on the runners' role. | -| `proxy_ip` | The proxy's elastic IP, where app.<domain>, tunnel.app.<domain> and \*.ws.<domain> point: the one address of the installation the internet reaches. | +| `proxy_ip` | The proxy's elastic IP, where every name of the installation's domain points: the one address of the installation the internet reaches. | | `proxy_group` | The proxy's group of one. Its machine, for `aws ssm start-session --target` (it has no SSH): aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names <this> --query 'AutoScalingGroups[0].Instances[0].InstanceId'. | | `bucket` | The volumes' bucket: every workspace's disk, versioned under Object Lock, reached only through the VPC's endpoint. | | `database_bucket` | The database's archive: the base backups and WAL the control plane ships as it writes, and restores from when its machine is replaced. Its role alone reaches it. | diff --git a/src/content/docs/reference/terraform/images.md b/src/content/docs/reference/terraform/images.md index 8ff02a9..be933b8 100644 --- a/src/content/docs/reference/terraform/images.md +++ b/src/content/docs/reference/terraform/images.md @@ -9,9 +9,9 @@ sidebar: -Every machine of an installation boots Spin OS, built for one release of spin. The module reads the image of each machine from its `images.json`, by `spin_version` and region, so an installation of `v20261025.01` can be made in a region listed here and nowhere else. To boot images of your own, name them with `image_ids` and their measurements with `image_measurements` ([the module's variables](/reference/terraform/root/#image_ids)). +Every machine of an installation boots Spin OS, built for one release of spin. The module reads the image of each machine from its `images.json`, by `spin_version` and region, so an installation of `v20261026.02` can be made in a region listed here and nowhere else. To boot images of your own, name them with `image_ids` and their measurements with `image_measurements` ([the module's variables](/reference/terraform/root/#image_ids)). | Region | Control plane | Proxy | Runner | |---|---|---|---| -| `us-east-1` | `ami-00cb2d0feffe89588` | `ami-0ac882e2869f704f9` | `ami-090fbe7203af7a8bd` | -| `us-west-2` | `ami-0ae9395569c631c81` | `ami-02b2c9c828e4db70d` | `ami-06f1c0680dcd7ea96` | +| `us-east-1` | `ami-0774cba716732d76f` | `ami-0d23fd135c2b1569a` | `ami-04dfd5df7d3bbaf5a` | +| `us-west-2` | `ami-0c0458b4218567425` | `ami-08808879d20e484cc` | `ami-0a2410b4fdd3f37e2` | diff --git a/src/content/docs/reference/terraform/root.md b/src/content/docs/reference/terraform/root.md index 5d9508b..86d8b4d 100644 --- a/src/content/docs/reference/terraform/root.md +++ b/src/content/docs/reference/terraform/root.md @@ -9,7 +9,7 @@ sidebar: -Source: `github.com/spin-stack/spin-terraform-aws?ref=v20261025.01`. Generated from its variables and outputs. +Source: `github.com/spin-stack/spin-terraform-aws?ref=v20261027.01`. Generated from its variables and outputs. ## Variables @@ -33,6 +33,7 @@ Source: `github.com/spin-stack/spin-terraform-aws?ref=v20261025.01`. Generated f | [`quiet_hours`](#quiet_hours) | `string` | the module's own | | [`time_zone`](#time_zone) | `string` | the module's own | | [`runner_instance_types`](#runner_instance_types) | `list(string)` | the module's own | +| [`runner_vcpus`](#runner_vcpus) | `object({ min = number, max = number })` | the module's own | | [`runner_spot`](#runner_spot) | `bool` | the module's own | | [`runner_data_volume_gb`](#runner_data_volume_gb) | `number` | the module's own | | [`runner_root_volume_gb`](#runner_root_volume_gb) | `number` | the module's own | @@ -165,11 +166,18 @@ The IANA zone quiet_hours are in; modules/controlplane's default is UTC. ### runner_instance_types -What the runners' group may start, most preferred first, one CPU generation; see modules/runners. +The types the runners' group may start, as names or patterns (c8id.\*), one CPU generation; see modules/runners. - Type: `list(string)` - Default: `null` (the module's own default) +### runner_vcpus + +How many vCPUs a runner may have, { min, max }; modules/runners' default is 32 to 48. + +- Type: `object({ min = number, max = number })` +- Default: `null` (the module's own default) + ### runner_spot Every runner a spot instance; modules/runners' default is true. @@ -248,7 +256,7 @@ Tags on everything both modules create. | `dashboard` | The installation's dashboard. | | `first_sign_in` | The setup page, the first administrator, and the command that reads their one-time password: an SSM SecureString this apply wrote without it ever being in the state. | | `name_servers` | The domain's zone's nameservers: what its registrar, or its parent's zone, points at. | -| `proxy_ip` | The proxy's elastic IP, where app.<domain>, tunnel.app.<domain> and \*.ws.<domain> point: the one address of the installation the internet reaches. | +| `proxy_ip` | The proxy's elastic IP, where every name of the installation's domain points: the one address of the installation the internet reaches. | | `controlplane_group` | The control plane's group of one; its machine is reached with `aws ssm start-session --target <instance>`. | | `update_status` | After an apply that changed a machine: how its replacement went. Successful, InProgress, or RollbackSuccessful with the reason - after which the old machine serves again, at once or, for a control plane that had handed its database over, once its watch has restored it from the archive. | | `update_abort` | After an apply whose new machine will not come up: the command that stops waiting for it - rolls the refresh back and abandons the launch the hook still holds - so the next apply can change the group. One command per group. | diff --git a/src/content/docs/reference/terraform/runners.md b/src/content/docs/reference/terraform/runners.md index 617fcc9..c52b951 100644 --- a/src/content/docs/reference/terraform/runners.md +++ b/src/content/docs/reference/terraform/runners.md @@ -9,14 +9,15 @@ sidebar: -Source: `github.com/spin-stack/spin-terraform-aws//modules/runners?ref=v20261025.01`. Generated from its variables and outputs. +Source: `github.com/spin-stack/spin-terraform-aws//modules/runners?ref=v20261027.01`. Generated from its variables and outputs. ## Variables | Variable | Type | Default | |---|---|---| | [`controlplane`](#controlplane) | `object({ name = string iam_name = string vpc_id = string subnet_ids = list(string) security_group_id = string runner_document_arn = string runner_user_data = string images = map(object({ id = string, root_device = string })) boot_log_policy_arn = string standard_vcpus = number boundary_arn = string })` | **required** | -| [`instance_types`](#instance_types) | `list(string)` | `["m8id.8xlarge","c8id.8xlarge","r8id.8xlarge","m8id.12xlarge","c8id.12xlarge"]` | +| [`instance_types`](#instance_types) | `list(string)` | `["c8i.*","c8id.*","m8i.*","m8id.*","r8i.*","r8id.*"]` | +| [`vcpus`](#vcpus) | `object({ min = number, max = number })` | `{"max":48,"min":32}` | | [`nested_virtualization`](#nested_virtualization) | `bool` | `true` | | [`spot`](#spot) | `bool` | `true` | | [`request_quota`](#request_quota) | `bool` | `true` | @@ -36,21 +37,32 @@ The control plane module's outputs, as they are: its name, where the runners go, ### instance_types -What the group may start. Keep them one CPU generation: a checkpoint records the processor -and its flags as -cpu host showed them (MachineIdentity in spin's internal/runner/vmm), and -resumes only onto a host that shows the same - a workspace suspended at night on one family -and brought back on another cold-boots. Nested virtualization is offered on C8i, M8i, R8i -(and their d variants), C7i, M7i, R7i and I7i; a d variant brings the local NVMe the runner's -data goes on. +The types the group may start, as EC2's allowed_instance_types takes them: names, or patterns +with \* (c8id.\*). The group starts any type they match that has vcpus and the disk data_volume_gb +leaves the data on - a local NVMe while it is 0, so a d variant - and AWS adds the sizes and +types it brings out without this list changing. -For spot, name several: AWS gives a spot machine from the pools a request can draw on, and -one type is one pool per zone. m8id.8xlarge alone scored 3 of 10 in us-west-2 and was reclaimed -within minutes; the five below score 9. How a list scores in a region, before installing: +Keep them one CPU generation: a checkpoint records the processor and its flags as -cpu host +showed them (MachineIdentity in spin's internal/runner/vmm), and resumes only onto a host that +shows the same - a workspace suspended at night on one family and brought back on another +cold-boots. Nested virtualization is offered on C8i, M8i, R8i (and their d variants), C7i, M7i, +R7i and I7i. Not c8i\* for c8i.\*: that matches the flex types too. + +For spot, the more pools a request can draw on the likelier a machine, and one type is one +pool per zone. How the types these match score in a region, before installing: aws ec2 get-spot-placement-scores --target-capacity 1 --region-names <region> \ - --instance-types <the types> + --single-availability-zone --instance-requirements-with-metadata \ + '{"ArchitectureTypes":["x86_64"],"InstanceRequirements":{"VCpuCount":{"Min":32,"Max":48},"MemoryMiB":{"Min":65536},"LocalStorage":"required","AllowedInstanceTypes":[<the types>]}}' - Type: `list(string)` -- Default: `["m8id.8xlarge","c8id.8xlarge","r8id.8xlarge","m8id.12xlarge","c8id.12xlarge"]` +- Default: `["c8i.*","c8id.*","m8i.*","m8id.*","r8i.*","r8id.*"]` + +### vcpus + +How many vCPUs a runner may have, at least and at most: each type instance_types matches within it is a pool the group may draw on. max is also what the quota is sized by (quota.tf). + +- Type: `object({ min = number, max = number })` +- Default: `{"max":48,"min":32}` ### nested_virtualization diff --git a/src/content/docs/use/cli.md b/src/content/docs/use/cli.md index 216b7e5..53d95a3 100644 --- a/src/content/docs/use/cli.md +++ b/src/content/docs/use/cli.md @@ -14,7 +14,7 @@ machine, make it executable and put it on your `PATH` as `spin`. The Linux build release's public package: ```bash -oras pull ghcr.io/spin-stack/spin-release:v20261025.01-spin-linux-amd64 +oras pull ghcr.io/spin-stack/spin-release:v20261026.02-spin-linux-amd64 ``` `spin version` prints the release it was built from, and the CLI says it on every request. An diff --git a/src/data/figures.json b/src/data/figures.json index c59b8b6..ffcf46b 100644 --- a/src/data/figures.json +++ b/src/data/figures.json @@ -110,7 +110,7 @@ ] }, "installation": { - "source": "https://github.com/spin-stack/spin-terraform-aws/blob/488dc2db08ac7d0c177e5a7355902c258446342f/docs/diagrams/installation", + "source": "https://github.com/spin-stack/spin-terraform-aws/blob/307060ab855fb648811aaf450e13db808f33f4a3/docs/diagrams/installation", "width": 0, "words": [ "AWS", diff --git a/src/data/pricing.json b/src/data/pricing.json index 9f5fef1..cb76153 100644 --- a/src/data/pricing.json +++ b/src/data/pricing.json @@ -1,19 +1,33 @@ { - "generated": "2026-10-26", + "generated": "2026-10-27", "published": "2026-10-23T22:13:55Z", "source": "https://b0.p.awsstatic.com/pricing/2.0/meteredUnitMaps/ec2/USD/current/ec2-ondemand-without-sec-sel/\u003cregion\u003e/Linux/index.json", "machines": { "controlPlane": "t8i.medium", "proxy": "t8i.micro", - "runners": [ - "m8id.8xlarge", - "c8id.8xlarge", - "r8id.8xlarge", - "m8id.12xlarge", - "c8id.12xlarge" + "runnerTypes": [ + "c8i.*", + "c8id.*", + "m8i.*", + "m8id.*", + "r8i.*", + "r8id.*" ], + "runnerVcpus": [ + 32, + 48 + ], + "runnerLocalDisk": true, "maxRunners": 1 }, + "runners": [ + "c8id.8xlarge", + "m8id.8xlarge", + "c8id.12xlarge", + "r8id.8xlarge", + "m8id.12xlarge", + "r8id.12xlarge" + ], "regions": { "us-east-1": { "c8id.12xlarge": { @@ -36,6 +50,11 @@ "vcpu": 32, "memory": "128 GiB" }, + "r8id.12xlarge": { + "hourly": 3.99168, + "vcpu": 48, + "memory": "384 GiB" + }, "r8id.8xlarge": { "hourly": 2.66112, "vcpu": 32, @@ -73,6 +92,11 @@ "vcpu": 32, "memory": "128 GiB" }, + "r8id.12xlarge": { + "hourly": 3.99168, + "vcpu": 48, + "memory": "384 GiB" + }, "r8id.8xlarge": { "hourly": 2.66112, "vcpu": 32, diff --git a/src/data/reference.json b/src/data/reference.json index 0ee7dfa..cb4ac2e 100644 --- a/src/data/reference.json +++ b/src/data/reference.json @@ -1,13 +1,13 @@ { "spin": { - "installs": "v20261025.01", + "installs": "v20261026.02", "commit": "c02ffc3acfd56cb242d21547cb177da68a3d4f11", "tag": "v20261026.02", - "machine": "v20261014.01" + "machine": "v20261023.01" }, "terraform": { - "commit": "488dc2db08ac7d0c177e5a7355902c258446342f", - "release": "v20261025.01" + "commit": "307060ab855fb648811aaf450e13db808f33f4a3", + "release": "v20261027.01" }, "brand": { "commit": "31d07eb9b76596695a138ff96403d881f358e80d" diff --git a/versions.yaml b/versions.yaml index 81c0e95..6ec8a16 100644 --- a/versions.yaml +++ b/versions.yaml @@ -18,8 +18,8 @@ - name: spin-terraform-aws kind: git source: https://github.com/spin-stack/spin-terraform-aws - version: v20261025.01 - pin: 488dc2db08ac7d0c177e5a7355902c258446342f + version: v20261027.01 + pin: 307060ab855fb648811aaf450e13db808f33f4a3 track: github-release note: >- The module the AWS pages describe and pin, and images.json, the releases an installation can