Packages

Spawn and manage Google Compute Engine instances over the REST API, with pluggable auth, telemetry, and an ergonomic instance builder.

Current section

Files

Jump to
gcp_compute lib gcp_compute.ex
Raw

lib/gcp_compute.ex

defmodule GcpCompute do
@moduledoc """
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.
"""
alias GcpCompute.{Config, Instances, Operations}
@doc "See `GcpCompute.Config.new/1`."
defdelegate new(opts), to: Config
@doc "See `GcpCompute.Config.production/1`."
defdelegate production(opts), to: Config
@doc "See `GcpCompute.Config.local/1`."
defdelegate local(opts), to: Config
@doc "See `GcpCompute.Config.from_env/2`."
defdelegate from_env(app, key), to: Config
@call_keys [:zone, :request_id, :timeout]
@doc """
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.)
"""
def config(otp_app, key \\ :gcp_compute) do
pt_key = {__MODULE__, :config, otp_app, key}
case :persistent_term.get(pt_key, nil) do
nil ->
case Config.from_env(otp_app, key) do
{:ok, config} = ok ->
:persistent_term.put(pt_key, config)
ok
error ->
error
end
config ->
{:ok, config}
end
end
@doc """
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).
"""
def clear_config(otp_app, key \\ :gcp_compute) do
:persistent_term.erase({__MODULE__, :config, otp_app, key})
:ok
end
@doc """
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`).
{:ok, vm} = GcpCompute.launch(config, "worker-1", machine_type: "e2-micro", spot: true)
"""
def launch(config, name, opts \\ []) do
{call_opts, spec_opts} = Keyword.split(opts, @call_keys)
Instances.insert_and_wait(config, [name: name] ++ spec_opts, call_opts)
end
@doc "Delete an instance and wait for it to be gone. Returns `:ok`."
def terminate(config, name, opts \\ []) do
case Instances.delete_and_wait(config, name, opts) do
{:ok, _op} -> :ok
{:error, _} = error -> error
end
end
@doc "See `GcpCompute.Instances.insert/3`."
def insert_instance(config, instance, opts \\ []), do: Instances.insert(config, instance, opts)
@doc "See `GcpCompute.Instances.insert_and_wait/3`."
def insert_instance_and_wait(config, instance, opts \\ []),
do: Instances.insert_and_wait(config, instance, opts)
@doc "See `GcpCompute.Instances.get/3`."
def get_instance(config, name, opts \\ []), do: Instances.get(config, name, opts)
@doc "See `GcpCompute.Instances.list/2`."
def list_instances(config, opts \\ []), do: Instances.list(config, opts)
@doc "See `GcpCompute.Instances.list_page/2`."
def list_instances_page(config, opts \\ []), do: Instances.list_page(config, opts)
@doc "See `GcpCompute.Instances.delete/3`."
def delete_instance(config, name, opts \\ []), do: Instances.delete(config, name, opts)
@doc "See `GcpCompute.Instances.delete_and_wait/3`."
def delete_instance_and_wait(config, name, opts \\ []),
do: Instances.delete_and_wait(config, name, opts)
@doc "See `GcpCompute.Instances.start/3`."
def start_instance(config, name, opts \\ []), do: Instances.start(config, name, opts)
@doc "See `GcpCompute.Instances.stop/3`."
def stop_instance(config, name, opts \\ []), do: Instances.stop(config, name, opts)
@doc "See `GcpCompute.Operations.poll_until_done/3`."
def wait_for_operation(config, operation, opts \\ []),
do: Operations.poll_until_done(config, operation, opts)
end