Architecture
Big picture
devfile/api has two layers. The first is a code-generation pipeline: Go types annotated with marker comments are read by a set of generators that emit CRDs, JSON schemas, deepcopy code, getters, override types, and a validation schema. The second is a runtime library that operates on those types at run time: applying a parent or plugin as an override, merging inherited content, normalizing discriminated unions, and validating internal references. The Go types are the shared center both layers turn on.
Go types (pkg/apis/workspaces/v1alpha2/*.go) +markers
|
| generator/ reads markers
v
CRDs (crds/) JSON schemas (schemas/) deepcopy/getters (zz_generated.*)
override types (ParentOverrides, PluginOverrides) TS model (build/typescript-model)
^
| runtime library consumes the same types
|
pkg/utils/overriding pkg/utils/unions pkg/validation pkg/attributesComponents
API types
The type definitions live in pkg/apis/workspaces/v1alpha2/. The top type is DevWorkspace, the CRD (devworkspace_types.go:95). The reusable content sits in DevWorkspaceTemplateSpecContent (devworkspacetemplate_spec.go:31), which carries the fields a devfile author actually writes: Variables, Attributes, Components, Projects, StarterProjects, DependentProjects, Commands, and Events (devworkspacetemplate_spec.go:31-107). Generated code (deepcopy, getters, union definitions, override types) lives beside it in zz_generated.*.go files and is never edited by hand.
The generator
generator/ is a separate Go module holding a custom code generator built on controller-tools. Its main.go registers seven generators by name: overrides, interfaces, crds, deepcopy, schemas, validate, and getters (generator/main.go:46-54). Each reads marker comments on the types (for example +devfile:jsonschema:generate on DevWorkspace at devworkspace_types.go:93) and writes one artifact. Because CRD generation extends controller-tools rather than calling controller-gen unchanged, the pipeline can add devfile-specific behavior such as the override types.
The override and merge library
pkg/utils/overriding/ applies a parent devfile or a plugin on top of a base devfile. It does not implement its own merge algorithm; it hands the work to Kubernetes strategic merge patch (see the flow below). pkg/utils/unions/ normalizes and simplifies discriminated unions. pkg/validation/ checks a devfile's internal consistency (duplicate ids, references that point at a component that exists), split by element type across commands.go, components.go, projects.go, endpoints.go, and events.go. pkg/attributes/ handles the free-form YAML attribute map.
How a request flows
Trace applying a parent's overrides onto a base devfile, the most interesting path in the library.
- Entry:
OverrideDevWorkspaceTemplateSpecBytes(originalBytes, patchBytes)converts the two YAML documents to JSON, unmarshals the base intoDevWorkspaceTemplateSpecContentand the patch into a generated override type (overriding.go:40). - Core:
OverrideDevWorkspaceTemplateSpec(original, patch)(overriding.go:75) does the work in order:ensureOnlyExistingElementsAreOverriddenrejects a patch that introduces a key not present in the base; an override may change existing elements, it cannot add new ones (overriding.go:76, defined atoverriding.go:133).unions.Normalizeis called on both base and patch, fixing each union's discriminator and erroring on an ambiguous one (overriding.go:80andoverriding.go:83).- Both sides are marshaled back to JSON, and
strategicpatch.NewPatchMetaFromStruct(original)builds patch metadata (merge keys and strategies) from the struct tags (overriding.go:106). strategicpatch.StrategicMergeMapPatchUsingLookupPatchMetaruns the Kubernetes strategic merge (overriding.go:111). This is the key decision: the merge rules come from Kubernetes, driven by the+patchMergeKeyand+patchStrategytags left on the fields.- The result is unmarshaled back to
DevWorkspaceTemplateSpecContentandunions.Simplifydrops the discriminators before returning (overriding.go:127).
- After each inheritance level is flattened,
MergeDevWorkspaceTemplateSpec(main, parentFlattened, plugins...)combines them (merging.go:40). It reads the top-level list field names, then uses reflection to append each list across all contents (merging.go:76-108), skipping anyplugincomponent from the main content because plugins arrive already flattened (merging.go:100-106). Duplicate keys across main, parent, and plugins are rejected byensureNoConflictWithParent(merging.go:209) andensureNoConflictsWithPlugins(merging.go:225).
Key design decisions
- Go types as the single source of truth. Every consumable artifact (CRDs, JSON schemas, TypeScript model, deepcopy, getters, override types, validation schema) is generated from the same Go types by the seven generators (
generator/main.go:46-54,README.md:11-24). A change to the format is a change to one Go struct, and the rest follows. - Reuse Kubernetes strategic merge patch instead of a custom merge. The override logic marshals to JSON and calls
strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(overriding.go:111), so the merge semantics (merge by key, replace, delete directives) are exactly Kubernetes semantics, driven by struct tags rather than bespoke code. - Discriminated unions follow the Kubernetes union KEP. The
Unioninterface points at the KEP for union types (union.go:22-36) and definesNormalizeandSimplify, applied across the whole tree withreflectwalk(normalize.go:71,normalize.go:78). - Override types are generated, not hand-written.
ParentOverridesandPluginOverridesare produced by theoverridesgenerator, and the+devfile:overrides:include:omitInPlugin=truemarker excludes fields a plugin must not override (variables, projects) at the type level (devworkspacetemplate_spec.go:45,devworkspacetemplate_spec.go:70).
Extension points
- The CRDs.
DevWorkspaceandDevWorkspaceTemplateare Kubernetes custom resources (devworkspace_types.go:88-94), so a controller such asdevfile/devworkspace-operatorreconciles them into running workspaces. parentandplugin. A devfile can inherit from aparentdevfile and pull in aplugin, resolved through the override and merge library above. This is the main way one devfile is composed from others.- Free-form attributes. The
Attributesmap is schemaless and preserves unknown fields (devworkspacetemplate_spec.go:52-55), giving implementations a place to attach tool-specific metadata without changing the schema. - The generated TypeScript model.
build/typescript-model/produces the@devfile/apinpm package, so JavaScript and TypeScript tools consume the same format from the same schema.