CARLOS rastrillo docs

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

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.

  1. Write the record as creating, with its host, before the first Create.
  2. In that first write, also store a random token and the signup's deadline, and run the signup's Create calls under that deadline. When the signup is done calling Create, it clears the token.
  3. Keep that host until Create returns ErrHostTaken. After any other error the instance may exist.
  4. Only the signup request that wrote the record calls Create without Resume. A refresh, a retry button and a repair all pass Resume: true.
  5. 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.
  6. Make every change a conditional update, such as objrec.UpdateJSON with the version you read. If the record changed (objrec.ErrChanged), read it again and decide again.
  7. 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.
  8. Never call Create for a replaced record. A refresh that finds one creates the record it names, if that record is missing, and then deletes the replaced one.
  9. Count new labels in the record, and stop at MaxRelabels. A count kept in memory starts again on every refresh.
  10. Keep the record creating until Ready is true, then mark it live. A hub also keeps routed, false until CheckRoute returns Routed, and sends people only to a routed tenant. A tenant still not routed at your deadline becomes failed, 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.