extension

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package extension implements lstk's Git-style extension mechanism: when a user runs `lstk <name>` and `<name>` is not a built-in command, lstk resolves and executes an external `lstk-<name>` executable, forwarding arguments, streams, and the exit code. This mirrors Git's `git-<name>` model and lstk's own IaC proxies, and is the only model that cleanly supports closed-source and third-party extensions written in any language: an extension is an opaque binary that never touches the core repository.

Index

Constants

View Source
const (
	EnvAPIVersion = "LSTK_EXT_API_VERSION"
	EnvContext    = "LSTK_EXT_CONTEXT"
)

The runtime-context contract is conveyed to an extension through exactly two environment variables:

  • EnvAPIVersion (LSTK_EXT_API_VERSION) — a flat integer kept outside the JSON payload so an extension can check contract compatibility before parsing.
  • EnvContext (LSTK_EXT_CONTEXT) — a single JSON object (see Context) carrying the resolved config directory, auth token, non-interactive state, and the list of running emulators.

APIVersion is bumped ONLY on a breaking change (a field removed or repurposed); adding a field does not bump it. Extensions therefore detect additive fields by their presence in the JSON object — not via the version — and use the version only to refuse a contract generation they predate. So any field added after version 1 must be distinguishable when absent (omitempty / pointer / null) for that presence check to work.

View Source
const APIVersion = 1

APIVersion is the integer version of the LSTK_EXT_* runtime-context contract that this lstk implements. It is exposed to extensions as LSTK_EXT_API_VERSION. Bump it only when a variable is removed or repurposed; adding a new variable is an additive change that keeps the same version.

View Source
const BundledBinaryName = "bundled-extensions"

BundledBinaryName is the file name of the multi-call binary that provides every LocalStack-bundled extension. It ships next to lstk (in BundledDir) as one binary rather than one lstk-<name> copy per extension, and dispatches on the name it is invoked as: lstk execs it with argv[0] set to "lstk-<name>" (see Invoke), the busybox/git approach.

This is what makes the bundle deliverable at all. Per-name copies would cost ~30 MB per extension in every archive and npm package; symlink aliases are dropped by the tar extractor, materialized as text files by the zip one, and need elevation to create on Windows. A single binary has none of those problems, at the cost of making the descriptions file load-bearing: it is the only record of which commands the binary provides, so LoadBundle treats a missing or unreadable one as a hard error rather than degrading to an empty set the way LoadDescriptions does for help rendering.

View Source
const DescriptionsFileName = "lstk-extensions.toml"

DescriptionsFileName is the static, hand-authored file shipped alongside the bundled extensions that maps a bundled extension's command name to a one-line description for help rendering. It is a single LocalStack-controlled file (not a per-extension manifest), version-locked to the bundled binaries and validated against them at release time. Its TOML body is a flat table of name = "description" entries, e.g.:

deploy = "Deploy your application to LocalStack"

When the multi-call bundle (BundledBinaryName) is installed the same file is also the record of which commands that binary provides; see LoadBundle for the stricter contract that implies.

View Source
const NamePrefix = "lstk-"

NamePrefix is the executable-name prefix that identifies an extension: an executable named "lstk-<name>" provides the "<name>" extension.

Variables

View Source
var ErrNotFound = errors.New("extension not found")

ErrNotFound is returned by Resolve when no matching extension executable exists in the bundled directory or on PATH.

Functions

func BundledDir

func BundledDir(logger log.Logger) string

BundledDir returns the directory in which lstk looks for bundled extensions: the directory containing the symlink-resolved lstk executable. Resolving symlinks is what makes this work through npm `.bin` links and Homebrew shims, where the invoked `lstk` is a link to the real binary living next to its bundled siblings. It returns "" when the executable path cannot be resolved.

func Invoke

func Invoke(ctx context.Context, ext *Extension, args []string, runCtx Context) error

Invoke executes the resolved extension with args forwarded unmodified, connecting the child's stdin/stdout/stderr to lstk's own so the user's terminal is wired straight through. The runtime context is layered on the inherited host environment.

The invocation is wrapped in an OpenTelemetry span recording the extension name, whether it was bundled, and the exit code, so extension usage is visible when telemetry is enabled (LSTK_OTEL); when telemetry is disabled the global no-op tracer makes this free and emits nothing. lstk does not inject trace context into the extension process, so an extension's own spans do not yet nest under lstk's trace (deferred).

