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

Spawn and manage Google Compute Engine instances over the REST API.

> #### Why REST? {: .info}
>
> The Compute Engine API is **REST/JSON only** — it has no gRPC endpoint.
> This library talks to it with [`Req`](https://hexdocs.pm/req); there is no
> connection pool or supervised process to run. A `GcpCompute.Config` is the
> only handle you pass around.

## Quickstart

    # 1. Mint tokens with Goth (or any GcpCompute.TokenProvider)
    children = [{Goth, name: MyApp.Goth}]

    # 2. Build a config (do this once, reuse it)
    {:ok, config} =
      GcpCompute.Config.production(
        project: "my-project",
        zone: "us-central1-a",
        goth: MyApp.Goth
      )

    # 3. Spawn a cheap, self-deleting Spot VM and wait for it
    {:ok, instance} =
      GcpCompute.insert_instance_and_wait(config,
        name: "worker-1",
        machine_type: "e2-micro",
        spot: true,
        max_run_duration: 3600,
        startup_script: "#!/bin/bash\necho ready > /tmp/ready"
      )

    GcpCompute.Instance.external_ip(instance)
    #=> "34.x.x.x"

    # 4. Tear it down
    {:ok, _op} = GcpCompute.delete_instance_and_wait(config, "worker-1")

The functions here are thin wrappers over `GcpCompute.Instances` and
`GcpCompute.Operations`; reach for those modules directly for the full surface.

# `clear_config`

```elixir
@spec clear_config(atom(), atom()) :: :ok
```

Drop the cached config for `{otp_app, key}` so the next `config/2` re-reads env.

Returns `:ok` whether or not an entry was cached. See `config/2` for the
caching caveat (`:persistent_term` writes trigger a global GC).

# `config`

```elixir
@spec config(atom(), atom()) ::
  {:ok, GcpCompute.Config.t()} | {:error, GcpCompute.Error.t()}
```

Load (and cache) a config from application env.

    # config :my_app, :gcp_compute, project: "p", zone: "...", token_provider: {...}
    {:ok, config} = GcpCompute.config(:my_app)

The result is memoized in `:persistent_term` on the first successful load and
reused on every later call. Caching is **first-write-wins with no
invalidation**: once cached, later changes to the application env are ignored.
Only successful loads are cached — an invalid env returns its `{:error, _}`
without caching, so it is re-read next call. To force a reload after
reconfiguring at runtime, call `clear_config/2` first. (`:persistent_term`
writes trigger a global GC, so treat clearing as a rare operation, not a
per-request one.)

> #### Cached configs are secrets at rest {: .warning}
>
> The cached value is the whole `%GcpCompute.Config{}`, including its
> `:token_provider` arg. With the production default
> (`GcpCompute.TokenProvider.Goth`) that arg is just a registered process
> name — nothing sensitive. With `GcpCompute.TokenProvider.Static` it is the
> raw bearer token, and `:persistent_term` gives it none of the protection you
> might assume:
>
>   * it is **process-global** — any code in the VM can read it with
>     `:persistent_term.get/1`, and `:persistent_term.get/0` enumerates
>     everything;
>   * it is **unencrypted** and written verbatim into `erl_crash.dump`;
>   * it is visible in `:observer` and `:recon`;
>   * `GcpCompute.Config`'s `Inspect` redaction does **not** apply — that only
>     affects `inspect/1` output, not term storage or a crash dump.
>
> This is accepted, not fixed: skipping the cache for credential-bearing
> providers would only move the token into the `%Config{}` that every caller
> holds anyway, while quietly turning `config/2` into an env re-read on every
> call. For production, mint tokens with Goth (or your own provider) so no
> long-lived credential is ever stored here.

# `delete_instance`

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

See `GcpCompute.Instances.delete/3`.

# `delete_instance_and_wait`

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

See `GcpCompute.Instances.delete_and_wait/3`.

# `from_env`

```elixir
@spec from_env(atom(), atom()) ::
  {:ok, GcpCompute.Config.t()} | {:error, GcpCompute.Error.t()}
```

See `GcpCompute.Config.from_env/2`.

# `get_instance`

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

See `GcpCompute.Instances.get/3`.

# `insert_instance`

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

See `GcpCompute.Instances.insert/3`.

# `insert_instance_and_wait`

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

See `GcpCompute.Instances.insert_and_wait/3`.

# `instance_logs`

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

See `GcpCompute.Instances.serial_port_output/3` — the VM's serial console
output, which is where a `:startup_script`'s output lands.

# `instance_operations`

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

See `GcpCompute.Instances.operations/3` — an instance's audit trail.

# `instance_preempted?`

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

See `GcpCompute.Instances.preempted?/3`.

# `instance_preemption`

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

See `GcpCompute.Instances.preemption/3` — the operation recording a Spot VM's
preemption, or `nil`. Works on an instance that is already gone.

# `launch`

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

Spawn an instance and wait until it's running.

Sugar over `insert_instance_and_wait/3`: the name is lifted out, and spec
options (`machine_type`, `spot`, …) are auto-separated from call options
(`:zone`, `:request_id`, `:timeout`, `:poll_interval`).

    {:ok, vm} = GcpCompute.launch(config, "worker-1", machine_type: "e2-micro", spot: true)

> #### `spot: true` is the default {: .warning}
>
> A bare `launch(config, "worker-1")` provisions a **Spot VM** that can be
> preempted at any time and, when it is, **deletes itself along with anything
> stored on it** (`provisioningModel: "SPOT"`, `automaticRestart: false`,
> `instanceTerminationAction: "DELETE"`). That is this library's intended
> default — cheap, self-cleaning batch workers — but pass `spot: false` for
> anything whose disk you care about.

# `list_instances`

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

See `GcpCompute.Instances.list/2`.

# `list_instances_page`

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

See `GcpCompute.Instances.list_page/2`.

# `local`

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

See `GcpCompute.Config.local/1`.

# `new`

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

See `GcpCompute.Config.new/1`.

# `production`

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

See `GcpCompute.Config.production/1`.

# `simulate_maintenance_event`

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

See `GcpCompute.Instances.simulate_maintenance_event/3` — on a Spot VM this
triggers a **real** preemption, for testing that your code survives one.

# `start_instance`

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

See `GcpCompute.Instances.start/3`.

# `stop_instance`

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

See `GcpCompute.Instances.stop/3`.

# `terminate`

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

Delete an instance and wait for it to be gone.

Returns `:ok` — not `{:ok, _}` like every sibling — because the completed
delete operation carries nothing a caller needs. Errors still arrive as
`{:error, %GcpCompute.Error{}}`; use `delete_instance_and_wait/3` if you want
the operation itself.

# `wait_for_operation`

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

See `GcpCompute.Operations.poll_until_done/3`.

---

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