# `GcpCompute.Instances`
[🔗](https://github.com/nyo16/gcp_compute/blob/v0.3.0/lib/gcp_compute/instances.ex#L1)

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
      )

# `spec`

```elixir
@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).

# `delete`

```elixir
@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`

```elixir
@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`

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

Fetch a single instance by name.

# `insert`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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_results` — `maxResults`. Combine with `:page_token` to paginate.
  * `:page_token` — `pageToken`; the `next_page_token` from a previous page.
  * `:order_by` — `orderBy`. Only `"name"` and `"creationTimestamp desc"` are
    accepted; `"name desc"` is rejected with a 400 (`DESC` is supported only on
    `creationTimestamp`).

# `operations`

```elixir
@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 {: .info}
>
> 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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Start a stopped instance.

# `stop`

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

Stop a running instance.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
