Packages
gcp_compute
0.2.0
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
Current section
Files
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