infra

Hetzner servers and Cloudflare tunnels, zones and DNS records, with tokens from the project’s store.

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.


source

InfraError

def InfraError(
    *args, **kwargs
):

Unspecified run-time error.

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 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:

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 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:

_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 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 tells the two apart, so the install hint is only offered when installing would help.


source

Infra

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 raises InfraError with the import failure attached as its cause.

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 is used:

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:

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:

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 appears as its message alone, and any other exception with its type name in front:

o = Infra(env={'HCLOUD_TOKEN': '', 'CLOUDFLARE_API_TOKEN': ''}).overview()
o['zones']
{'rows': [],
 'error': 'CLOUDFLARE_API_TOKEN is not set — add it in Workflows → Environment'}