Overview
Run tasks on a hammerkit server, with the credentials of service accounts and the build files that were published to it.
Experimental. The config and the API can still change between releases, see the remote compute spec.
A hammerkit server runs tasks for other people and agents on a machine that has what the work needs: a Mac mini for iOS builds, a Linux box with big CPUs, the credentials to deploy. Callers never get those credentials, and they can not change what a task does: they name a published build file, the tasks to run and where the code comes from, and the server decides with its config whether they may.
How it fits together
- Who calls. Every call carries an OAuth token of an identity provider the server trusts. The server knows people, CI jobs and agents by the claims of that token only, see Authentication.
- What they may do. Entitlements map callers to
service accounts (
useor onlyrequest), to the repositories they may run from and to what they may do with build files (run,proposeorpublish). - Which credentials. Accounts are the service accounts of the operator, with the secret provider commands that read secrets as them. The secrets of a published build file say which account each is read as, so the account of a run follows from its tasks, never from the request.
- What runs. A run executes tasks of a published build file, pinned to a version, so whoever may run can not change what a task does.
- Which code. The code comes from a git ref of a repository the server reads itself, see Sources, or from files the caller uploads.
- Where it runs. Runs are queued, limited per caller and executed in a process of their own on the backend.
Starting a server
hammerkit server reads its own config file, so it needs no build file.
hammerkit server --config /etc/hammerkit/server.yamlThe server runs until it is interrupted. HAMMERKIT_SERVER_CONFIG can stand in for
--config.
Setting up a server
The pieces, in the order an operator sets them up.
- Identity provider. Register an API audience for the server and list the issuer
under
issuers(Authentication). Nothing else identifies a caller. - Accounts. Define one service account per set of credentials a build needs, and the
secret provider commands that read them (Accounts). A
production account is limited to
source: [ref]and the branches it may run from. - Repositories. List the repositories the server may read under
sources, with a token that a provider command mints, for example from a GitHub App (Sources). - Entitlements. Say who may
useorrequestwhich account, which repositories they may name and whether they mayrun,proposeorpublishbuild files (Entitlements). - Start it.
hammerkit server --config server.yaml, behind a TLS proxy or withtls. SendSIGHUPto read the entitlements again without a restart (Changing entitlements).
A small config for one team on a Linux box, with a GitHub App token and a production account that needs a second person:
listen: { host: 0.0.0.0, port: 8480 }
behindTlsProxy: true
issuers:
- { issuer: https://login.example.com, audience: hammerkit, clientId: hammerkit-cli }
secretProviders:
gsm: { command: [gcloud, secrets, versions, access, latest, '--secret={{ref}}'] }
github-app: { command: [/usr/local/bin/github-app-token, '{{ref}}'] }
accounts:
scm-read: { providers: { github-app: { env: { APP_KEY_FILE: /etc/hammerkit/app.pem } } } }
op-payments-dev: { providers: { gsm: { env: { CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: /etc/hammerkit/dev.json } } } }
op-payments-prod:
providers: { gsm: { env: { CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: /etc/hammerkit/prod.json } } }
source: [ref]
refs: [main]
sources:
- match: github.com/corp/*
secret: { from: 'github-app:payments', account: scm-read }
runs:
env: { allow: [PATH, DOCKER_HOST] }
entitlements:
- group: { iss: https://login.example.com, name: platform }
accounts: { op-payments-dev: use, op-payments-prod: use }
repos: [github.com/corp/*]
builds: publish
- group: { iss: https://login.example.com, name: eng-payments }
accounts: { op-payments-dev: use, op-payments-prod: request }
repos: [github.com/corp/payments]
builds: runA developer then registers and signs in with remote add and remote login,
someone in platform publishes the build file with remote publish, and a run is a
POST /v1/runs (remote run does not exist yet). A run that reads the production secret
waits in remote approvals until a member of platform approves it.
Config
The file belongs to whoever operates the server and is reviewed like code. Unknown keys are an error, so a typo is not silently ignored.
listen:
host: 127.0.0.1 # default
port: 8480 # default
tls:
cert: /etc/hammerkit/tls.crt
key: /etc/hammerkit/tls.key
issuers:
- issuer: https://login.example.com
audience: hammerkit
groupsClaim: groups # default
clientId: hammerkit-cli
scope: openid api://hammerkit/.default
- issuer: https://token.actions.githubusercontent.com # CI jobs, matched by claims
audience: hammerkit
secretProviders: # the commands that read secrets, see Secret providers
gsm:
command: [gcloud, secrets, versions, access, latest, '--secret={{ref}}']
accounts: # the service accounts a secret can be read as
op-payments-dev:
description: Payments, test secrets
providers: { gsm: { env: { CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: /etc/hammerkit/creds/payments-dev.json } } }
op-payments-prod:
description: Payments, production
providers: { gsm: { env: { CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: /etc/hammerkit/creds/payments-prod.json } } }
source: [ref] # only from reviewed code
refs: [main, 'release/*'] # and only from these branches
scm-read: # reads the repositories, no entitlement may name it
description: Read-only access to source repositories
providers: { gsm: { env: { CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: /etc/hammerkit/creds/scm-read.json } } }
entitlements:
- subject: { iss: https://login.example.com, sub: 4f1c2b }
accounts: { op-payments-dev: use, op-payments-prod: request }
builds: run
- group: { iss: https://login.example.com, name: release-managers }
accounts: { op-payments-prod: use }
builds: publish
- claims:
iss: https://token.actions.githubusercontent.com
match: { repository: corp/payments, ref: refs/heads/main }
accounts: { op-payments-dev: use }
repos: [github.com/corp/payments]
builds: publish
sources: # the repositories a run may get its code from
- match: github.com/corp/* # an organisation
secret: { from: 'gsm:github-readonly-token', account: scm-read }
- match: gitlab.corp.com/platform/** # a group with its subgroups
secret: { from: 'gsm:gitlab-group-token', account: scm-read, username: oauth2 }
runtimes: [docker] # what tasks may run on, docker by default
approvals:
timeout: 1h # default
maxWaiting: 100 # default
cooldown: 10m # default, a rejected version can not be proposed again before
storage:
directory: /var/lib/hammerkit # default ~/.hammerkit/server
runs:
maxConcurrent: 4 # default
keep: 200 # default
limits: # per caller (issuer and subject), 429 over it
waiting: 10 # default, runs that wait for an approval
queued: 20 # default, runs that wait for a free place
running: 2 # default, runs that execute at once, the others wait in the queue
maxDuration: 1h # default, a longer run is stopped and fails
maxLogBytes: 1048576 # default, log output kept per run
env:
allow: [PATH, DOCKER_HOST] # the only variables of the server a run sees, none by default
set: { CI: 'true' }
backend:
type: host
platform: { os: macos, arch: arm64 } # detected when left out| Key | What it does |
|---|---|
listen | Interface and port. Port 0 picks a free one. |
tls | Certificate and key to serve https. |
behindTlsProxy | Set to true when a proxy in front terminates TLS. |
issuers | The OAuth issuers whose tokens are accepted, see Authentication. |
secretProviders | The commands that read secrets, see Accounts. |
accounts | The service accounts a secret can be read as, see Accounts. |
entitlements | Who may use or request which account, which repositories and what they may do with build files, see Entitlements. |
approvals | How long requested runs and proposals wait, see Approvals. |
sources | The repositories a run may get its code from, see Sources. |
runs | Concurrency, limits per caller, durations and the environment of runs, see Runs. |
runtimes | What tasks may run on, see Backend. |
backend | Where tasks run and what the machine offers, see Backend. |
storage | Where the server keeps what must survive a restart: the published build files, the uploaded source files, the task cache and the records of the runs (records/). Runs work in runs/ below it while they run. |
Plain http carries OAuth tokens, so it is accepted on the loopback interface only.
Anywhere else the config needs tls, or behindTlsProxy: true.
The server checks the references within the file when it starts: an entitlement has to name a configured issuer and defined accounts, an account only names declared secret providers, and a source reads its token with a provider the account has credentials for.
Discovery
GET /.well-known/hammerkit needs no token and returns what the server is, so a
client can check it before signing in:
{
"name": "hammerkit",
"version": "1.25.0",
"protocol": 1,
"issuers": [
{
"issuer": "https://login.example.com",
"audience": "hammerkit",
"clientId": "hammerkit-cli",
"scope": "openid api://hammerkit/.default"
}
],
"backend": { "type": "host", "platform": { "os": "macos", "arch": "arm64" } }
}GET /healthz answers {"status":"ok"} for a load balancer or a process manager.
Every other path needs a token.