# orca.hcloud

> A module of morca's API.

Hetzner Cloud's API, as the pool autoscaler needs it (#267, `orca.pool_scale`): make a server, list a pool's, delete one, and the architecture of a server type.

var h = HCloud.from_env()        # raises without a token
    var got = h.create(spec)         # Created: ok, or no stock, or why not
    for s in h.servers("orca-pool=lab"): ...
    _ = h.delete(s.id)               # True if it's gone, already or now
    h.arch("cax31")                  # "arm"

The token is `ORCA_HCLOUD_TOKEN`, or the secret `ORCA_HCLOUD_TOKEN_SECRET`
names (`orca.control.fetch_secret`: `ORCA_SECRET_FETCH` on a Hetzner
stack); it's sent as a bearer token and never put in an error. A call
isn't retried and times out after 10 s: the scheduler calls these from its
tick and tries again on the next.
`ORCA_HCLOUD_ENDPOINT` (`https://api.hetzner.cloud/v1`) is for tests'
fake. Hetzner says a type can't be had in a location now with
`resource_unavailable` or `placement_error`, or `server_type_unavailable`
for one it doesn't offer there: `Created.no_stock`, which the autoscaler
waits on, apart from any other refusal, which it reports.

## Structs

### `ServerSpec`

```mojo
struct ServerSpec
```

A server to make.

Implements: `Copyable`, `Movable`

Fields:

- `name: String`: 1 to 63 of a-z, 0-9 and `-`: a host name.
- `server_type: String`
- `location: String`
- `image: String`: The OS image, e.g. `ubuntu-24.04`.
- `user_data: String`: cloud-init.
- `labels: List[Tuple[String, String]]`
- `ssh_keys: List[String]`: Names of the project's SSH keys to put on it; may be empty.
- `firewall: String`: A firewall's name to apply, or empty.

### `Created`

```mojo
struct Created
```

What a create came to: `ok` with the server's `id` and `address`, or `no_stock` (wait and try again, or elsewhere), or neither, with `why`.

Implements: `Copyable`, `Movable`

Fields:

- `ok: Bool`
- `no_stock: Bool`
- `id: String`
- `address: String`
- `why: String`

### `Server`

```mojo
struct Server
```

Implements: `Copyable`, `Movable`

Fields:

- `id: String`
- `name: String`
- `status: String`: `initializing`, `starting`, `running`, `stopping`, `off`, ...
- `server_type: String`
- `location: String`
- `address: String`
- `pool: String`: Its `orca-pool` label, or empty.

### `HCloud`

```mojo
struct HCloud
```

Implements: `Movable`

Fields:

- `endpoint: String`
- `timeout_ms: Int`

#### `__init__`

```mojo
def __init__(out self, endpoint: String, token: String)
```

#### `from_env`

```mojo
def from_env() -> Self
```

From `ORCA_HCLOUD_TOKEN` (or the secret `ORCA_HCLOUD_TOKEN_SECRET` names) and `ORCA_HCLOUD_ENDPOINT`.

#### `arch`

```mojo
def arch(self, server_type: String) -> String
```

The architecture of `server_type` (`arm` or `x86`); raises for a type Hetzner doesn't have.

#### `firewall_id`

```mojo
def firewall_id(self, name: String) -> String
```

The id of the firewall named `name`; raises for none.

#### `create`

```mojo
def create(self, spec: ServerSpec) -> Created
```

Makes the server `spec` says. Hetzner out of `spec`'s type in its location is `no_stock`; any other refusal is `why`.

#### `servers`

```mojo
def servers(self, label_selector: String) -> List[Server]
```

The servers with labels `label_selector` matches (`orca-pool=lab`), every page.

#### `delete`

```mojo
def delete(self, id: String) -> Bool
```

Deletes server `id`: True once it's gone, now or before (404).

## Functions

### `configured`

```mojo
def configured() -> Bool
```

Whether this process has a Hetzner token to use.

### `error_of`

```mojo
def error_of(body: String) -> Tuple[String, String]
```

A Hetzner error body's `error.code` and `error.message`, or empty.

### `is_no_stock`

```mojo
def is_no_stock(code: String) -> Bool
```

## Aliases

### `DEFAULT_ENDPOINT`

```mojo
comptime DEFAULT_ENDPOINT = "https://api.hetzner.cloud/v1"
```

### `TOKEN`

```mojo
comptime TOKEN = "ORCA_HCLOUD_TOKEN"
```

### `TOKEN_SECRET`

```mojo
comptime TOKEN_SECRET = "ORCA_HCLOUD_TOKEN_SECRET"
```

### `ENDPOINT`

```mojo
comptime ENDPOINT = "ORCA_HCLOUD_ENDPOINT"
```

### `NO_STOCK`

```mojo
comptime NO_STOCK = Array[__literal_size__=Int(3)](String("resource_unavailable"), String("placement_error"), String("server_type_unavailable"), __list_literal__=NoneType(None))
```

Hetzner's error codes for "not now, not here".
