DRIVE_FILE, DriveError is PipelineError('drive.json', True)
Deploy runs the whole deploy through one script. When something goes wrong halfway, it helps to run the parts separately and see what each one printed. Drive is a pipeline whose steps are six short Python snippets, each calling one library function: write the Dockerfile, write the compose stack, open the tunnel, point an extra hostname at it, provision the server, and push the secrets to GitHub. You can read each snippet, run it, fix the cause of a failure and run it again.
Drive saves its plan to drive.json, apart from the Deploy plan, so neither overwrites the other. DriveError is the same class as PipelineError.
One deploy operation as a code template that can run as a step on its own.
A part is one deploy operation written as a code template. Part holds the package the code calls, a label and description for display, the code with {placeholders} for the project’s values, the keys the code reads (needs), the keys it stores (writes), and the files it creates (produces).
PARTS holds the six parts in the order they run. image writes the Dockerfile that dockeasy detects. stack wraps that image in a compose file with Caddy and cloudflared. tunnel creates or reuses the Cloudflare tunnel and stores its token. dns points SECOND_HOST at the same tunnel, and stops with a message when that key is unset. server provisions the Hetzner server if it is missing, copies the project over and starts the stack. secrets pushes the declared keys to GitHub so that CI can run the same deploy.
image dockeasy Dockerfile needs=[] writes=[]
stack vpseasy compose stack needs=[] writes=[]
tunnel cfeasy Cloudflare tunnel needs=['CLOUDFLARE_API_TOKEN'] writes=['CF_TUNNEL_TOKEN']
dns cfeasy extra hostname needs=['CLOUDFLARE_API_TOKEN'] writes=[]
server vpseasy server and rsync needs=['HCLOUD_TOKEN'] writes=['HETZNER_IP']
secrets gheasy push secrets needs=['GITHUB_TOKEN'] writes=[]
The project values every part’s template is filled with: names, host, tunnel, port and file lists.
context works out, once, every value the templates use. The app name comes from gheasy’s config, then the name in pyproject.toml, then the folder name. DOMAIN from the store is split into a subdomain and a base domain when it has more than one dot, and the tunnel name follows the same rule as in deploy.py. keys lists the keys in gheasy’s env_schema, or just DOMAIN when there is none.
{'app': 'demo',
'host': 'app.example.com',
'domain': 'example.com',
'subdomain': 'app',
'tunnel': 'app_example.com',
'port': '8080',
'keys': ['DOMAIN']}
_step fills a part’s template from the context and wraps it in a Step whose argv runs the code with python -c. The filled-in code is also kept in meta['code'], so a user interface can show exactly what will run. This is the tunnel part for the example project:
cfeasy · Cloudflare tunnel | python -c … cfeasy | ['CLOUDFLARE_API_TOKEN']
from cfeasy import CF
from dockeasy import env_get, env_set
cf = CF(token=env_get('CLOUDFLARE_API_TOKEN'))
tid, tok = cf.setup_tunnel('example.com', 'app' or None, tunnel_name='app_example.com')
env_set('CF_TUNNEL_TOKEN', tok)
print('tunnel', tid, 'for', 'app.example.com')
Every part as a runnable Step, its template filled with this project’s values.
parts does the same for all six parts, with one context and the interpreter you choose (this process’s Python by default). This is the last one, which pushes the secrets:
from gheasy import gh_push_env
from dockeasy import env_get
keys = ['DOMAIN']
vals = {k: v for k in keys if (v := env_get(k, default=''))}
print('pushing', len(vals), 'of', len(keys), 'declared keys')
gh_push_env(vals, path='/proj')
The deploy as six separate library calls, each a step you can run on its own.
Drive uses these rendered parts as its default plan. Until you save a plan, refresh (which state calls) renders the parts again whenever the values in the store change, so changing DOMAIN changes the snippets. Once a plan is saved it is used as written. state also returns the context and the four packages involved.