diff --git a/ci/builder/Dockerfile b/ci/builder/Dockerfile index 4e8958f9c1580..5fdf51503bf5b 100644 --- a/ci/builder/Dockerfile +++ b/ci/builder/Dockerfile @@ -172,7 +172,7 @@ COPY nodesource.asc . RUN gpg --dearmor < nodesource.asc > /etc/apt/keyrings/nodesource.gpg \ && rm nodesource.asc \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ && apt-get update \ && apt-get install -y --no-install-recommends nodejs \ && apt-get clean \ @@ -496,7 +496,7 @@ COPY nodesource.asc . RUN gpg --dearmor < nodesource.asc > /etc/apt/keyrings/nodesource.gpg \ && rm nodesource.asc \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ && apt-get update \ && apt-get install -y --no-install-recommends nodejs \ && apt-get clean \ diff --git a/misc/completions/bash/_scratch b/misc/completions/bash/_scratch index acc6c92cbd689..4e30d93de4e86 100644 --- a/misc/completions/bash/_scratch +++ b/misc/completions/bash/_scratch @@ -4,7 +4,7 @@ _shtab_scratch_subparsers=('login' 'create' 'list' 'ls' 'ssh' 'sftp' 'destroy' ' _shtab_scratch_option_strings=('-h' '--help') _shtab_scratch_login_option_strings=('-h' '--help') -_shtab_scratch_create_option_strings=('-h' '--help' '--key-name' '--security-group-name' '--extra-tags' '--instance-profile' '--output-format' '--git-rev' '--ssh' '--max-age-days') +_shtab_scratch_create_option_strings=('-h' '--help' '--key-name' '--security-group-name' '--extra-tags' '--instance-profile' '--output-format' '--git-rev' '--ssh' '--instance-type' '--size-gb' '--max-age-days') _shtab_scratch_list_option_strings=('-h' '--help' '--all' '--output-format') _shtab_scratch_ls_option_strings=('-h' '--help' '--all' '--output-format') _shtab_scratch_ssh_option_strings=('-h' '--help') @@ -14,8 +14,8 @@ _shtab_scratch_rm_option_strings=('-h' '--help' '--all-mine' '-y' '--yes' '--out _shtab_scratch_push_option_strings=('-h' '--help' '--rev') _shtab_scratch_completion_option_strings=('-h' '--help') _shtab_scratch_forward_option_strings=('-h' '--help') -_shtab_scratch_go_option_strings=('-h' '--help' '--max-age-days') -_shtab_scratch_claude_option_strings=('-h' '--help' '--max-age-days') +_shtab_scratch_go_option_strings=('-h' '--help' '--instance-type' '--size-gb' '--max-age-days') +_shtab_scratch_claude_option_strings=('-h' '--help' '--instance-type' '--size-gb' '--max-age-days') diff --git a/misc/completions/zsh/_scratch b/misc/completions/zsh/_scratch index 07af1c5d4be9e..b58d53280c127 100644 --- a/misc/completions/zsh/_scratch +++ b/misc/completions/zsh/_scratch @@ -31,6 +31,8 @@ _shtab_scratch_defaults_added=0 _shtab_scratch_claude_options=( "(- : *)"{-h,--help}"[show this help message and exit]" + "--instance-type[Override the EC2 instance type. The AMI is derived from its architecture. May be used without a machine config.]:instance_type:" + "--size-gb[Override the root volume size in GiB.]:size_gb:" "--max-age-days[Maximum age for scratch instance in days. Defaults to 1.5]:max_age_days:" ":Machine config name from misc\/scratch (e.g. dev-box).:" ) @@ -56,6 +58,8 @@ _shtab_scratch_create_options=( "--output-format[]:output_format:(table csv)" "--git-rev[Git revision of \`materialize\` codebase to push to scratch instance. Defaults to \`HEAD\`]:git_rev:" "--ssh[ssh into the machine after the launch script is run. Only works if a single instance was started]" + "--instance-type[Override the EC2 instance type. The AMI is derived from its architecture. May be used without a machine config.]:instance_type:" + "--size-gb[Override the root volume size in GiB.]:size_gb:" "--max-age-days[Maximum age for scratch instance in days. Defaults to 1.5]:max_age_days:" ":Use a config from \{machine\}.json in \`misc\/scratch\`. Hint\: \`dev-box\` is a good starter\!:" ) @@ -85,6 +89,8 @@ _shtab_scratch_forward_defaults_added=0 _shtab_scratch_go_options=( "(- : *)"{-h,--help}"[show this help message and exit]" + "--instance-type[Override the EC2 instance type. The AMI is derived from its architecture. May be used without a machine config.]:instance_type:" + "--size-gb[Override the root volume size in GiB.]:size_gb:" "--max-age-days[Maximum age for scratch instance in days. Defaults to 1.5]:max_age_days:" ":Machine config name from misc\/scratch (e.g. dev-box). Used only when creating a new instance.:" ) diff --git a/misc/python/materialize/cli/scratch/claude.py b/misc/python/materialize/cli/scratch/claude.py index 38960c9bb1c4e..641795764c58e 100644 --- a/misc/python/materialize/cli/scratch/claude.py +++ b/misc/python/materialize/cli/scratch/claude.py @@ -13,10 +13,12 @@ import datetime from materialize.cli.scratch import check_required_vars, list_all_instances -from materialize.cli.scratch.create import MAX_AGE_DAYS, multi_json, pick_machine +from materialize.cli.scratch.create import ( + MAX_AGE_DAYS, + load_machine_descs, + pick_machine, +) from materialize.scratch import ( - MZ_ROOT, - MachineDesc, launch_cluster, mssh, print_instances, @@ -32,6 +34,19 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: default=None, help="Machine config name from misc/scratch (e.g. dev-box).", ) + parser.add_argument( + "--instance-type", + type=str, + help=( + "Override the EC2 instance type. The AMI is derived from its " + "architecture. May be used without a machine config." + ), + ) + parser.add_argument( + "--size-gb", + type=int, + help="Override the root volume size in GiB.", + ) parser.add_argument( "--max-age-days", type=float, @@ -41,10 +56,18 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: def run(args: argparse.Namespace) -> None: - machine = args.machine or pick_machine() - - with open(MZ_ROOT / "misc" / "scratch" / f"{machine}.json") as f: - descs = [MachineDesc.model_validate(obj) for obj in multi_json(f.read())] + # Resolve the machine config name eagerly so it can be shown below, unless + # the user supplied a bare instance type instead. + machine = args.machine + if machine is None and args.instance_type is None: + machine = pick_machine() + label = machine or args.instance_type + + descs = load_machine_descs( + machine, + instance_type=args.instance_type, + size_gb=args.size_gb, + ) if len(descs) != 1: raise RuntimeError( @@ -61,7 +84,7 @@ def run(args: argparse.Namespace) -> None: if active: print_instances(active, numbered=True) - print(f" {len(active) + 1}) Create new instance ({machine})") + print(f" {len(active) + 1}) Create new instance ({label})") while True: choice = input("Select an instance or create new [#]: ").strip() try: @@ -79,13 +102,13 @@ def run(args: argparse.Namespace) -> None: instance = None if instance is None: - say(f"Creating from {machine}...") + say(f"Creating from {label}...") max_age = datetime.timedelta(days=args.max_age_days) extra_tags = {"LaunchedBy": whoami()} instances = launch_cluster( descs, extra_tags=extra_tags, - delete_after=datetime.datetime.utcnow() + max_age, + delete_after=datetime.datetime.now(datetime.timezone.utc) + max_age, ) print("Launched:") diff --git a/misc/python/materialize/cli/scratch/create.py b/misc/python/materialize/cli/scratch/create.py index 6c557814cd432..69a2556a6de62 100644 --- a/misc/python/materialize/cli/scratch/create.py +++ b/misc/python/materialize/cli/scratch/create.py @@ -21,6 +21,7 @@ launch_cluster, mssh, print_instances, + render_table, whoami, ) @@ -36,8 +37,6 @@ def _natsort_key(s: str) -> list: def pick_machine() -> str: """Show available machine configs and let the user pick one.""" - from prettytable import PrettyTable - scratch_dir = MZ_ROOT / "misc" / "scratch" configs: list[tuple[str, str, str]] = [] for f in sorted(scratch_dir.glob("*.json"), key=lambda f: _natsort_key(f.stem)): @@ -46,12 +45,13 @@ def pick_machine() -> str: for d in descs: configs.append((f.stem, d.instance_type, d.name)) - pt = PrettyTable() - pt.field_names = ["#", "Config", "Instance Type", "Name"] - pt.align = "l" - for i, (stem, instance_type, name) in enumerate(configs, 1): - pt.add_row([i, stem, instance_type, name]) - print(pt) + render_table( + ["#", "CONFIG", "INSTANCE TYPE", "NAME"], + [ + [str(i), stem, instance_type, name] + for i, (stem, instance_type, name) in enumerate(configs, 1) + ], + ) while True: choice = input("Select a machine config [#]: ").strip() @@ -92,6 +92,44 @@ def multi_json(s: str) -> list[dict[Any, Any]]: return result +def load_machine_descs( + machine: str | None, + *, + instance_type: str | None = None, + size_gb: int | None = None, +) -> list[MachineDesc]: + """Load machine configs, applying optional overrides. + + With no `machine` but an explicit `instance_type`, synthesize a single + default config so callers can launch an arbitrary instance type without a + preset file. With no `machine` and no `instance_type`, prompt for one.""" + if machine is None and instance_type is not None: + desc = MachineDesc(name=instance_type, instance_type=instance_type) + if size_gb is not None: + desc.size_gb = size_gb + return [desc] + + if machine is None: + machine = pick_machine() + + with open(MZ_ROOT / "misc" / "scratch" / f"{machine}.json") as f: + print(f"Reading machine configs from {f.name}") + descs = [MachineDesc.model_validate(obj) for obj in multi_json(f.read())] + + if instance_type is not None: + if len(descs) != 1: + raise RuntimeError( + f"--instance-type requires a single-machine config, got {len(descs)}" + ) + descs[0].instance_type = instance_type + # An explicit type override invalidates any pinned AMI; re-derive it. + descs[0].ami = None + if size_gb is not None: + for d in descs: + d.size_gb = size_gb + return descs + + def configure_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--key-name", type=str, required=False, help="Optional EC2 Key Pair name" @@ -139,6 +177,19 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: "Hint: `dev-box` is a good starter!" ), ) + parser.add_argument( + "--instance-type", + type=str, + help=( + "Override the EC2 instance type. The AMI is derived from its " + "architecture. May be used without a machine config." + ), + ) + parser.add_argument( + "--size-gb", + type=int, + help="Override the root volume size in GiB.", + ) parser.add_argument( "--max-age-days", type=float, @@ -158,15 +209,11 @@ def run(args: argparse.Namespace) -> None: "extra-tags must be a JSON dictionary of strings to strings" ) - if args.machine: - with open(MZ_ROOT / "misc" / "scratch" / f"{args.machine}.json") as f: - print(f"Reading machine configs from {f.name}") - descs = [MachineDesc.model_validate(obj) for obj in multi_json(f.read())] - else: - args.machine = pick_machine() - with open(MZ_ROOT / "misc" / "scratch" / f"{args.machine}.json") as f: - print(f"Reading machine configs from {f.name}") - descs = [MachineDesc.model_validate(obj) for obj in multi_json(f.read())] + descs = load_machine_descs( + args.machine, + instance_type=args.instance_type, + size_gb=args.size_gb, + ) check_required_vars() extra_tags["LaunchedBy"] = whoami() @@ -184,7 +231,7 @@ def run(args: argparse.Namespace) -> None: security_group_name=args.security_group_name, instance_profile=args.instance_profile, extra_tags=extra_tags, - delete_after=datetime.datetime.utcnow() + max_age, + delete_after=datetime.datetime.now(datetime.timezone.utc) + max_age, git_rev=args.git_rev, extra_env={}, ) diff --git a/misc/python/materialize/cli/scratch/go.py b/misc/python/materialize/cli/scratch/go.py index a09718bfd99a1..bd6e581e3e3d7 100644 --- a/misc/python/materialize/cli/scratch/go.py +++ b/misc/python/materialize/cli/scratch/go.py @@ -14,10 +14,8 @@ import datetime from materialize.cli.scratch import check_required_vars -from materialize.cli.scratch.create import MAX_AGE_DAYS, multi_json +from materialize.cli.scratch.create import MAX_AGE_DAYS, load_machine_descs from materialize.scratch import ( - MZ_ROOT, - MachineDesc, get_instance, launch_cluster, mssh, @@ -32,11 +30,26 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "machine", + nargs="?", + default=None, help=( "Machine config name from misc/scratch (e.g. dev-box). " "Used only when creating a new instance." ), ) + parser.add_argument( + "--instance-type", + type=str, + help=( + "Override the EC2 instance type. The AMI is derived from its " + "architecture. May be used without a machine config." + ), + ) + parser.add_argument( + "--size-gb", + type=int, + help="Override the root volume size in GiB.", + ) parser.add_argument( "--max-age-days", type=float, @@ -46,9 +59,12 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: def run(args: argparse.Namespace) -> None: - # Read the machine config - with open(MZ_ROOT / "misc" / "scratch" / f"{args.machine}.json") as f: - descs = [MachineDesc.model_validate(obj) for obj in multi_json(f.read())] + # Read the machine config, applying any overrides. + descs = load_machine_descs( + args.machine, + instance_type=args.instance_type, + size_gb=args.size_gb, + ) if len(descs) != 1: raise RuntimeError( @@ -69,14 +85,15 @@ def run(args: argparse.Namespace) -> None: pass # No existing instance — create one - say(f"No existing instance found, creating from {args.machine}...") + label = args.machine or args.instance_type + say(f"No existing instance found, creating from {label}...") max_age = datetime.timedelta(days=args.max_age_days) extra_tags = {"LaunchedBy": whoami()} instances = launch_cluster( descs, extra_tags=extra_tags, - delete_after=datetime.datetime.utcnow() + max_age, + delete_after=datetime.datetime.now(datetime.timezone.utc) + max_age, ) print("Launched:") diff --git a/misc/python/materialize/cli/scratch/list_cmd.py b/misc/python/materialize/cli/scratch/list_cmd.py index c2ea9823127b6..348bd889ed32c 100644 --- a/misc/python/materialize/cli/scratch/list_cmd.py +++ b/misc/python/materialize/cli/scratch/list_cmd.py @@ -25,4 +25,9 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: def run(args: argparse.Namespace) -> None: instances = list_all_instances(owners=args.owner or None, all=args.all) - print_instances(instances, args.output_format) + # Show the owner column only when listing instances beyond your own. + print_instances( + instances, + args.output_format, + show_launched_by=args.all or bool(args.owner), + ) diff --git a/misc/python/materialize/scratch.py b/misc/python/materialize/scratch.py index 8e2ff167e13b3..df4c8ab2f89b9 100644 --- a/misc/python/materialize/scratch.py +++ b/misc/python/materialize/scratch.py @@ -13,6 +13,7 @@ import csv import datetime import os +import re import shlex import subprocess import sys @@ -29,7 +30,6 @@ InstanceTypeDef, RunInstancesRequestServiceResourceCreateInstancesTypeDef, ) -from prettytable import PrettyTable from pydantic import BaseModel from materialize import MZ_ROOT, git, spawn, ui, util @@ -38,12 +38,51 @@ DEFAULT_SECURITY_GROUP_NAME = "scratch-security-group" DEFAULT_INSTANCE_PROFILE_NAME = "admin-instance" +# Ubuntu 26.04 LTS (Resolute Raccoon) base images, keyed by the CPU +# architecture that EC2 reports for an instance type. +# +# To update: open https://cloud-images.ubuntu.com/locator/ec2/, filter to the +# desired release, zone `us-east-1`, and instance type `hvm:ebs-ssd-gp3`. Copy +# the AMI id for each architecture (amd64 -> x86_64, arm64) below. Bumping the +# release also means updating the comment above and the `ami_user` default if +# Canonical changes it. +AMIS_BY_ARCH: dict[str, str] = { + "x86_64": "ami-0b6d9d3d33ba97d99", + "arm64": "ami-0bc7f2dbdcc6b5303", +} + SSH_COMMAND = ["mssh", "-o", "StrictHostKeyChecking=off"] SFTP_COMMAND = ["msftp", "-o", "StrictHostKeyChecking=off"] say = ui.speaker("scratch> ") +def instance_type_arch(instance_type: str) -> str: + """Return the CPU architecture for an EC2 instance type as a key into + `AMIS_BY_ARCH`. Queries EC2, which also validates that the type exists.""" + infos = boto3.client("ec2").describe_instance_types( + InstanceTypes=[cast(InstanceTypeType, instance_type)] + )["InstanceTypes"] + if not infos: + raise RuntimeError(f"unknown EC2 instance type: {instance_type}") + archs = infos[0]["ProcessorInfo"]["SupportedArchitectures"] + for arch in archs: + if arch in AMIS_BY_ARCH: + return arch + raise RuntimeError( + f"no scratch AMI for instance type {instance_type} " + f"(architectures: {', '.join(archs)})" + ) + + +def resolve_ami(desc: "MachineDesc") -> str: + """Resolve the AMI for a machine, deriving it from the instance type's + architecture unless the config pins one explicitly.""" + if desc.ami: + return desc.ami + return AMIS_BY_ARCH[instance_type_arch(desc.instance_type)] + + def tags(i: Instance) -> dict[str, str]: if not i.tags: return {} @@ -80,58 +119,123 @@ def instance_host(instance: Instance, user: str | None = None) -> str: return f"{user}@{instance.id}" +# Launch tags the instance Name as "{nonce}-{config name}"; the nonce is +# redundant with the instance id in a flat listing, so strip it for display. +_NONCE_PREFIX = re.compile(r"^[0-9a-f]{8}-") + + +def _short_name(n: str | None) -> str: + if not n: + return "-" + return _NONCE_PREFIX.sub("", n, count=1) + + +def render_table(headers: list[str], rows: list[list[str]]) -> None: + """Print a borderless, left-aligned table, like `docker ps` / `kubectl get`: + columns separated by two spaces, an uppercase header, no rules.""" + widths = [max(len(r[c]) for r in [headers, *rows]) for c in range(len(headers))] + for r in [headers, *rows]: + print(" ".join(cell.ljust(w) for cell, w in zip(r, widths)).rstrip()) + + +def _format_expires(dt: datetime.datetime | None) -> str: + """Render a delete-after time as a compact relative duration.""" + if dt is None: + return "-" + secs = (dt - datetime.datetime.now()).total_seconds() + if secs <= 0: + return "expired" + if secs >= 86400: + return f"{secs / 86400:.1f}d" + if secs >= 3600: + return f"{secs / 3600:.0f}h" + return f"{secs / 60:.0f}m" + + def print_instances( - ists: list[Instance], format: str = "table", numbered: bool = False + ists: list[Instance], + format: str = "table", + numbered: bool = False, + show_launched_by: bool = False, ) -> None: - field_names = [ - "Name", - "Instance ID", - "Public IP Address", - "Private IP Address", - "Launched By", - "Delete After", - "State", - ] + if format == "csv": + # CSV is machine-readable: emit the full set of columns, unabridged. + field_names = [ + "Name", + "Instance ID", + "Public IP Address", + "Private IP Address", + "Launched By", + "Delete After", + "State", + ] + if numbered: + field_names = ["#"] + field_names + w = csv.writer(sys.stdout) + w.writerow(field_names) + for idx, i in enumerate(ists, 1): + t = tags(i) + row = [ + name(t), + i.instance_id, + i.public_ip_address, + i.private_ip_address, + launched_by(t), + delete_after(t), + i.state["Name"], + ] + if numbered: + row = [idx] + row + w.writerow(row) + return + + if format != "table": + raise RuntimeError("Unknown format passed to print_instances") + + # Table is for humans: a compact subset that fits a default terminal. + # Private IP is omitted (mssh connects by instance id, not IP) and the + # owner only shown when listing instances beyond your own. + headers = ["INSTANCE ID", "NAME", "STATE", "EXPIRES", "PUBLIC IP"] + if show_launched_by: + headers.append("LAUNCHED BY") if numbered: - field_names = ["#"] + field_names + headers = ["#"] + headers rows = [] for idx, i in enumerate(ists, 1): t = tags(i) + state = i.state["Name"] + # A dead instance has no meaningful expiry; don't show a future time. + expires = ( + "-" + if state in ("terminated", "shutting-down") + else _format_expires(delete_after(t)) + ) row = [ - name(t), i.instance_id, - i.public_ip_address, - i.private_ip_address, - launched_by(t), - delete_after(t), - i.state["Name"], + _short_name(name(t)), + state, + expires, + i.public_ip_address or "-", ] + if show_launched_by: + row.append(launched_by(t) or "-") if numbered: - row = [idx] + row + row = [str(idx)] + row rows.append(row) - if format == "table": - pt = PrettyTable() - pt.field_names = field_names - pt.align = "l" - pt.add_rows(rows) - print(pt) - elif format == "csv": - w = csv.writer(sys.stdout) - w.writerow(field_names) - w.writerows(rows) - else: - raise RuntimeError("Unknown format passed to print_instances") + + render_table(headers, rows) def mssh( instance: Instance, command: str, *, - extra_ssh_args: list[str] = [], + extra_ssh_args: list[str] | None = None, input: bytes | None = None, quiet: bool = False, ) -> None: """Runs a command over SSH via EC2 Instance Connect.""" + extra_ssh_args = extra_ssh_args or [] host = instance_host(instance) if command: if not quiet: @@ -275,7 +379,9 @@ class MachineDesc(BaseModel): name: str launch_script: str | None = None instance_type: str - ami: str + # Optional: when unset, the AMI is derived from the instance type's + # architecture via `resolve_ami`. + ami: str | None = None tags: dict[str, str] = {} size_gb: int = 50 checkout: bool = True @@ -317,7 +423,7 @@ def launch( vpcs = ec2.describe_vpcs() vpc_id = None for vpc in vpcs["Vpcs"]: - if vpc["IsDefault"] == True: + if vpc["IsDefault"]: vpc_id = vpc["VpcId"] break if vpc_id is None: @@ -472,13 +578,16 @@ def launch_cluster( key_name: str | None = None, security_group_name: str = DEFAULT_SECURITY_GROUP_NAME, instance_profile: str | None = DEFAULT_INSTANCE_PROFILE_NAME, - extra_tags: dict[str, str] = {}, + extra_tags: dict[str, str] | None = None, delete_after: datetime.datetime, git_rev: str = "HEAD", - extra_env: dict[str, str] = {}, + extra_env: dict[str, str] | None = None, ) -> list[Instance]: """Launch a cluster of instances with a given nonce""" + extra_tags = extra_tags or {} + extra_env = extra_env or {} + if not nonce: nonce = util.nonce(8) @@ -486,7 +595,7 @@ def launch_cluster( launch( key_name=key_name, instance_type=d.instance_type, - ami=d.ami, + ami=resolve_ami(d), ami_user=d.ami_user, tags={**d.tags, **extra_tags}, display_name=f"{nonce}-{d.name}", @@ -499,15 +608,15 @@ def launch_cluster( for d in descs ] - loop = asyncio.get_event_loop() - loop.run_until_complete( - asyncio.gather( + async def setup_all() -> None: + await asyncio.gather( *( setup(i, git_rev if d.checkout else "HEAD") for (i, d) in zip(instances, descs) ) ) - ) + + asyncio.run(setup_all()) for i in instances: i.reload() @@ -581,7 +690,7 @@ def is_old(i: InstanceTypeDef) -> bool: if delete_after is None: return False delete_after = float(delete_after) - return datetime.datetime.utcnow().timestamp() > delete_after + return datetime.datetime.now(datetime.timezone.utc).timestamp() > delete_after return [ i diff --git a/misc/scratch/big-dev-box.json b/misc/scratch/big-dev-box.json index a7a577129a135..a8c0eb95c413c 100644 --- a/misc/scratch/big-dev-box.json +++ b/misc/scratch/big-dev-box.json @@ -2,7 +2,6 @@ "name": "one-off load testing", "launch_script": "true", "instance_type": "r5a.4xlarge", - "ami": "ami-0ecb62995f68bb549", "size_gb": 200, "tags": {} } diff --git a/misc/scratch/ci-aarch64-large.json b/misc/scratch/ci-aarch64-large.json index 39e1a2bd593ce..7b33dfd45a798 100644 --- a/misc/scratch/ci-aarch64-large.json +++ b/misc/scratch/ci-aarch64-large.json @@ -2,7 +2,6 @@ "name": "c8g.12xlarge as used by CI, when a large box is needed (aarch64-large)", "launch_script": "true", "instance_type": "c8g.12xlarge", - "ami": "ami-01b9f1e7dc427266e", "size_gb": 128, "tags": {} } diff --git a/misc/scratch/ci-aarch64-medium.json b/misc/scratch/ci-aarch64-medium.json index 8af6a9ca03376..8df7f94dc17b3 100644 --- a/misc/scratch/ci-aarch64-medium.json +++ b/misc/scratch/ci-aarch64-medium.json @@ -2,7 +2,6 @@ "name": "c8g.4xlarge as used by CI, when a medium box is needed (aarch64-medium)", "launch_script": "true", "instance_type": "c8g.4xlarge", - "ami": "ami-01b9f1e7dc427266e", "size_gb": 128, "tags": {} } diff --git a/misc/scratch/ci-aarch64.json b/misc/scratch/ci-aarch64.json index f8103946fcc31..23922fc3e1bcb 100644 --- a/misc/scratch/ci-aarch64.json +++ b/misc/scratch/ci-aarch64.json @@ -2,7 +2,6 @@ "name": "c8g.2xlarge as used by the CI (aarch64)", "launch_script": "true", "instance_type": "c8g.2xlarge", - "ami": "ami-01b9f1e7dc427266e", "size_gb": 128, "tags": {} } diff --git a/misc/scratch/ci-x86_64-large.json b/misc/scratch/ci-x86_64-large.json index b4fdb716bd45c..2cc8927d530e8 100644 --- a/misc/scratch/ci-x86_64-large.json +++ b/misc/scratch/ci-x86_64-large.json @@ -2,7 +2,6 @@ "name": "m7a.8xlarge as used by CI", "launch_script": "true", "instance_type": "m7a.8xlarge", - "ami": "ami-0ecb62995f68bb549", "size_gb": 250, "tags": {} } diff --git a/misc/scratch/ci.json b/misc/scratch/ci.json index 8fbc7e72a18e6..aa620f31e060d 100644 --- a/misc/scratch/ci.json +++ b/misc/scratch/ci.json @@ -2,7 +2,6 @@ "name": "c7a.2xlarge as used by the CI", "launch_script": "true", "instance_type": "c7a.2xlarge", - "ami": "ami-0ecb62995f68bb549", "size_gb": 128, "tags": {} } diff --git a/misc/scratch/dev-box.json b/misc/scratch/dev-box.json index f0caef4cc56e4..b771c8fd13782 100644 --- a/misc/scratch/dev-box.json +++ b/misc/scratch/dev-box.json @@ -2,7 +2,6 @@ "name": "one-off testing", "launch_script": "true", "instance_type": "t3.2xlarge", - "ami": "ami-0ecb62995f68bb549", "size_gb": 50, "tags": {} } diff --git a/misc/scratch/persist-benchmarking.json b/misc/scratch/persist-benchmarking.json index 0fed93aaadd5d..93bc676e7ffc5 100644 --- a/misc/scratch/persist-benchmarking.json +++ b/misc/scratch/persist-benchmarking.json @@ -2,7 +2,6 @@ "name": "persist benchmarking", "launch_script": "true", "instance_type": "m6i.8xlarge", - "ami": "ami-0ecb62995f68bb549", "size_gb": 200, "tags": {} } diff --git a/misc/scratch/prod-like-arm.json b/misc/scratch/prod-like-arm.json index dbabd08d8482c..aa50e5e613b07 100644 --- a/misc/scratch/prod-like-arm.json +++ b/misc/scratch/prod-like-arm.json @@ -2,7 +2,6 @@ "name": "production-like ARM", "launch_script": "true", "instance_type": "r6g.4xlarge", - "ami": "ami-05d47d29a4c2d19e1", "size_gb": 200, "tags": {} } diff --git a/misc/scratch/provision.bash b/misc/scratch/provision.bash index db4b628c7714a..8b17b13267dd9 100644 --- a/misc/scratch/provision.bash +++ b/misc/scratch/provision.bash @@ -16,9 +16,12 @@ set -euo pipefail ARCH=$(uname -m) ARCH_GO=$(echo "$ARCH" | sed -e "s/aarch64/arm64/" -e "s/x86_64/amd64/") -DOCKER_VERSION=29.1.3 -DOCKER_COMPOSE_VERSION=2.37.3 -NODE_VERSION=22.22.1 +# Keep these in sync with ci/builder/Dockerfile so scratch matches CI. Docker +# itself is unpinned there (it uses the distro docker.io); we stay on the 28 +# line until moby#51845 is fixed (Docker 29 is unstable). +DOCKER_VERSION=28.5.2 +DOCKER_COMPOSE_VERSION=5.0.1 +NODE_VERSION=24.16.0 NODE_ARCH=$(echo "$ARCH" | sed -e "s/aarch64/arm64/" -e "s/x86_64/x64/") # Everything runs in parallel — no serial APT bottleneck. @@ -64,13 +67,6 @@ apt-get install -y -qq --no-install-recommends \ screen \ unzip) & -# Update ec2-instance-connect scripts to a version that works with OpenSSL 3. -# https://github.com/aws/aws-ec2-instance-connect-config/issues/38 -(curl -fsSL "https://github.com/aws/aws-ec2-instance-connect-config/archive/refs/tags/1.1.17.zip" -o /tmp/ec2-ic.zip -unzip -q -o /tmp/ec2-ic.zip -d /tmp -cp /tmp/aws-ec2-instance-connect-config-1.1.17/src/bin/* /usr/share/ec2-instance-connect/ -rm -rf /tmp/ec2-ic.zip /tmp/aws-ec2-instance-connect-config-1.1.17) & - # Rust (sudo -u ubuntu sh -c "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -q -y" >/dev/null 2>&1) & @@ -83,7 +79,9 @@ rm -rf /tmp/awscli.zip /tmp/aws) & # uv (curl -fsSL https://astral.sh/uv/install.sh | sudo -u ubuntu sh >/dev/null 2>&1) & -# kubectl + kind + k9s +# kubectl + kind + k9s. kind and kubectl mirror ci/builder/Dockerfile, which +# deliberately holds them back: newer versions break Cloudtest (minio fails to +# come up on the kind cluster). (curl -fsSL "https://dl.k8s.io/release/v1.24.3/bin/linux/$ARCH_GO/kubectl" -o /usr/local/bin/kubectl chmod +x /usr/local/bin/kubectl) & (curl -fsSL "https://kind.sigs.k8s.io/dl/v0.29.0/kind-linux-$ARCH_GO" -o /usr/local/bin/kind diff --git a/misc/scratch/remote-dev.json b/misc/scratch/remote-dev.json index 51ff376ae1ca2..2f2f9c1e1f52e 100644 --- a/misc/scratch/remote-dev.json +++ b/misc/scratch/remote-dev.json @@ -2,7 +2,6 @@ "name": "vs code remote dev", "launch_script": "true", "instance_type": "c7a.4xlarge", - "ami": "ami-0ecb62995f68bb549", "size_gb": 100, "tags": {} } diff --git a/misc/scratch/small.json b/misc/scratch/small.json index 1b7da617a5713..0f319172112b3 100644 --- a/misc/scratch/small.json +++ b/misc/scratch/small.json @@ -2,7 +2,6 @@ "name": "small", "launch_script": "true", "instance_type": "t3.small", - "ami": "ami-0ecb62995f68bb549", "size_gb": 64, "tags": {} }