GcpCompute (GcpCompute v0.3.0)

Copy Markdown View Source

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

Why REST?

The Compute Engine API is REST/JSON only — it has no gRPC endpoint. This library talks to it with 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.

Summary

Functions

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

Load (and cache) a config from application env.

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

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

Spawn an instance and wait until it's running.

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

Delete an instance and wait for it to be gone.

Functions

clear_config(otp_app, key \\ :gcp_compute)

@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(otp_app, key \\ :gcp_compute)

@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

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:

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(config, name, opts \\ [])

@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(config, name, opts \\ [])

@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(app, key)

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

See GcpCompute.Config.from_env/2.

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

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

See GcpCompute.Instances.get/3.

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

@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(config, instance, opts \\ [])

@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(config, name, opts \\ [])

@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(config, name, opts \\ [])

@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?(config, name, opts \\ [])

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

See GcpCompute.Instances.preempted?/3.

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

@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(config, name, opts \\ [])

@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

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(config, opts \\ [])

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

See GcpCompute.Instances.list/2.

list_instances_page(config, opts \\ [])

@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(opts)

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

See GcpCompute.Config.local/1.

new(opts)

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

See GcpCompute.Config.new/1.

production(opts)

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

See GcpCompute.Config.production/1.

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

@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(config, name, opts \\ [])

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

See GcpCompute.Instances.start/3.

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

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

See GcpCompute.Instances.stop/3.

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

@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(config, operation, opts \\ [])

@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.