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 ¶
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" )
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 = 1
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.
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 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.
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 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 confers no authorization by itself; it is sent only as the
// ?key= parameter on Google's identity endpoints, never to fulfillmenttools.
FirebaseAPIKey string `yaml:"firebaseApiKey"`
// 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"`
// 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 and tokens 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.
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"`
}
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.