CrowdControl.Provider.Gce.API (crowd_control v0.1.1)

Copy Markdown View Source

Every gcp_compute call in the project, and the error vocabulary built on top of it.

The same confinement CrowdControl.Backend.Docker.API and CrowdControl.Backend.Kubernetes.API provide: one file to audit when the client library moves, and one place where %GcpCompute.Error{} becomes {:error, {:gce, _}}. CrowdControl.Provider.Gce never handles a GcpCompute struct — not even a successful one. get_instance/3 and list_all/3 return plain maps, which is also what lets the provider's tests build instances by hand.

Vocabulary

  • {:gce, {:not_found, message}} — HTTP 404, wherever it surfaced. Gce.release/1 treats it as success, which is what makes teardown idempotent.
  • {:gce, {:http_status, status, message}} — any other non-2xx.
  • {:gce, {:transport, message}} — the request never got an answer.
  • {:gce, {:operation_timeout, message}} — an instances.insert or instances.delete operation that did not reach DONE in time. The instance may exist anyway, which is why the provider rolls back on this.
  • {:gce, {:operation_failed, message}} — reached DONE carrying an error.
  • {:gce, {:token_fetch_failed, message}}, {:gce, {:missing_token, _}} — credentials, decided before the network.
  • {:gce, {:bad_spec, message}}GcpCompute.Instance.spec/1 rejected the options, before any VM existed.
  • {:gce, {:bad_config, message}} — no usable client config.
  • {:gce, {:list_page_limit, pages}} — pagination did not terminate. An error, never a short list; see list_all/3.

Only messages travel, never %GcpCompute.Error{}'s :body: that field can hold the Req exception whose request headers carry the bearer token. The dep's own Inspect redacts it, but an error tuple that reaches a crash report or Logger metadata is not always rendered through Inspect.

Summary

Types

A %GcpCompute.Config{}.

One instance, flattened to plain data.

Functions

Build the client config for opts.

Delete an instance and wait for the operation, treating 404 as success.

Raise unless the optional :gcp_compute dependency is available.

Create an instance and wait for the operation to finish.

Build and validate the instances.insert body.

Every instance matching filter, following nextPageToken to the last page.

The project a config points at.

The zone a config points at.

Types

config()

@type config() :: struct()

A %GcpCompute.Config{}.

Opaque here on purpose: it belongs to the optional dep, it holds a live token-provider argument, and it must never reach a persisted handle.

instance()

@type instance() :: %{
  name: String.t() | nil,
  status: String.t() | nil,
  external_ip: String.t() | nil,
  internal_ip: String.t() | nil,
  created_at: DateTime.t() | nil,
  labels: %{optional(String.t()) => String.t()},
  metadata: %{optional(String.t()) => String.t()}
}

One instance, flattened to plain data.

external_ip/internal_ip are the first of each; metadata is the metadata.items list collapsed into a map, which is where the raw owner and the agent token live.

Functions

config(opts)

@spec config(keyword()) :: {:ok, config()} | {:error, term()}

Build the client config for opts.

Either a %GcpCompute.Config{} passed as :gce_config, or one built from :project, :zone, :token_provider (and the rest of GcpCompute.Config.new/1's options).

Application env under :gce fills in whatever opts omits. That fallback is the reattach path rather than a convenience: a persisted handle carries no token provider — it would be a live credential at rest — so the node that reconnects gets its client config from configuration, not from the Store record.

config :crowd_control,
  gce: [project: "my-project", zone: "us-central1-a"]

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

@spec delete_and_wait(config(), String.t(), keyword()) :: :ok | {:error, term()}

Delete an instance and wait for the operation, treating 404 as success.

Already-gone is the desired end state, which is what makes Gce.release/1 idempotent across the several teardown paths that call it.

ensure_gcp_compute!()

@spec ensure_gcp_compute!() :: :ok

Raise unless the optional :gcp_compute dependency is available.

Called from Gce.acquire/1 only, so that merely loading this module — as CrowdControl.Reaper does when it walks configured backends — never raises.

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

@spec get_instance(config(), String.t(), keyword()) ::
  {:ok, instance()} | {:error, term()}

Read one instance.

insert_and_wait(config, spec, opts \\ [])

@spec insert_and_wait(config(), map(), keyword()) ::
  {:ok, instance()} | {:error, term()}

Create an instance and wait for the operation to finish.

The operation, not the guest: this returns while Debian is still booting and the guest agent has not yet seen the SSH key. Reachability is CrowdControl.Provider.Gce.Tunnel's problem and readiness is GET /v1/health's.

instance_spec(opts)

@spec instance_spec(keyword()) :: {:ok, map()} | {:error, term()}

Build and validate the instances.insert body.

Fails before anything is created, which is the only failure on the acquire path that needs no rollback.

list_all(config, filter, opts \\ [])

@spec list_all(config(), String.t() | nil, keyword()) ::
  {:ok, [instance()]} | {:error, term()}

Every instance matching filter, following nextPageToken to the last page.

Never GcpCompute.Instances.list/2: it returns one page and no indication that there were more. A short list is not a cosmetic bug here. CrowdControl.Reaper reads this as the evidence of what is live, so a live sandbox missing from it is live? = no, stored? = yes — and the reaper deletes the store record of a running, billed VM, orphaning it permanently. Truncation must therefore be impossible, and any page failure surfaces as {:error, _} rather than a shorter list. This is the same hazard, and the same shape, as CrowdControl.Backend.Kubernetes.API.list_all/3.

project(config)

@spec project(config()) :: String.t()

The project a config points at.

zone(config)

@spec zone(config()) :: String.t()

The zone a config points at.

Read back out of the built config rather than out of the caller's options, so that :gce_config and [project:, zone:] produce identical handles and the library's own default zone can never disagree with the handle's.