gatus: watch the 99stack runway

99stack is prepaid: when the balance runs out the servers stop. A pulled
check on the account API alerts when billing.secondsdue - the provider's own
runway at current spend - drops under 14 days: checked every 6h, reminding
every 24h. gatus_endpoint gains a `headers` field for APIs behind a
credential.

The vault gains ninetyninestack_api_token (account-wide; 99stack has no
read-only scope) and gatus_push_tokens.arbret, used by the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
counterweight 2026-10-19 22:10:26 +02:00
parent d50774c39a
commit 3be5e7934c
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
4 changed files with 277 additions and 219 deletions

View file

@ -5,7 +5,8 @@
# is the right way round for them: all three are about how the outside world
# sees us, so they must be measured from outside. Gatus polls from the
# observability host and needs nothing installed anywhere else - there is no
# script, no timer and no token, because nothing is reporting in.
# script, no timer and no push token, because nothing is reporting in. (The
# provider-balance check does carry an API credential - see below.)
#
# That also means these have no heartbeat. A heartbeat answers "did the thing
# that was supposed to report in do so"; when Gatus does the checking itself,
@ -150,9 +151,50 @@
'minimum-reminder-interval': '6h'}]}] }}"
loop: "{{ public_tcp }}"
# ── Provider balance ─────────────────────────────────────────────────────
# 99stack is prepaid: when the balance runs out the servers stop. The one
# pulled check here that carries a credential - unlike everything above,
# the API will not answer anonymously (403 "Authorization token is
# missing").
#
# The condition is RUNWAY, not balance. billing.secondsdue is the
# provider's own "seconds until you run out at current usage", so it
# follows the spend as servers come and go; a fixed balance threshold
# would be one more number written down once and left behind. 336h of
# runway, same as domain expiry, because topping up is a manual act too.
#
# The token is account-wide - 99stack has no read-only scope - so it can
# create and destroy servers. It lands in Gatus's endpoints file (0640
# root:gatus), the same place as the push tokens.
- name: Require the 99stack API token
ansible.builtin.assert:
that:
- ninetyninestack_api_token is defined
- ninetyninestack_api_token | length > 0
fail_msg: >-
ninetyninestack_api_token must be in group_vars/all/vault.yml - an
API token from the 99stack control panel.
quiet: true
- name: Build the provider endpoints
ansible.builtin.set_fact:
provider_endpoints:
- name: 99stack-balance
group: provider
url: https://api.99stack.com/v1.5/account/info
interval: 6h
headers:
Authorization: "Bearer {{ ninetyninestack_api_token }}"
conditions:
- "[STATUS] == 200"
- "[BODY].billing.secondsdue > {{ 14 * 86400 }}"
alerts:
- {type: signal, failure-threshold: 1, success-threshold: 1,
send-on-resolved: true, minimum-reminder-interval: 24h}
- name: Register the public-facing endpoints
ansible.builtin.include_role:
name: gatus_endpoint
vars:
gatus_endpoint_name: public
gatus_endpoint_pulled: "{{ domain_endpoints + dns_endpoints + http_endpoints + tcp_endpoints }}"
gatus_endpoint_pulled: "{{ domain_endpoints + dns_endpoints + http_endpoints + tcp_endpoints + provider_endpoints }}"