GcpCompute.Instances (GcpCompute v0.3.0)

Copy Markdown View Source

CRUD + lifecycle for Compute Engine instances.

All functions take a GcpCompute.Config and default to its :zone; override per call with zone: "..." in opts. Mutating calls return a GcpCompute.Operation; the *_and_wait helpers poll it to completion for you.

{:ok, instance} =
  GcpCompute.Instances.insert_and_wait(config,
    name: "worker-1",
    machine_type: "e2-micro",
    spot: true
  )

Summary

Types

An instance body: either a keyword spec for GcpCompute.Instance.spec/1, or a plain, string-keyed Compute API body map (passed through verbatim).

Functions

Delete an instance. Accepts :zone and :request_id (see insert/3).

Delete an instance and block until the operation is done.

Fetch a single instance by name.

Create an instance. instance is either a keyword spec (built and validated by GcpCompute.Instance.spec/1) or a raw Compute API body map.

Insert an instance and block until it exists, returning the live GcpCompute.Instance.

List instances in a zone (single page).

List one page of instances, surfacing the pagination token.

Every operation recorded against an instance, newest first — its audit trail.

Whether this instance was preempted. Convenience over preemption/3.

The operation recording this instance's preemption, or nil if there is none.

Read an instance's serial console output — in practice, "the VM's logs".

Simulate a maintenance event. On a Spot VM this triggers a real preemption.

Start a stopped instance.

Stop a running instance.

Types

spec()

@type spec() :: keyword() | map()

An instance body: either a keyword spec for GcpCompute.Instance.spec/1, or a plain, string-keyed Compute API body map (passed through verbatim).

Functions

delete(config, name, opts \\ [])

@spec delete(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}

Delete an instance. Accepts :zone and :request_id (see insert/3).

delete_and_wait(config, name, opts \\ [])

@spec delete_and_wait(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}

Delete an instance and block until the operation is done.

get(config, name, opts \\ [])

@spec get(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, GcpCompute.Instance.t()} | {:error, GcpCompute.Error.t()}

Fetch a single instance by name.

insert(config, instance, opts \\ [])

@spec insert(GcpCompute.Config.t(), spec(), keyword()) ::
  {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}

Create an instance. instance is either a keyword spec (built and validated by GcpCompute.Instance.spec/1) or a raw Compute API body map.

Options

  • :zone — override the config's default zone.
  • :request_id — idempotency token (requestId). Must be an RFC 4122 UUID: GCP rejects anything else with %GcpCompute.Error{reason: :api_error, status: 400} and the message "Idempotent request error: Idempotent id should be in UUID format as defined in RFC 4122." GCP deduplicates retries that carry the same value; this library never generates one and never retries a POST or DELETE on your behalf.

insert_and_wait(config, instance, opts \\ [])

@spec insert_and_wait(GcpCompute.Config.t(), spec(), keyword()) ::
  {:ok, GcpCompute.Instance.t()} | {:error, GcpCompute.Error.t()}

Insert an instance and block until it exists, returning the live GcpCompute.Instance.

Accepts every insert/3 option plus :timeout and :poll_interval (ms) for the operation poll — see GcpCompute.Operations.poll_until_done/3.

list(config, opts \\ [])

@spec list(
  GcpCompute.Config.t(),
  keyword()
) :: {:ok, [GcpCompute.Instance.t()]} | {:error, GcpCompute.Error.t()}

List instances in a zone (single page).

Returns just the items; callers that need to paginate should use list_page/2, which also surfaces the nextPageToken.

Options

Same as list_page/2.

list_page(config, opts \\ [])

@spec list_page(
  GcpCompute.Config.t(),
  keyword()
) ::
  {:ok, %{items: [GcpCompute.Instance.t()], next_page_token: String.t() | nil}}
  | {:error, GcpCompute.Error.t()}

List one page of instances, surfacing the pagination token.

Returns {:ok, %{items: [Instance.t()], next_page_token: String.t() | nil}}; next_page_token is nil on the last (or only) page — pass it back as :page_token to fetch the next page.

Options

All option names are snake_case and are translated to the Compute API's camelCase query parameters. An unknown key is rejected with {:error, %GcpCompute.Error{reason: :invalid_argument}} rather than silently dropped.

  • :zone — override the config's default zone. Listing is per-zone: the Compute aggregatedList endpoint is not wrapped, so finding an instance whose zone you do not know means iterating zones yourself.

  • :filter — Compute API filter expression. Verified against the live API:

    filter: ~s(name = "worker-1")            # exact
    filter: ~s(name = "worker-*")            # prefix — glob, see below
    filter: ~s(name != "worker-1")
    filter: ~s(status = "RUNNING")
    filter: ~s(labels.role = "batch")
    filter: ~s(labels.env = "prod" AND status = "RUNNING")

    Note the sharp edge: instances.list accepts globs, not regexes. The ~ and !~ operators are rejected with %GcpCompute.Error{reason: :api_error, status: 400} and "Invalid list filter expression", so use name = "prefix*" rather than name ~ "prefix".

  • :max_resultsmaxResults. Combine with :page_token to paginate.

  • :page_tokenpageToken; the next_page_token from a previous page.

  • :order_byorderBy. Only "name" and "creationTimestamp desc" are accepted; "name desc" is rejected with a 400 (DESC is supported only on creationTimestamp).

