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
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
@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).
@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.
@spec get(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Instance.t()} | {:error, GcpCompute.Error.t()}
Fetch a single instance by name.
@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.
@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.
@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.
@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 ComputeaggregatedListendpoint is not wrapped, so finding an instance whose zone you do not know means iterating zones yourself.:filter— Compute APIfilterexpression. 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.listaccepts globs, not regexes. The~and!~operators are rejected with%GcpCompute.Error{reason: :api_error, status: 400}and"Invalid list filter expression", so usename = "prefix*"rather thanname ~ "prefix".:max_results—maxResults. Combine with:page_tokento paginate.:page_token—pageToken; thenext_page_tokenfrom a previous page.:order_by—orderBy. Only"name"and"creationTimestamp desc"are accepted;"name desc"is rejected with a 400 (DESCis supported only oncreationTimestamp).
@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.
@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")
@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.
@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 port1..4(default1, 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".
@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")
@spec start(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
Start a stopped instance.
@spec stop(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
Stop a running instance.