Documentation
¶
Overview ¶
Package config holds fft's project model and its YAML persistence.
A project is one fulfillmenttools tenant plus the Firebase project that authenticates against it. Users configure a project once and switch between them with `fft project use`.
Why not viper ¶
Viper is the natural reach for a CLI config file, and it is the wrong tool here: it lower-cases every key on both read and write. `activeProject` comes back as `activeproject`, `baseUrl` as `baseurl`, and a load-then-save cycle silently rewrites the user's file into keys nothing reads. So the file is persisted with gopkg.in/yaml.v3 into the typed structs below, and viper is used only for what it is good at: the flag → env → default precedence chain on the global flags.
Index ¶
- Constants
- Variables
- func CandidateEmail(username, projectID, environment string) string
- func DefaultPath() (string, error)
- func EmulatorBaseURL(port int) string
- func HistoryFromEnv(lookup func(string) (string, bool)) (enabled, set bool)
- func NormalizeBaseURL(raw string) (string, error)
- func ReadOnlyDenied(v string) bool
- func ReadOnlyFromEnv(lookup func(string) (string, bool)) bool
- type Config
- type EnvVar
- type Error
- type Project
- type Settings
- type Store
Constants ¶
const ( OutputTable = "table" OutputJSON = "json" OutputYAML = "yaml" )
Output formats a project may default to.
const ( EnvBaseURL = "FFT_BASE_URL" EnvFirebaseAPIKey = "FFT_FIREBASE_API_KEY" EnvEmail = "FFT_EMAIL" EnvPassword = "FFT_PASSWORD" EnvIDToken = "FFT_ID_TOKEN" EnvUsername = "FFT_USERNAME" EnvProjectID = "FFT_PROJECT_ID" EnvEnvironment = "FFT_ENVIRONMENT" EnvTenant = "FFT_TENANT" // EnvEnv is an alias for EnvEnvironment, matching the --env flag of // `fft project add`. A user who wrote --env and then exported the same value // should not have to discover that the variable is spelled differently. EnvEnv = "FFT_ENV" // EnvReadOnly refuses every request that would change the tenant, whatever the // project's own configuration says. See [ReadOnlyFromEnv]. EnvReadOnly = "FFT_READ_ONLY" // EnvHistory switches the local request history on or off, whatever the config // file says. See [HistoryFromEnv]. EnvHistory = "FFT_HISTORY" )
Environment variables that describe a headless project.
const EphemeralName = "env"
EphemeralName is the name given to the project synthesized from the environment. It shows up in `fft project list` so that a confused CI log is still readable.
const Version = 2
Version is the schema version written to new config files. It exists so that a future breaking change to the file layout can be detected and migrated rather than misread.
Version 2 moved the Firebase Web API key out of the config file and into the secret store. A v1 file is migrated in place on the next run (the key is swept into the store and the plaintext field dropped); a v2 file read by an older build trips the newer-version guard in Store.Load, which is what we want now that the key no longer lives here.
Variables ¶
var ( // ErrNoActiveProject means nothing told fft which project to use. ErrNoActiveProject = errors.New("no active project") // ErrProjectNotFound means a project was named but is not configured. ErrProjectNotFound = errors.New("project not found") )
Sentinels callers branch on. They are always wrapped in an Error, so they carry exit code 3 with them.
Functions ¶
func CandidateEmail ¶
CandidateEmail builds the synthetic address fulfillmenttools issues for a username: {username}@ocff-{projectId}-{env}.com.
It is a *candidate*, not the truth. Some tenants authenticate with a plain corporate address instead, so this only seeds the sign-in attempt; whatever actually works is what gets stored in Project.Email. It returns "" when it has too little to work with, rather than a plausible-looking wrong answer.
func DefaultPath ¶
DefaultPath is $XDG_CONFIG_HOME/fft/config.yaml, falling back to ~/.config/fft/config.yaml.
func EmulatorBaseURL ¶ added in v0.8.0
EmulatorBaseURL is where an emulator on port answers.
func HistoryFromEnv ¶ added in v0.8.0
HistoryFromEnv reads FFT_HISTORY: whether it records, and whether it was set at all. An unset or empty variable leaves the decision to the config file and to headless mode.
The parse fails closed in the opposite direction to ReadOnlyFromEnv, because what is being guarded is the opposite: history is a record kept on disk, so a typo must not start one. Only an affirmation switches it on, and any other value switches it off.
lookup may be nil, in which case os.LookupEnv is used.
func NormalizeBaseURL ¶
NormalizeBaseURL validates and canonicalises a project's API root.
A bare host is assumed to be https, and a trailing slash is dropped so that joining a path onto the result never produces a double slash. Plain http is refused unless the host is a loopback address: a bearer token sent in the clear to a real fulfillmenttools tenant is a credential leak, and the only legitimate reason to point fft at http is a mock server on localhost.
The host itself is not pinned to a *.fulfillmenttools.com suffix. That is a considered risk, not an oversight: fft is pointed at self-hosted and staging endpoints that do not share the production suffix, and once https is enforced (above) and cross-host redirects are refused (see the tenant client's pinRedirect), an https host the user configured is the host they meant.
func ReadOnlyDenied ¶ added in v0.6.0
ReadOnlyDenied reports whether v is one of the values that turns read-only off — the same short list EnvReadOnly recognises. It is exported for the one other place that parses a read-only value fft did not get through a bound flag: a --read-only passed to a flag-parsing-disabled component command, read as a raw string. Case and surrounding space are forgiven, as they are for the variable.
func ReadOnlyFromEnv ¶ added in v0.2.0
ReadOnlyFromEnv reports whether FFT_READ_ONLY forbids writes.
It is deliberately not part of FromEnv's all-or-nothing set. That set is what it takes to *synthesize* a project — a base URL, a key, an address and a credential — and a policy is none of those things. FFT_READ_ONLY alone must not conjure a project out of nothing; it tightens whichever project fft was going to use anyway, configured or synthesized.
The parse fails closed. The values above are the only ways to say no; anything else is yes, including a typo. A guardrail that a misspelling silently disarms is not a guardrail — and the cost of being wrong in this direction is a refused write, which the user can see and undo.
The empty string is one of the denials, so FFT_READ_ONLY= (set, but to nothing) reads as no. It is the one place where "set" and "yes" come apart, and it is deliberate: `FFT_READ_ONLY=$SOMETHING_UNSET fft …` should behave as though the variable had not been mentioned, not lock the tenant on the strength of a typo in someone else's variable name.
lookup may be nil, in which case os.LookupEnv is used.
Types ¶
type Config ¶
type Config struct {
Version int `yaml:"version"`
ActiveProject string `yaml:"activeProject,omitempty"`
Projects []Project `yaml:"projects,omitempty"`
Settings Settings `yaml:"settings"`
}
Config is the whole of ~/.config/fft/config.yaml.
func (*Config) Remove ¶
Remove deletes the named project and, if it was the active one, clears the active selection. It reports whether the project existed.
type EnvVar ¶ added in v0.8.0
type EnvVar struct{ Name, Value string }
EnvVar is one environment variable and its value.
func EmulatorEnv ¶ added in v0.8.0
EmulatorEnv is the environment that points fft at the emulator serving baseURL, in the order it is shown.
It is the headless id-token path (FromEnv) because that is the only door in: the emulator cannot stand in for Google's sign-in, so `fft project add` has nothing to sign in to and these four variables are the whole of the configuration.
It lives here, beside the variables it names, because three places need exactly the same four lines — the emulator's own start-up notice, the UI's recipe, and the UI's runs — and three hand-written copies of a credential recipe drift.
type Error ¶
type Error struct {
Err error
// contains filtered or unexported fields
}
Error is a configuration problem: no active project, an unknown project name, an unreadable file. It exits 3 and carries a hint telling the user what to run next, which is the difference between an error a user can act on and one they have to go and read the manual about.
func NewError ¶
NewError wraps err as a configuration problem, exiting 3, with a hint telling the user which command fixes it. hint may be empty.
type Project ¶
type Project struct {
// Name identifies the project to the user and namespaces its secrets.
Name string `yaml:"name"`
// BaseURL is the fully-qualified API root, for example
// "https://acme.api.fulfillmenttools.com". It is stored, never derived.
//
// The official docs contradict themselves on whether the host is
// "{projectId}.api…" or "ocff-{projectId}.api…", so fft refuses to guess:
// ProjectID and Environment below exist for display and for building a
// candidate email, and are never used to construct a URL.
BaseURL string `yaml:"baseUrl"`
// FirebaseAPIKey is the Firebase *Web* API key. It identifies the Firebase
// project and is sent only as the ?key= parameter on Google's identity
// endpoints, never to fulfillmenttools.
//
// It is treated as sensitive: hydrated at runtime from the secret store (a
// configured project) or the environment (a headless one) and never written
// to the plaintext config file — hence the yaml:"-".
FirebaseAPIKey string `yaml:"-"`
// LegacyFirebaseAPIKey catches the cleartext key that pre-v2 config files
// stored under firebaseApiKey. It exists only so the startup migration can
// move it into the secret store; nothing else sets it, and once the migration
// clears it, omitempty drops it from the file. See migrateAPIKeys in cmd/fft.
LegacyFirebaseAPIKey string `yaml:"firebaseApiKey,omitempty"`
// Email is the address that actually authenticates. fulfillmenttools users
// often have a synthetic address ({username}@ocff-{projectId}-{env}.com) but
// not always, so whatever value signs in successfully is persisted verbatim
// rather than recomputed.
Email string `yaml:"email"`
// Username is the short login name the email was derived from, kept so that
// `fft project list` can show something a human recognises.
Username string `yaml:"username,omitempty"`
// Tenant, ProjectID and Environment are descriptive only.
Tenant string `yaml:"tenant,omitempty"`
ProjectID string `yaml:"projectId,omitempty"`
Environment string `yaml:"environment,omitempty"`
// ReadOnly refuses every request that would change this tenant. The project
// still gets, lists and searches — including the POST-bodied cursor searches,
// which are reads — and a refused write is refused before fft signs in, so it
// costs no round trip and mints no token.
//
// It protects the *tenant*, not the config file: `fft project use` and
// `fft project remove` still work on a read-only project, because configuring
// fft is not changing anything in the tenant.
//
// omitempty, so that adding this field churns no existing config file.
ReadOnly bool `yaml:"readOnly,omitempty"`
// Ephemeral marks a project synthesized from FFT_* environment variables in
// headless mode. It is never written to disk — hence the yaml:"-".
Ephemeral bool `yaml:"-"`
}
Project is one configured fulfillmenttools tenant. It holds no secrets: the password, tokens and Firebase API key all live in a [secrets.Store].
func FromEnv ¶
FromEnv synthesizes an ephemeral project from the environment, reporting whether one was found.
This is how fft works in CI, and it is not a convenience. A GitHub Linux runner has no Secret Service, so the OS keychain is simply unavailable there; a headless project therefore touches neither the keychain nor the config file — everything it needs comes from the environment and dies with the process.
A project is synthesized only when the base URL, the Firebase Web API key, an email and *some* credential (a password to sign in with, or an id token to use directly) are all present. A partial set is ignored rather than half-honoured: silently falling back to the config file when one variable is missing is how a CI job ends up running against the wrong tenant.
FFT_EMAIL may be left out when FFT_USERNAME, FFT_PROJECT_ID and FFT_ENV/FFT_ENVIRONMENT are given: the synthetic address is then built from them exactly as `fft project add --username` does. It is only a candidate — the sign-in is what settles it — but it means a CI job configures the same four values a human types, rather than having to know how the address is spelled.
The base URL is put through NormalizeBaseURL, the same guard `fft project add` applies: a headless FFT_BASE_URL of http://a-real-tenant would otherwise send the bearer token in the clear, since this path never touched the check. A bad base URL is a hard error — never a silent fall-back to the config file, which is how a CI job ends up running against the wrong tenant — but only once the rest of the set is present, so an unrelated stray FFT_BASE_URL does not fail a run that was never going to be headless.
lookup may be nil, in which case os.LookupEnv is used.
type Settings ¶
type Settings struct {
// Output is the default output format when -o is not given.
Output string `yaml:"output"`
// UpdateCheck enables the once-a-day check for a newer fft release.
UpdateCheck bool `yaml:"updateCheck"`
// NoKeyring stores credentials in a 0600 file rather than the OS keychain, for
// a machine that has none — WSL, a container, a headless server. It is what
// --no-keyring says, made permanent; the flag and FFT_NO_KEYRING still win.
//
// omitempty, so that adding this field churns no existing config file.
NoKeyring bool `yaml:"noKeyring,omitempty"`
// NoHistory stops fft from recording the requests it sends in its local
// history. FFT_HISTORY still wins, as the environment does over every setting.
NoHistory bool `yaml:"noHistory,omitempty"`
}
Settings are the preferences that apply across all projects.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists a Config as YAML.