Documentation
¶
Overview ¶
Package feature implements the devcontainer feature pipeline: reference resolution (OCI / HTTPS / Local), option processing, dependency-graph ordering, and dockerfile generation.
PR6 implements the local resolver, option merge, and DAG ordering (no network). OCI / HTTPS land in PR7; dockerfile-gen in PR8; devcontainer.metadata image-label read in PR9.
See design/features.md for the full design.
Index ¶
- Constants
- Variables
- func GenerateBuildContext(plan BuildPlan, dst string) error
- func Matches(baked, req config.FeatureMetadata, mode MatchMode) bool
- func MatchesResolved(baked, req config.ResolvedFeature, mode MatchMode) bool
- func MergeOptions(meta config.FeatureMetadata, user map[string]any) (map[string]any, []config.Warning, error)
- func Order(features []config.ResolvedFeature, overrideOrder []string) ([]config.ResolvedFeature, []config.Warning, error)
- func ParseMetadataLabel(label string) ([]config.FeatureMetadata, error)
- func ResolveLocalRef(configDir, ref string) string
- func SerializeEnvFile(opts map[string]any) []byte
- type BuildPlan
- type DiskStore
- type DiskStoreOptions
- type FeatureCycleError
- type FeatureDAGTooDeepError
- type Fetched
- type MatchMode
- type Store
Constants ¶
const MetadataLabel = "devcontainer.metadata"
MetadataLabel is the image-label key that carries the accumulated devcontainer metadata across base image + feature layers + final resolved config. Spec-defined.
Variables ¶
var ErrNotImplemented = errors.New("feature: store does not support this source kind")
ErrNotImplemented is returned by stores for source kinds they don't (yet) support. PR6's DiskStore returns this for OCI and HTTPS.
Functions ¶
func GenerateBuildContext ¶
GenerateBuildContext writes a Dockerfile and build-context directory to dst that, when built, produces a feature-extended image atop plan.BaseImage. dst must be empty or non-existent.
Layout written:
dst/
Dockerfile
build-context/
0/
run.sh
feature.env
install.sh (copied from feature.Dir)
... (everything else from feature.Dir)
1/
...
AlreadyInstalled features are not copied or invoked but still consume an index slot so log lines / Dockerfile comments stay aligned with plan.Features.
func Matches ¶
func Matches(baked, req config.FeatureMetadata, mode MatchMode) bool
Matches reports whether `baked` (an entry from a base image's devcontainer.metadata label) satisfies the request `req`. Used by Engine.Up to mark cfg.Features entries AlreadyInstalled and skip fetch + dockerfile-gen for them.
func MatchesResolved ¶
func MatchesResolved(baked, req config.ResolvedFeature, mode MatchMode) bool
MatchesResolved is the strict-mode-aware version that knows about ResolvedRef (the digest pinning) for byte-level identity comparison. In permissive mode it falls through to Matches on Metadata.
func MergeOptions ¶
func MergeOptions(meta config.FeatureMetadata, user map[string]any) (map[string]any, []config.Warning, error)
MergeOptions combines user-supplied options with feature defaults and validates against the feature's option declarations.
Order of precedence (later wins): defaults → user. Keys not declared in the feature's options are kept (some features accept arbitrary extras) but emit WarnUnknownFeatureOption. Enum-typed options are hard-validated; values outside the enum produce an error. Proposals (soft constraints) do not validate.
Returns the merged map and any non-fatal warnings. Errors are validation failures (enum mismatch, type mismatch on declared options).
func Order ¶
func Order(features []config.ResolvedFeature, overrideOrder []string) ([]config.ResolvedFeature, []config.Warning, error)
Order returns the install order for a slice of features. Per spec:
- Features whose id matches an entry in overrideOrder lead the result, in declaration order. (Hard override; not topo-sorted.)
- The rest are topo-sorted by their installsAfter and dependsOn edges, with alphabetical tie-breaking.
Order is idempotent: calling it twice produces the same slice.
Order tolerates partially-fetched features: if Metadata is empty (no installsAfter / dependsOn declared), the topo sort degenerates to alphabetical-by-ref. This lets config.Resolve apply overrideFeatureInstallOrder before fetch, and the engine re-run Order with full metadata after fetch.
Errors:
- *FeatureCycleError if installsAfter / dependsOn form a cycle.
- *FeatureDAGTooDeepError if depth exceeds the hard limit (64).
Warnings: WarnDeepFeatureChain when depth exceeds the soft limit (16).
func ParseMetadataLabel ¶
func ParseMetadataLabel(label string) ([]config.FeatureMetadata, error)
ParseMetadataLabel parses the JSON array stored at the devcontainer.metadata image label. Returns (nil, nil) for an empty or missing label. Unparseable labels return an error so callers can surface diagnostics; callers may treat the error as "no label" if they want to be permissive.
All entries are returned, including the no-ID "resolved config" entry written by a previous build — it carries the user's mergeable overrides and must not be dropped or those overrides are lost on rebuild.
func ResolveLocalRef ¶
ResolveLocalRef converts a (possibly relative) local feature ref to an absolute path. Used by callers (the engine) before invoking Store.Fetch with FeatureSourceLocal. configDir is typically filepath.Dir(ResolveOptions.ConfigPath).
func SerializeEnvFile ¶
SerializeEnvFile emits options as a `KEY="value"` env file the feature's install.sh sources. Keys are uppercased and shell-safe; values are shell-quoted with single quotes. Sorted by key for determinism.
Layout per spec / devpod convention:
OPTION_KEY1='value1' OPTION_KEY2='true'
Boolean values render as "true" / "false" without quoting changes. Numeric values render as their fmt.Sprint form.
Types ¶
type BuildPlan ¶
type BuildPlan struct {
// BaseImage is the reference of the image to build from. It will
// be supplied as the build-arg _DEV_CONTAINERS_BASE_IMAGE.
BaseImage string
// Features are the resolved features in install order. Each must
// have Dir + Metadata populated (i.e. fetched) unless
// AlreadyInstalled is true, in which case it is skipped entirely.
Features []config.ResolvedFeature
// RemoteUser and ContainerUser feed into the final metadata label
// entry. RemoteUser also drives the _DEV_CONTAINERS_IMAGE_USER
// arg so the final image's USER matches the spec's intent.
RemoteUser string
ContainerUser string
// BaseImageMetadata carries the parsed contents of the base image's
// existing devcontainer.metadata label. The generated final image's
// label includes these entries as a prefix so metadata accumulates
// across rebuilds — a feature installed two builds ago stays
// recorded even if today's build only layers a new one.
BaseImageMetadata []config.FeatureMetadata
}
BuildPlan describes what to layer on top of a base image to produce a feature-extended devcontainer image.
type DiskStore ¶
type DiskStore struct {
// contains filtered or unexported fields
}
DiskStore is the production Store implementation. Supports FeatureSourceLocal (PR6), FeatureSourceOCI (PR7), and FeatureSourceHTTPS (PR7).
func NewDiskStore ¶
func NewDiskStore(opts DiskStoreOptions) (*DiskStore, error)
NewDiskStore constructs a DiskStore. The cache directory is created if it doesn't exist; an error is returned if it cannot be created.
type DiskStoreOptions ¶
type DiskStoreOptions struct {
// CacheDir is the on-disk root for cached OCI / HTTPS feature
// artifacts. Default: os.UserCacheDir()/devcontainer-go/features.
// Local-source features are not cached (the source path IS the cache).
CacheDir string
// OCIKeychain provides credentials for OCI feature pulls. Nil falls
// back to authn.DefaultKeychain (ambient docker config / env vars /
// credential helpers). Callers with short-lived registry tokens
// supply a custom Keychain that returns fresh credentials per call.
OCIKeychain authn.Keychain
// HTTPSHeaders are additional headers to send on HTTPS feature
// fetches. Useful for Authorization on private servers.
HTTPSHeaders map[string]string
// HTTPSClient overrides the default *http.Client used for HTTPS
// fetches. If nil, the package's default client (5-minute timeout)
// is used. Tests substitute this to drive httptest servers.
HTTPSClient *http.Client
}
DiskStoreOptions configures DiskStore.
type FeatureCycleError ¶
type FeatureCycleError struct {
Path []string
}
FeatureCycleError is returned by Order when the feature graph has a cycle. Path is the cycle in encounter order, repeating the start node at the end so it reads as a closed loop.
func (*FeatureCycleError) Error ¶
func (e *FeatureCycleError) Error() string
type FeatureDAGTooDeepError ¶
type FeatureDAGTooDeepError struct {
Depth int
Path []string // chain from a root to the deepest node
}
FeatureDAGTooDeepError is returned by Order when the feature graph exceeds the hard depth limit (64).
func (*FeatureDAGTooDeepError) Error ¶
func (e *FeatureDAGTooDeepError) Error() string
type Fetched ¶
type Fetched struct {
// Dir is the absolute path to the directory containing
// devcontainer-feature.json and install.sh.
Dir string
// ResolvedRef is the pinned reference: a digest for OCI, the
// content hash (sha256) for HTTPS, or the absolute path for Local.
// This is what gets recorded on the devcontainer.metadata label so
// rebuilds use the exact same bytes even if a tag has moved.
ResolvedRef string
// Metadata is the parsed devcontainer-feature.json.
Metadata config.FeatureMetadata
}
Fetched is the result of resolving one feature.
type MatchMode ¶
type MatchMode int
MatchMode controls how Matches compares a baked feature entry to a requested feature.
const ( // MatchPermissive (default) considers a feature "already installed" // if the baked id matches and the baked semver is >= the request. // Non-semver versions fall back to strict-string-equality — // guessing version ordering on arbitrary tags is unsafe. MatchPermissive MatchMode = iota // MatchStrict requires byte-level equality on the resolved digest. // Only set when reproducible builds matter; rebuilds are required // whenever any feature artifact changes upstream. MatchStrict )
type Store ¶
type Store interface {
Fetch(ctx context.Context, ref string, kind config.FeatureSourceKind) (Fetched, error)
}
Store fetches the artifact bytes for one feature reference and returns the unpacked-on-disk path plus parsed metadata. Implementations cache fetched artifacts so repeat calls for the same ref hit local disk.
Local refs (paths starting with ./, ../, or /) must be resolved to absolute paths by the caller before invocation; Store does not know about the source devcontainer.json directory.