🤖 provision
amadan.net/rastrillo/rastrillo/provision
Use provision in a central app, such as a home or a hub, to create tenant instances on CARLOS and tell when each one is ready. A hub also uses it to publish its path map and to check that each tenant's path reaches that tenant.
A home gives each person an instance on its own host. A hub serves every tenant from one hostname, at https://<product>/<slug>/, and the platform sends each prefix to that tenant's instance.
provision keeps no records. It tells you what happened, and your app writes the record. Follow the record rules below, or a crash or a second refresh can leave an instance nobody owns.
Creating an instance
Make the host with NewLabel(slug), then a dot and the tenant app's instance domain. The label is the slug, cut to MaxSlug characters, then a hyphen and six random characters. A slug is lower-case letters, digits and inner hyphens; anything else is ErrSlug.
Pass the host to Create. Only the first call for a host leaves Resume false. Every later call sets Resume: true, so Create checks whether the instance already exists before it posts again.
For a hub, create each tenant in the hub's region. Leave Region empty when the tenant app's default region is the hub's. Otherwise pass the hub's $CARLOS_REGION.
What to do with each result:
Create returns |
What to do |
|---|---|
Created |
The instance exists. Wait for Ready. |
Created with Landed |
An earlier call for this host created it. Treat it as created. |
Created with Accepted |
The platform is still creating it. Keep the record creating and wait for Ready. You do not need to call Create again. |
ErrHostTaken |
Another creation owns this host. In the signup request only, make a new label and create again, at most MaxRelabels times. If a refresh has taken the record over, keep the host instead. |
*RetryError |
The create may have landed, or the platform said to try later. Keep the host. Call again with Resume: true, after After if it is set. |
| Your context's error | The create may have been sent. Keep the host and call again with Resume: true. |
ErrConflict |
The platform refused, for example because instances are not enabled for the app. A new label will not help. Mark the record failed. |
ErrUnauthorized, ErrForbidden or ErrNotFound |
The credential is wrong, lacks the role, or belongs to another app. Fix the credential. Retrying will not help. |
ErrBadRequest |
The platform refuses this host or message. Create checks both first and sends nothing when it can tell. Fix the request. |
Every refusal is an *APIError with the platform's status and sentence, and matches one of the errors above with errors.Is. The errors are for logs and records. Show people your own sentences.
Instance reads the platform's record of a host. A host that does not exist is ErrNotFound. An answer that does not name the host is a *RetryError: ask again later.
Set Config.Observe to log every console call. It never sees the token.
provision cannot delete an instance. A person deletes one with carlos instances delete.
Telling when it is ready
Ready asks the tenant's own host for /api/version. It is true only for a 200 whose Rastrillo-Host header names that host. The platform's setting-up page, a redirect and every other answer are not ready. An error also means not ready yet.
Call Ready once per page refresh, and mark the record live when it is true. In a script or a test, WaitReady checks until the instance is ready or your context ends.
Build every tenant you create with rastrillo.Options.NameInstance set. Without it, Ready and CheckRoute never see the tenant. Set it on the hub too, so CheckRoute can tell when the hub answered.
The probes never follow a redirect. Do not redirect /api/version on the tenant's own host, or it never reads ready.
Publishing a hub's paths
PublishPaths sends the hub's whole path map, from each slug to its tenant's host. The platform replaces the map each time, so build must return every tenant, not only the new one. Include every live tenant, routed or not, and leave out tenants still creating.
Nothing is sent if build fails, if the map is empty (ErrEmptyMap), or if a prefix or host is one the platform would refuse (ErrBadRequest). Read every page of your records in build, and return an error if any page or record fails to read.
A 500 or 502 is retried once. A second one, or a 429, is a *RetryError. Publish again later.
Publishes for one hostname take turns within one process. Two processes can still overwrite each other's new prefix. Republish the map from your records on a daily carlos schedule tick to repair it.
You cannot publish an empty map. When your last live tenant fails, its prefix stays published and may still reach its instance. To clear it, send a PUT of {"paths": {}} to the same console endpoint yourself.
Checking the path
After a publish, CheckRoute asks https://<product>/<slug>/api/version who answered. Call it once per refresh until it returns Routed. It reports one of four states:
Routed: the path reaches the tenant. Mark the record routed and send people there.RoutePending: not known yet. Check again.FellThrough: the hub answered. A new map can take a few seconds to go live, so keep checking until your deadline.NoBasePath: the tenant answered 404 under its prefix. Build it to serve under/<slug>.
Route.Err returns the error for a state, for the record's failure text. WaitRouted checks until the path is routed or your context ends, and its error says why it was not.
Keeping the record
Your app writes one record per tenant. Follow these rules, so that a crash, a retry or two refreshes at once never make a second instance or lose one.
- Write the record as
creating, with its host, before the firstCreate. - In that first write, also store a random token and the signup's deadline, and run the signup's
Createcalls under that deadline. When the signup is done callingCreate, it clears the token. - Keep that host until
CreatereturnsErrHostTaken. After any other error the instance may exist. - Only the signup request that wrote the record calls
CreatewithoutResume. A refresh, a retry button and a repair all passResume: true. - A refresh leaves a record that has a token alone until its deadline. After the deadline, the refresh clears the token with a conditional update before it calls
Create. - Make every change a conditional update, such as
objrec.UpdateJSONwith the version you read. If the record changed (objrec.ErrChanged), read it again and decide again. - Pick a new label only in the signup request, and only with a conditional update on the version the signup wrote. If the record changed, a refresh has taken it over: stop, and keep the host. A record keyed by slug gets the new host. For a record keyed by label, first mark the old record
replaced, naming the new label. Then create the new record, and then delete the old one. - Never call
Createfor areplacedrecord. A refresh that finds one creates the record it names, if that record is missing, and then deletes thereplacedone. - Count new labels in the record, and stop at
MaxRelabels. A count kept in memory starts again on every refresh. - Keep the record
creatinguntilReadyis true, then mark itlive. A hub also keepsrouted, false untilCheckRoutereturnsRouted, and sends people only to a routed tenant. A tenant still not routed at your deadline becomesfailed, with the error's text.
Credentials
Make one Console per credential, and use credentials bound to one app. An account-wide credential reaches every app in the account. A home needs an instance credential for the tenant app. A hub needs that one, and a publish or instance credential for the hub app to publish paths. Keep both in the central app; a tenant needs neither.
Set Config.Account to the account's sqid, not its name. The console answers 404 to a name.
If your hub publishes a leak profile, list two things in it: the hub-app credential can do much more than publish paths, and the Rastrillo-Host header shows any visitor the platform hostname behind each tenant's path.
API
const DefaultTimeout = 30 * time.Second
const MaxSlug = 25
const MaxRelabels = 2
const MaxPaths = 10000
const ProbeTimeout = 5 * time.Second
var ErrSlug, ErrEmptyMap error
var ErrHostTaken, ErrConflict, ErrNotFound, ErrUnauthorized, ErrForbidden, ErrBadRequest error
var ErrFellThrough, ErrNoBasePath, ErrNotRouted error
type Config struct {
Console, Account, App, Token string
HTTP *http.Client
Timeout time.Duration
Observe func(Exchange)
}
type Exchange struct {
Method, Path string
Status int
Header http.Header
Message string
Err error
}
type Console struct{ /* unexported */ }
type CreateRequest struct {
Host, Region, Message string
Resume bool
}
type Created struct {
Host string
Landed, Accepted bool
Operation, Phase string
}
type Box struct{ Box, Region string }
type Instance struct {
Host, App, Account, Region, Channel, CreatedBy string
CreatedAt time.Time
ServedBy []Box
Gated bool
}
type Published struct {
Prefixes int
Changed bool
}
type Answer struct {
URL string
Status int
Version, Build, Instance string
}
type RouteState int // RoutePending, Routed, FellThrough, NoBasePath
type Route struct {
State RouteState
Prefix, Tenant, Hub Answer
}
type APIError struct {
Status int
Message string
}
type RetryError struct {
Status int
After time.Duration
Message string
Err error
}
func New(cfg Config) (*Console, error)
func NewLabel(slug string) (string, error)
func Ready(ctx context.Context, hc *http.Client, host string) (bool, Answer, error)
func WaitReady(ctx context.Context, hc *http.Client, host string, every time.Duration) (Answer, error)
func CheckRoute(ctx context.Context, hc *http.Client, product, slug, host string) (Route, error)
func WaitRouted(ctx context.Context, hc *http.Client, product, slug, host string,
every time.Duration) (Route, error)
func (c *Console) Create(ctx context.Context, req CreateRequest) (Created, error)
func (c *Console) Instance(ctx context.Context, host string) (Instance, error)
func (c *Console) PublishPaths(ctx context.Context, product string,
build func(context.Context) (map[string]string, error)) (Published, error)
func (s RouteState) String() string
func (r Route) Err() error
func (e *APIError) Error() string
func (e *APIError) Is(target error) bool
func (e *RetryError) Error() string
func (e *RetryError) Unwrap() error
Read this page as markdown — exact, unstyled, and cheap for an agent to fetch.