A non-zero exit from the extension is wrapped as a silent error carrying the *exec.ExitError, so the top-level handler propagates the child's exit code as lstk's own (via main.go's errors.As check) without printing an extra lstk-level error line over the extension's output. Modelled on the IaC proxies' exec path.

func InvokeQuietlyWithJSON added in v1.3.0

func InvokeQuietlyWithJSON(ctx context.Context, ext *Extension, args []string, opts QuietOptions) (json.RawMessage, error)

InvokeQuietlyWithJSON runs ext on lstk's own behalf rather than the user's (the counterpart to Invoke) and returns the `data` of the single envelope it prints. It guarantees, regardless of the caller:

  • the extension sees json: true and nonInteractive: true, and no session, machine or endpoint id;
  • LOCALSTACK_DISABLE_EVENTS=1, so the run is not reported as user activity;
  • nil stdin, captured stdout and discarded stderr — nothing reaches the user's terminal;
  • a non-zero exit, unparseable output or a non-"ok" envelope is an error.

It bypasses Invoke/proc.Run on purpose: this is a short captured-output exec, not a wrapped tool the user is driving, so there is no signal to forward. The working directory is inherited.

func LoadDescriptions

func LoadDescriptions(dir string, logger log.Logger) map[string]string

LoadDescriptions reads the bundled descriptions file from dir and returns a map of extension command name to one-line description. A missing or unreadable file degrades to an empty map without error, so help rendering never fails on account of descriptions. dir is the bundled-extensions directory; an empty dir yields an empty map. It serves the pre-bundling shape (standalone lstk-<name> files next to lstk); with the multi-call bundle installed, Resolver.List takes descriptions from the Bundle it already loaded instead.

Types

type Bundle added in v1.2.0

type Bundle struct {
	Path string
	// contains filtered or unexported fields
}

Bundle is the installed multi-call bundle: the path to its binary and the commands it provides with their help descriptions, taken from the descriptions file.

func LoadBundle added in v1.2.0

func LoadBundle(dir string, logger log.Logger) (*Bundle, error)

LoadBundle returns the multi-call bundle installed in dir, or (nil, nil) when dir has no BundledBinaryName executable — the pre-bundling shape, and any user who has only lstk-<name> files there. When the binary IS present, the descriptions file becomes mandatory: a missing, unreadable, malformed, or empty one is returned as an error, because without it lstk cannot know which commands the binary answers to, and silently reporting "unknown command" would hide a broken install.

Every key must also be a dispatchable command name (validate.ExtensionName, the rule the release gate applies to the same file). A key that fails it would make lstk exec the binary under an argv[0] nothing answers to, so it is reported as a broken bundle rather than skipped: the file is LocalStack's own release artifact, and an inconsistency in it is a release bug to surface, not a per-entry condition to tolerate.

func (*Bundle) Description added in v1.2.0

func (b *Bundle) Description(name string) string

Description returns the one-line help description of a bundled command, or "" when the bundle does not provide it.

func (*Bundle) Names added in v1.2.0

func (b *Bundle) Names() []string

Names returns the bundle's extension command names, sorted.

func (*Bundle) Provides added in v1.2.0

func (b *Bundle) Provides(name string) bool

Provides reports whether the bundle provides the given extension command.

type Context

type Context struct {
	ConfigDir      string     `json:"configDir"`
	AuthToken      string     `json:"authToken,omitempty"`
	NonInteractive bool       `json:"nonInteractive"`
	JSON           bool       `json:"json"`
	SessionID      string     `json:"sessionId,omitempty"`
	MachineID      string     `json:"machineId,omitempty"`
	EndpointURL    string     `json:"endpointUrl,omitempty"`
	Emulators      []Emulator `json:"emulators"`
}

Context is the resolved runtime context lstk conveys to an extension, rendered as the LSTK_EXT_CONTEXT JSON object. The command boundary populates it (resolving running emulators, config dir, auth token, interactivity, the resolved --json flag, and the telemetry session id) and Environ renders it. An empty AuthToken is omitted from the JSON; Emulators is always present, marshalling to [] when no emulator is running so an extension always decodes a list.

SessionID is lstk's per-process telemetry correlation id, conveyed so an extension emitting its own telemetry can join it to the ext:<name> event lstk records for the same invocation. It is omitted when lstk telemetry is disabled (there is no session to correlate), which means absence is ambiguous to the extension: it cannot tell a telemetry-disabled lstk from an lstk predating the field. Like every field added after version 1 it is detected by presence, not by LSTK_EXT_API_VERSION.

MachineID is the anonymized machine identity lstk stamps on its own telemetry events — already the salted hash, never a raw Docker or system id — conveyed so an extension emitting telemetry reports the same machine without re-deriving it. Conveying it exposes nothing new: it is the prepared hash, and the child environment already carries AuthToken. It is omitted when lstk telemetry is disabled, which a disabled client makes absent together with SessionID (it computes neither), so absence is ambiguous in the same way SessionID's is.

EndpointURL is the endpoint lstk was asked to target instead of a locally managed emulator, resolved with lstk's own source precedence (--endpoint-url, then LSTK_ENDPOINT_URL, then AWS_ENDPOINT_URL) and conveyed verbatim: it is not validated, normalized, or probed. A malformed or unreachable value reaches the extension unchanged on purpose — judging it is the extension's job, and an extension diagnosing a broken endpoint has to see exactly what the user set. It is omitted when no endpoint source was given, i.e. the invocation targets the default local emulator. Note that Emulators is independent of it: that array is what local Docker discovery found, not what lstk was told to target.

func (Context) Environ

func (c Context) Environ(base []string) ([]string, error)

Environ layers the resolved contract on top of the inherited host environment base (typically os.Environ()), returning a new slice suitable for exec.Cmd.Env. The host environment is preserved so extensions inherit the user's PATH, locale, and tool configuration; only LSTK_EXT_API_VERSION and LSTK_EXT_CONTEXT are added. Any inherited LSTK_EXT_* is stripped first so a stray value cannot shadow lstk's resolved context.

type Emulator

type Emulator struct {
	Type     string `json:"type"`     // emulator type, e.g. "aws", "snowflake", "azure"
	Endpoint string `json:"endpoint"` // full URL, e.g. "http://localhost:4566"
	Port     string `json:"port"`     // resolved host port, e.g. "4566"
}

Emulator describes one running LocalStack emulator in the context payload.

type Extension

type Extension struct {
	Name        string
	Path        string
	Bundled     bool
	Argv0       string
	Description string
}

Extension is a resolved extension executable: its command name (the part after the "lstk-" prefix) and the absolute path to the executable that provides it. Bundled reports whether it was resolved from the bundled-extensions directory (which ships with lstk and takes precedence over PATH) rather than from PATH.

Argv0 is the name the executable is invoked as (its argv[0]). For a standalone lstk-<name> file it is simply the file's own base name; for a command provided by the multi-call bundle (BundledBinaryName) it is "lstk-<name>", which is how that one binary learns which extension to be.

Description is the one-line help text for a bundled extension, taken from the descriptions file beside it. It is empty for PATH extensions (help lists them name-only) and for bundled files the file does not describe. Resolver.List fills it in; Resolve leaves it empty, since dispatch never renders it.

func NewExtension

func NewExtension(name, path string, bundled bool) *Extension

NewExtension returns an Extension for the given command name and executable path, invoked under its own file name.

type QuietOptions added in v1.3.0

type QuietOptions struct {
	ConfigDir string
	// AuthToken is conveyed because extensions may refuse to run without it.
	AuthToken string
}

QuietOptions is what a caller of InvokeQuietlyWithJSON chooses. Everything else about the run is fixed by the helper; justify any field added here.

type Resolver

type Resolver struct {
	BundledDir string
	// contains filtered or unexported fields
}

Resolver discovers and resolves extension executables. It searches the bundled-extensions directory (BundledDir) before PATH, so a bundled extension wins over a same-named executable on PATH. A zero BundledDir disables the bundled search (used in tests that exercise only the PATH path).

func NewResolver

func NewResolver(logger log.Logger) *Resolver

NewResolver returns a Resolver whose bundled-extensions directory is derived from the symlink-resolved location of the running lstk executable, so it is found even when lstk is invoked through a symlink or package shim.

func (*Resolver) List

func (r *Resolver) List() []Extension

List returns the extensions resolvable from the bundle, the bundled directory and PATH, de-duplicated by command name with that same precedence (so a bundled extension shadows a same-named PATH executable), sorted by command name. It never executes an extension. A bundle that fails to load is logged and skipped rather than failing the listing, so help rendering never breaks on account of it; Resolve is where that failure is reported.

Bundled entries carry their help Description. With a bundle installed the descriptions come from the load that already parsed the file; without one (or with a broken one) the lenient LoadDescriptions serves the standalone lstk-<name> files in the bundled dir, the pre-bundling shape. PATH entries are always name-only, whatever the file says.

func (*Resolver) Resolve

func (r *Resolver) Resolve(name string) (*Extension, error)

Resolve returns the extension for the given command name, searching in order: the multi-call bundle in the bundled directory (for the commands its descriptions file lists), standalone lstk-<name> files in the bundled directory, then PATH. It returns ErrNotFound when no match exists anywhere.

A bundle whose descriptions file cannot be loaded does not block extensions found elsewhere, but when nothing else provides the name that load error is returned instead of ErrNotFound: the user has the binary installed and asked for a command it very likely provides, and "unknown command" would hide the broken install from them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL