# infra


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

A deployed app lives on a Hetzner server behind a Cloudflare tunnel.
This module lists and cleans up those resources: servers through
vpseasy, and tunnels, zones and DNS records through cfeasy. It reads the
API tokens, `HCLOUD_TOKEN` and `CLOUDFLARE_API_TOKEN`, from the
project’s environment store, so you do not have to export them first.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/pullup/blob/main/pullup/infra.py#L22"
target="_blank" style="float:right; font-size:smaller">source</a>

### InfraError

``` python
def InfraError(
    *args, **kwargs
):
```

*Unspecified run-time error.*

[`InfraError`](https://vedicreader.github.io/pullup/infra.html#infraerror)
is the error meant to be shown to a person: a library that is missing, a
token that is not set, a name that does not exist. A failure from the
provider or the network keeps its own type, and `overview` reports the
two differently.

vpseasy’s `Hetzner` client reads `HCLOUD_TOKEN` from the process
environment when it is created.
[`_with_env`](https://vedicreader.github.io/pullup/infra.html#_with_env)
sets values in `os.environ` for the length of a `with` block and then
puts back what was there before, removing keys that did not exist. Empty
values are not set:

``` python
os.environ['PULLUP_DEMO'] = 'before'
with _with_env({'PULLUP_DEMO': 'during', 'PULLUP_ABSENT': 'x'}):
    inside = os.environ['PULLUP_DEMO'], os.environ['PULLUP_ABSENT']
inside, os.environ['PULLUP_DEMO'], 'PULLUP_ABSENT' in os.environ
```

    (('during', 'x'), 'before', False)

The values are restored even when the block raises, which matters
because creating a client is what runs inside it.

[`_record`](https://vedicreader.github.io/pullup/infra.html#_record)
turns a Cloudflare DNS record into a flat row. Missing text fields
become empty strings and `ttl` is kept as given. `tunnel` is true when
the record’s content ends in `.cfargotunnel.com`, the address of a
Cloudflare tunnel:

``` python
_record({'id': 'a1', 'type': 'CNAME', 'name': 'app.example.com',
         'content': '9f2b.cfargotunnel.com', 'proxied': True, 'ttl': 1})
```

    {'id': 'a1',
     'type': 'CNAME',
     'name': 'app.example.com',
     'content': '9f2b.cfargotunnel.com',
     'proxied': True,
     'ttl': 1,
     'tunnel': True}

[`installed`](https://vedicreader.github.io/pullup/stack.html#installed)
says whether a module can be found, which is not the same as whether it
imports: a package whose code fails at import time is found, and then
breaks at the call.
[`_why`](https://vedicreader.github.io/pullup/infra.html#_why) tells the
two apart, so the install hint is only offered when installing would
help.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/pullup/blob/main/pullup/infra.py#L52"
target="_blank" style="float:right; font-size:smaller">source</a>

### Infra

``` python
def Infra(
    env:NoneType=None
):
```

*Hetzner servers and Cloudflare tunnels, zones and DNS records, using
this project’s tokens.*

When vpseasy or cfeasy is installed but fails to import,
[`Infra`](https://vedicreader.github.io/pullup/infra.html#infra) raises
[`InfraError`](https://vedicreader.github.io/pullup/infra.html#infraerror)
with the import failure attached as its cause.

[`Infra`](https://vedicreader.github.io/pullup/infra.html#infra) looks
each token up itself: in the environment store first, then in this
process’s environment, and `''` when neither has it. A store that
raises, such as a locked keychain, is passed over and the environment
answers instead. Any object with a `get(key)` method works as the store,
including a plain dict, which is how a caller supplies tokens without a
keychain. An empty dict counts as no store, and the default
[`EnvStore`](https://vedicreader.github.io/pullup/env.html#envstore) is
used:

``` python
infra = Infra(env={'HCLOUD_TOKEN': 'hc-demo'})
infra.token('HCLOUD_TOKEN'), infra.token('CLOUDFLARE_API_TOKEN')
```

    ('hc-demo', '')

`status` reports which libraries are installed and which tokens are set,
without revealing the values and without sending a request:

``` python
Infra(env={'HCLOUD_TOKEN': 'hc-demo'}).status()
```

    {'vpseasy': True,
     'cfeasy': True,
     'hcloud_token': True,
     'cf_token': False,
     'keys': ['HCLOUD_TOKEN', 'CLOUDFLARE_API_TOKEN']}

The listing methods return trimmed rows: `servers` and `keys` (the
account’s SSH keys) from Hetzner, and `tunnels`, `zones` and `records`
from Cloudflare. `tunnels` leaves out deleted tunnels, and `records`
takes a zone’s name or its 32-character id. `verify` runs cfeasy’s token
check, so a missing permission shows up before a deploy runs into it.
`delete_server` and `delete_tunnel` remove one resource each, and
`delete_server` first checks that a server of that name exists.

Both deletions check their argument before anything else, so an empty
name fails without a request:

``` python
test_fail(lambda: Infra(env={'HCLOUD_TOKEN': 'hc-demo'}).delete_server(' '), contains='name the server')
test_fail(lambda: Infra(env={'HCLOUD_TOKEN': 'hc-demo'}).delete_tunnel(''), contains='name the tunnel')
```

`overview` fetches servers, tunnels and zones in one call. Each section
is `{rows, error}`, so a Cloudflare failure cannot hide the servers. An
[`InfraError`](https://vedicreader.github.io/pullup/infra.html#infraerror)
appears as its message alone, and any other exception with its type name
in front:

``` python
o = Infra(env={'HCLOUD_TOKEN': '', 'CLOUDFLARE_API_TOKEN': ''}).overview()
o['zones']
```

    {'rows': [],
     'error': 'CLOUDFLARE_API_TOKEN is not set — add it in Workflows → Environment'}
