Documentation
¶
Overview ¶
Package cloud resolves the two things forge needs to talk to a hosted control plane, and keeps them apart on purpose:
- the ENDPOINT, which is a per-environment fact declared in that environment's KCL (forge.ControlPlane) and lives in git.
- the CREDENTIAL, which is an opaque bearer string that must never live in git, resolved at call time from a flag, an environment variable, the shared credentials file `forge login` writes (forge/pkg/credentials) keyed by THAT endpoint, or a credential helper the host application names (forge/pkg/cloudcred).
This split mirrors internal/secrets: KCL emits the DECLARATION, Go resolves the VALUE. It is also why there is no `forge context use`. A stateful "current endpoint" would make the answer to "which server did that command hit" depend on machine-local state that never appears in a diff; deriving it from the env argument makes the answer a property of the repository instead.
Nothing here imports control-plane or reliant, and nothing may. The endpoint is an arbitrary URL and the credential an opaque string — the same relationship forge's External deploy target has with flyctl, which needs FLY_API_TOKEN without forge linking Fly's SDK.
Index ¶
- Constants
- Variables
- func CredentialFromToken(tok *oauth2.Token, now time.Time) credentials.Credential
- func CredentialsPath() (string, error)
- func DeviceLabel() string
- func LoginScopes() []string
- func OpenBrowser(target string) error
- func ResolveOrganization(ctx context.Context, c Caller) (string, error)
- func SignInHint() string
- func VerifyToken(ctx context.Context, ep Endpoint, cred Credential) error
- type BrowserLogin
- type Caller
- type Client
- type Credential
- type CredentialSource
- type Declaration
- type Endpoint
- type Error
- type ErrorDetail
Constants ¶
const ( // CodeNotFound: the thing does not exist. Several reads turn this into // an ANSWER rather than a failure — "never cut" is not an error. CodeNotFound = "not_found" // CodeAlreadyExists: a create collided with an existing row. CodeAlreadyExists = "already_exists" // CodeFailedPrecondition: the server refused given the current state. // The category every promote refusal shares; Reason says which. CodeFailedPrecondition = "failed_precondition" // CodeInvalidArgument: the request itself was wrong. CodeInvalidArgument = "invalid_argument" // CodeUnimplemented: this control plane does not serve the procedure. CodeUnimplemented = "unimplemented" // CodeUnauthenticated and CodePermissionDenied: the credential was // missing/stale, or lacks the scope. CodeUnauthenticated = "unauthenticated" CodePermissionDenied = "permission_denied" CodeDeadlineExceeded = "deadline_exceeded" CodeResourceExhausted = "resource_exhausted" )
The Connect error codes forge branches on. Named so a caller never writes the literal, and never searches for it in a message.
const ClientID = "forge-cli"
ClientID is forge's OAuth client id at a control plane.
const CredentialOrder = "--token, then the env's declared token_env, then what `forge login` stored, " + "then the credential helper $" + cloudcred.HelperEnv + " (a host application's session — Reliant sets it)"
CredentialOrder is the resolution order in one line, for help text and errors that explain where a credential comes from.
const DefaultEndpoint = "https://admin.reliantapi.com"
DefaultEndpoint is Reliant's production control plane: the one a plain `go install`'d forge targets with no configuration.
It is the Go copy of KCL's forge.RELIANT_CLOUD_ENDPOINT, which is what `forge.ControlPlane {}` resolves to. KCL owns the default for every env; this copy exists for the one command that runs with no env to render — `forge login` outside a project (or in one that declares no control plane). TestControlPlaneDefault_KCLMatchesGo renders the schema and fails if the two drift.
It is the PUBLIC origin serving forge's whole hosted surface: the controlplane.v1.* RPCs and the CLI OAuth endpoints (/oauth/authorize, /oauth/token). reliant's API host is a different server and answers 404 for both, so pointing this at it would break login for every user.
const DefaultTokenEnv = "FORGE_CONTROL_PLANE_TOKEN"
DefaultTokenEnv is the environment variable forge reads a control-plane credential from when an environment's ControlPlane declaration does not name a different one.
const ReasonHeader = "x-forge-error-reason"
ReasonHeader is the Connect error-metadata key under which a control plane delivers a stable, machine-readable domain reason code. It matches forge's own server-side convention (pkg/svcerr.ReasonHeader), and the value is an app-defined snake_case code such as "promotion_conflict".
Declared here rather than imported from pkg/svcerr on purpose: this is the CLIENT half of the contract, and it must not acquire a dependency on forge's server library to read one header name. HTTP header keys are case-insensitive, so the canonicalization does not matter.
Variables ¶
var ErrNoCredential = errors.New("no control-plane credential")
ErrNoCredential is returned when no credential could be resolved. Use errors.Is to detect it; the message is already actionable (it names the remedy for this machine — `forge login`, or signing in to the host application — and the environment variable), so callers should surface it as-is rather than wrapping it in a generic auth failure.
Functions ¶
func CredentialFromToken ¶
func CredentialFromToken(tok *oauth2.Token, now time.Time) credentials.Credential
CredentialFromToken turns a token response into the stored entry.
func CredentialsPath ¶
CredentialsPath is where the shared credentials file lives, from $FORGE_HOME, $XDG_CONFIG_HOME and the home directory. forge/pkg/credentials owns the precedence; the environment is read here, at the application edge.
func DeviceLabel ¶
func DeviceLabel() string
DeviceLabel names this machine in the minted token's name (forge-cli@<device>). A re-login from the same device replaces that device's token server-side rather than accumulating new ones.
func LoginScopes ¶
func LoginScopes() []string
LoginScopes is what `forge login` asks for: deploy, manage secrets and manage custom domains for the caller's organization. The control plane grants the intersection of this list, the scopes the forge-cli client is allowed, and what the user's role grants, and reports it — so an older control plane or a member without a grant gets a narrower token, never a refusal.
func OpenBrowser ¶
OpenBrowser launches the platform's URL handler.
func ResolveOrganization ¶
ResolveOrganization asks the control plane which organization the credential behind c acts for.
forge does not DECLARE the organization: it is a fact about the credential, and a declared copy could only ever agree with it or be wrong. Reading it from the authority that enforces it (the registry realm scopes pushes to this same org) leaves nothing to drift.
The id is a lowercase UUID, because that is the path segment the platform registry's grammar uses (`<org>/<project>/<image>`).
func SignInHint ¶
func SignInHint() string
SignInHint is the "authenticate and retry" remedy for THIS process. Under a host application that provides a credential helper, the user is meant to be signed in once, to the host — so the remedy is the host's sign-in, not a second login to forge. A standalone forge says `forge login`.
func VerifyToken ¶
func VerifyToken(ctx context.Context, ep Endpoint, cred Credential) error
VerifyToken checks a credential against the endpoint before storing it, so `forge login --token` fails at login rather than at the first real command.
Types ¶
type BrowserLogin ¶
type BrowserLogin struct {
// Endpoint is the control plane being logged in to.
Endpoint Endpoint
// OpenURL is the browser launcher. Injectable so a test drives the
// whole flow without opening a real browser.
OpenURL func(string) error
// Timeout bounds the wait for the browser. A login that hangs forever is
// indistinguishable from one about to succeed.
Timeout time.Duration
// Out receives the "open this URL" notice.
Out io.Writer
}
BrowserLogin runs the interactive half of `forge login`: the OAuth 2.0 authorization-code flow with PKCE (RFC 7636) against the endpoint's /oauth/authorize and /oauth/token, with a loopback redirect (RFC 8252).
The mechanics are forge/pkg/oauth2.LoopbackLogin, which `reliant auth login` also runs. forge depends on no reliant code, deliberately. A user deploying to hosted infra without the reliant harness must never have to install reliant first, so `forge login` stands alone.
func (BrowserLogin) Run ¶
func (b BrowserLogin) Run(ctx context.Context) (credentials.Credential, error)
Run performs the flow and returns the credential to store.
type Client ¶
type Client struct {
Endpoint Endpoint
Credential Credential
HTTP *http.Client
// contains filtered or unexported fields
}
Client calls a hosted control plane over Connect's HTTP+JSON binding: POST <base>/<package>.<Service>/<Method> with a JSON body and a JSON reply.
WHY HTTP+JSON AND NOT A GENERATED CLIENT. A generated client would mean vendoring the control plane's protos into forge, which is the one coupling that must not ship — it would make forge's deploy story specific to our control plane in exactly the way forge's External target is deliberately NOT specific to Fly.io. Connect's JSON binding is a documented, stable part of the protocol and needs nothing but net/http, so the wire contract stays a contract rather than a shared build dependency.
The cost is real and worth naming: forge does not get compile-time checking of request and response shapes against the server's protos. A server-side field rename surfaces as a missing value at runtime rather than a build failure. That is the accepted trade for independence, and it is why Call decodes into explicitly-declared local structs — the shape forge expects is written down in one place instead of inferred.
func NewClient ¶
func NewClient(ep Endpoint, cred Credential) *Client
NewClient builds a Client with a bounded default timeout. A deploy CLI that hangs forever on an unreachable endpoint is worse than one that fails: CI would sit until its own job timeout with no diagnosis.
func (*Client) Call ¶
Call invokes one Connect procedure. procedure is the fully-qualified path WITHOUT a leading slash, e.g. "controlplane.v1.DeployService/ListReleases".
req is marshalled to JSON and out is unmarshalled from the reply. A non-200 becomes a *Error (see error.go), which carries the Connect code, the domain reason and the server's error details as DATA. Callers branch on those fields — never on the message text, which is display copy the server is free to improve.
type Credential ¶
type Credential struct {
Token string
Source CredentialSource
// From is the human-readable origin: the flag name, the env var name,
// the credentials file path, or the helper's description of the
// session it used. Safe to print; never the token itself.
From string
// ExpiresAt is when the token stops being accepted, when known. Only a
// helper's tokens carry one today; nil means unknown.
ExpiresAt *time.Time
}
Credential is a resolved bearer token plus where it came from.
func ResolveCredential ¶
func ResolveCredential(flagToken string, ep Endpoint) (Credential, error)
ResolveCredential returns the bearer credential for ep, in this precedence order:
- flagToken — an explicit --token on the command line
- os.Getenv(ep.TokenEnv) — the env var the environment DECLARED
- the credentials file — what `forge login` stored FOR ep.URL
- the credential helper — $FORGE_CREDENTIAL_HELPER, a host application that holds the user's session (Reliant sets it), minting a short-lived token for ep on demand
WHY THIS ORDER. It runs most-explicit to most-ambient. A flag is typed for one command and can mean nothing else, so it must win — that is what makes a one-off override possible at all, and what makes a bug report reproducible ("run it with --token X"). The env var comes next because CI is where it is used: a pipeline has no browser and no persistent home directory, so the variable IS its credential, and it must beat any file that happens to exist in a cached runner image. The file comes before the helper because a human who ran `forge login` chose that credential for that endpoint deliberately, and a host application signed in as somebody else must not displace it. The helper is last because it is the most ambient of all: it is simply "whoever this machine is signed in to the host as" — the right DEFAULT, and the wrong override.
An EXPIRED `forge login` is not a dead end when a helper is configured: the user is still signed in to the host, so resolution falls through to it rather than demanding a second login. Without a helper it is reported as expired, because then `forge login` really is the fix.
The file lookup is keyed by the endpoint. A login to staging can never be presented to prod: a prod command finds no entry and says so, instead of sending staging's token to prod's server and failing with an indistinguishable 401. The helper is asked FOR ep, and refuses an endpoint its session does not belong to — the endpoint comes from a project's KCL, and a repository is not a trusted party.
type CredentialSource ¶
type CredentialSource string
CredentialSource says where a resolved credential came from. Commands print it so "which credential did that use" never requires guessing — the single most common confusion when a CI run and a laptop disagree.
const ( SourceFlag CredentialSource = "flag" // --token SourceEnv CredentialSource = "env" // the declared token_env SourceLogin CredentialSource = "credentials file" // written by `forge login` // SourceHelper is a token the credential helper named by // $FORGE_CREDENTIAL_HELPER minted from a host application's session // (pkg/cloudcred). forge does not know which application; Credential.From // carries the helper's own description of it. SourceHelper CredentialSource = "credential helper" )
The places a credential can come from, in the precedence order resolution tries them. An explicit flag beats the environment, the environment beats the credentials file on disk, and the file beats a host application's session. That order is what makes a CI run overridable, a human's own `forge login` deliberate, and a signed-in host the ambient default rather than a thing that silently wins.
type Declaration ¶
Declaration is the per-environment control-plane declaration, as read from the environment's rendered KCL.
Declared HERE, at the consumer, rather than importing the CLI's entity type: this package needs two strings, and taking a dependency on the renderer's struct would point the dependency the wrong way (cli depends on cloud, not the reverse) and drag the whole entity graph in behind it.
type Endpoint ¶
type Endpoint struct {
// Env is the environment this endpoint was declared by. Carried so
// error messages can name the KCL file to edit.
Env string
// URL is the base URL, trailing slash trimmed.
URL string
// TokenEnv is the env var NAME the credential is read from.
TokenEnv string
}
Endpoint is one environment's resolved hosted control plane: where to send requests, and which env var holds the credential.
There is deliberately no package-level "current endpoint" and no way to set one. An Endpoint only ever comes from a specific environment's declaration, which is what makes two environments resolvable to two different endpoints in the same process — and what removes the class of bug where a command silently targets whatever was selected last.
func ResolveEndpoint ¶
func ResolveEndpoint(env string, decl *Declaration) (Endpoint, error)
ResolveEndpoint turns one environment's declaration into an Endpoint.
decl == nil means the environment declares no hosted control plane. That is the overwhelmingly common case, so it returns a plain error naming the exact file to edit rather than falling back to DefaultEndpoint. An env that declares nothing is not hosted; defaulting here would mean a project with no hosted anything could send a request somewhere by accident, which is the one outcome this signature is shaped to prevent. The Reliant-cloud default belongs to the DECLARATION (`forge.ControlPlane {}` fills endpoint in KCL), never to its absence.
type Error ¶
type Error struct {
// HTTPStatus is the response status. Kept because a response that is
// not a Connect envelope at all (a proxy's HTML error page) has no
// code, and the status is then the only classification available.
HTTPStatus int
// Code is the Connect error code as its wire name: "already_exists",
// "not_found", "failed_precondition", "unauthenticated". Empty when
// the body was not a Connect envelope.
Code string
// Reason is the app-defined domain reason from ReasonHeader, e.g.
// "promotion_conflict" or "rollout_in_flight". Empty when the server
// sent none.
//
// THIS IS THE FIELD TO BRANCH ON. The Connect code says the category
// (FailedPrecondition), and three different refusals share it; the
// reason says which one, and is the server's committed contract.
Reason string
// Message is the server's own message, verbatim and untruncated. For
// DISPLAY — never for control flow.
Message string
// Details are the Connect error details, each left in its wire form.
//
// NOT DECODED, and that is deliberate: decoding a google.protobuf.Any
// would mean vendoring the control plane's protos into forge, which is
// the one coupling forge's deploy story must not take on. Connect's
// JSON binding includes a `debug` member carrying the message as
// protojson when the server could produce it, so a caller reads the
// payload as JSON (see DetailJSON) without a generated type.
Details []ErrorDetail
// Procedure and Endpoint are where this happened, for the message.
Procedure string
Endpoint string
// contains filtered or unexported fields
}
Error is a failed control-plane call, as a TYPE rather than a sentence.
WHY THIS EXISTS. Call used to flatten every failure into a formatted string, and callers then recovered the classification by searching that string — `strings.Contains(err.Error(), "already_exists")`. That is wrong in both directions. A server that improves its prose breaks forge's control flow, which is a silent break: the branch simply stops matching and the error falls through to a generic failure. And forge's OWN message text contains the words it searches for, so a message that merely mentions a code reads as that code.
Worse, the structured half was discarded entirely. A promote refusal carries a DeployPromoteRefusal detail naming the promotion that actually landed — exactly what a pipeline needs to print instead of "someone else moved prod" — and flattening to a string threw it away.
So every primitive maps REASONS, not message text, to exit codes.
func (*Error) DetailJSON ¶
func (e *Error) DetailJSON(typeName string) (json.RawMessage, bool)
DetailJSON returns the protojson payload of the first detail whose type matches typeName, and whether one was found.
Matches on the SUFFIX after the last '/' and accepts either a bare full name or a type URL, because a server may send "controlplane.v1.X" or "type.googleapis.com/controlplane.v1.X" and both name the same message.
A detail with no `debug` member is reported as not found: the base64 proto bytes alone are unreadable without the descriptor forge deliberately does not have, and returning them would hand a caller a payload it cannot use.
type ErrorDetail ¶
type ErrorDetail struct {
// Type is the protobuf full name, e.g.
// "controlplane.v1.DeployPromoteRefusal".
Type string `json:"type"`
// Value is the base64 (raw std, unpadded) proto binary.
Value string `json:"value"`
// Debug is the same message as protojson, present when the server had
// descriptors for it. This is the member a forge caller reads.
Debug json.RawMessage `json:"debug,omitempty"`
}
ErrorDetail is one Connect error detail in its wire form.