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