operations(config, name, opts \\ [])

@spec operations(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, [GcpCompute.Operation.t()]} | {:error, GcpCompute.Error.t()}

Every operation recorded against an instance, newest first — its audit trail.

Answers "what happened to my VM?" when it is gone and you did not delete it: operations outlive the instance they targeted, so this works on a name that no longer resolves.

{:ok, ops} = GcpCompute.Instances.operations(config, "worker-1")
Enum.map(ops, &{&1.operation_type, &1.status_message})
#=> [{"compute.instances.preempted", "Instance was preempted."},
#    {"insert", nil}]

Options

  • :zone - override the config's default zone.

Why this is filtered client-side

The zone operation list is filtered server-side by targetLink, then matched exactly here. Three alternatives were measured against the live API and rejected: an exact targetLink match needs the www.googleapis.com host the API returns, which is not the compute.googleapis.com host :base_url builds (silently matches nothing); mixing the modern AND syntax with a legacy eq regex makes the API silently drop a clause (an unrelated instance tested as preempted); and orderBy with any filter is a hard 400, so the newest-first ordering is done here too.

preempted?(config, name, opts \\ [])

@spec preempted?(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, boolean()} | {:error, GcpCompute.Error.t()}

Whether this instance was preempted. Convenience over preemption/3.

{:ok, true} = GcpCompute.Instances.preempted?(config, "worker-1")

preemption(config, name, opts \\ [])

@spec preemption(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, GcpCompute.Operation.t() | nil} | {:error, GcpCompute.Error.t()}

The operation recording this instance's preemption, or nil if there is none.

A preempted Spot VM using the default instanceTerminationAction: "DELETE" is deleted, so get/3 answers 404 and the instance itself can no longer tell you why it went away. This can, because the operation outlives it.

case GcpCompute.Instances.preemption(config, "worker-1") do
  {:ok, nil} -> :not_preempted
  {:ok, op} -> {:preempted_at, op.end_time}
end

{:ok, nil} is also the answer for an instance that never existed — absence of evidence, not an error.

serial_port_output(config, name, opts \\ [])

@spec serial_port_output(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, %{contents: binary(), start: integer(), next: integer()}}
  | {:error, GcpCompute.Error.t()}

Read an instance's serial console output — in practice, "the VM's logs".

This is instances.getSerialPortOutput. It is the only log surface the Compute API itself offers, and it is where a :startup_script's output lands, so it is how you find out what your boot script actually did. Cloud Logging is a separate API and is deliberately not wrapped here.

Returns {:ok, %{contents: binary, start: integer, next: integer}}. :next is the byte offset to pass back as :start to read only what has been written since — the buffer is large (a booted Debian image is ~145 KB on port 1), so re-fetching it whole on every poll is wasteful.

{:ok, %{contents: log, next: next}} = Instances.serial_port_output(config, "worker-1")

# …later, only the new bytes:
{:ok, %{contents: delta}} = Instances.serial_port_output(config, "worker-1", start: next)

Options

  • :zone — override the config's default zone.
  • :port — serial port 1..4 (default 1, where boot and startup-script output go). Validated locally; the API answers 400 for anything else.
  • :start — byte offset to read from, normally a previous call's :next. A negative value is interpreted by the API as "from the end".

simulate_maintenance_event(config, name, opts \\ [])

@spec simulate_maintenance_event(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}

Simulate a maintenance event. On a Spot VM this triggers a real preemption.

This is GCP's documented way to test that your code survives preemption without waiting for a random one. It is not a dry run: the instance is genuinely preempted, and with the default instanceTerminationAction: "DELETE" it is then genuinely deleted.

Measured against a real Spot VM: RUNNING -> STOPPING after ~60 s -> gone (404) after ~73 s. Budget minutes, not seconds, and see preemption/3 for how to confirm it afterwards.

{:ok, _op} = GcpCompute.Instances.simulate_maintenance_event(config, "worker-1")

start(config, name, opts \\ [])

@spec start(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}

Start a stopped instance.

stop(config, name, opts \\ [])

@spec stop(GcpCompute.Config.t(), String.t(), keyword()) ::
  {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}

Stop a running instance.