feature

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

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

View Source
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

View Source
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

func GenerateBuildContext(plan BuildPlan, dst string) error

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:

  1. Features whose id matches an entry in overrideOrder lead the result, in declaration order. (Hard override; not topo-sorted.)
  2. 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

func ResolveLocalRef(configDir, ref string) string

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

func SerializeEnvFile(opts map[string]any) []byte

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.

func (BuildPlan) HasWork

func (p BuildPlan) HasWork() bool

HasWork reports whether plan has any features that still need installation. Useful for the engine to decide whether to skip the generated build entirely.

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.

func (*DiskStore) CacheDir

func (s *DiskStore) CacheDir() string

CacheDir returns the on-disk cache root. Useful for debugging and for the `rm -rf $cache` recovery path documented in design §8.

func (*DiskStore) Fetch

func (s *DiskStore) Fetch(ctx context.Context, ref string, kind config.FeatureSourceKind) (Fetched, error)

Fetch resolves a feature reference per its source kind.

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.

Jump to

Keyboard shortcuts

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