Skip to main content
Components are defined in Go using the component.Definition struct from pkg/component. Plugins publish those definitions through the plugin SDK and register hidden component command handlers for typed RPC dispatch. Core uses the definitions to plan create and render component metadata, then calls the handlers to inspect, mutate, and reconcile project state.

The Definition struct

Core identity

Name is the machine-readable identifier used in commands like sitectl set fcrepo off. It must be unique within the plugin. DefaultState is on or off, which controls what the component does if the operator never sets it explicitly. Most components default to on to match the upstream template.

Create flow

PromptOnCreate controls whether sitectl create isle asks the operator about this component. Set it to true for components that represent a meaningful architectural choice that should be made at install time, such as whether to include fcrepo or Blazegraph. FollowUps is a list of additional non-secret questions that appear when a specific state or disposition is chosen. For example, fcrepo asks which file system URI to use when it is enabled. Follow-up specs include: Follow-up flags are forwarded to the plugin as passthrough argv so Cobra can parse the plugin-owned flags. They can be visible in process listings, and the typed RPC params sensitivity check does not cover passthrough Args. Never use a follow-up for passwords, tokens, private keys, secret keys, or any other sensitive value. Secret material must use a purpose-built stdin, file, environment-reference, or secret-store flow whose transport and redaction contract is explicit.

Domains: On and Off

On and Off are both DomainSpec values that describe what changes when the component is in each state:
Compose rules describe changes to Compose YAML. Drupal rules describe changes to the Drupal config sync directory. Each YAMLStateSpec holds a list of YAMLRule entries:
Operation support is domain-specific. Compose mutation accepts set, restore, and delete; additional operations such as replace, contains, and not_contains are used by supported state-detection paths. Do not assume every operation is writable in every domain—cover both detection and application in the component tests. Files is the generic project-file domain for component-owned state outside Compose and Drupal YAML. It uses FileStateSpec and FileRule:
When Path is set, the target is JSON and the rule sets, restores, or deletes the named object path while retaining unrelated fields. Without Path, a text rule owns only its marked block; set/restore writes the block and delete removes it. Use stable, component-specific markers and the smallest possible JSON path or text block. The same context file-access layer supports local and remote projects, so a generic file rule must be deterministic, bounded, and idempotent in both environments. Generic file rules do not make an entire file component-owned. The normal field ownership contract still applies: unmarked text, adjacent JSON fields, and all other downstream content must survive set, disable, and converge operations. For operational work that structured YAML and generic file rules cannot express, ComponentSpec supports lifecycle hooks (BeforeEnable, AfterEnable, BeforeDisable, AfterDisable) that run Go functions with access to the Docker client and context config. Hooks are an exception for runtime transitions, not a reason to hide deterministic file ownership in arbitrary code.

Field ownership contract

Treat every component as the owner of a small, explicit part of the downstream project. Record the contract next to the definition and enforce it in tests:
  • List every project-relative file and smallest semantic YAML unit the component may change for each allowed disposition: for example, a scalar key, identified mapping or sequence entry, or generated block. Components may share a containing map or list when they own disjoint units and preserve one another’s entries. They must not own the same unit.
  • Preserve unknown keys, adjacent fields, unrelated sequence entries, and downstream policy outside those units. Enabling, disabling, or repairing drift may change owned fields, but must not normalize or reconstruct neighboring configuration.
  • Replace an entire file only when the component is its exclusive owner and the contract says so. An explicit base-owner/nested-augmenter relationship is the exception: document the parent and child units, make the convergence pipeline reconcile the base owner’s selected disposition before every augmenter and reapply each augmenter’s selected disposition after any base repair, and state that downstream edits inside the base-owned file are not guaranteed to survive base repair. Do not depend on incidental registration, lexical, or rule order.
  • Test every allowed disposition, an enable-disable-enable cycle, repeated application for idempotence, deliberate drift followed by repair, and sentinel unrelated fields that must survive every transition. For disjoint units in a shared container, apply the components in both orders and prove that their writes commute. For a declared base-owner/nested-augmenter relationship, cover every enabled/disabled combination in the documented order and reconverge after repairing either owner.
For example, the ISLE fcrepo and blazegraph components share three Drupal context files but own different named entries under each reaction’s actions mapping. Their apply path seeds a missing context once, then reconciles only those exact entries. It tests every Fedora/Blazegraph state transition while sentinel keys and existing action bodies prove that downstream configuration survives.

Gates

Gates controls safety checks before a state change is applied:
Set LocalOnly: true for components that should only be changed on local contexts. Set DisableConfirmation or EnableConfirmation to a custom prompt string. If empty, sitectl uses a default that mentions the possibility of rewriting Compose files and Drupal config.

Behavior

Behavior records operational metadata that helps sitectl (and operators) understand what a state change entails:
DataMigration tells operators whether enabling or disabling this component requires a data migration:

Dependencies

Dependencies.DrupalModules lists which Drupal modules must be present when the component is enabled, and how sitectl should treat them if the component is later disabled:
strict means the module is part of the component’s contract: enable it when the component is enabled, and consider it out of place when the component is disabled. enable_only means the module must exist when the component is enabled, but disabling the component does not imply removing or uninstalling it.

Registering a component

Plugins publish component metadata and register its apply handlers at startup:
RegisterComponentDefinitions exposes the v1 Definition metadata used by create and core component review. RegisterComponentCommand installs the plugin’s hidden list, describe, reconcile, and set handlers; their flags must match the typed RPC parameter contract. Shared Compose service components should use sdk.RegisterServiceComponents(...), which registers both metadata and standard handlers. The older component.Registry stores implementations of the legacy component.Component interface. It does not accept component.Definition values and is not how a v1 plugin publishes component metadata.

When to define a new component

A feature is a good candidate for a component if:
  • Operators frequently ask how to enable or disable it (it comes up in support channels)
  • Enabling or disabling it requires coordinated changes across Compose files and Drupal config
  • It represents a meaningful architectural choice that should be made at install time
If a feature is always on and has no meaningful off state, it does not need to be a component. If the component represents a reusable, self-contained service such as Traefik, Solr, MariaDB, Valkey, or Memcached, follow the Service components architecture. The shared operation belongs in core sitectl; the application plugin should only add app-specific wiring.

Cross-domain feature bundles

Use an application-plugin feature bundle when one operator-visible capability must change several application domains as a unit—for example, a Compose service plus a Drupal action, or Composer requirements plus Drupal and search configuration. Do not model only the container when that would leave the application half-configured, and do not move stack-specific rules into core merely because the bundle touches Compose. A bundle should declare whole-owned canonical assets separately from narrow mutations, preflight every target input before its first write, and expose the same state through create, set, describe, validate, and converge. Keep it local-only when the result must be reviewed and committed in the downstream fork. Dependency manifests can be owned narrowly, but package-manager lock files, live configuration imports, deployment, and data backfills remain explicit downstream operations. The ISLE mergepdf and hocr-search bundles are the reference implementation and publish their exact whole-file, field, compatibility, and rollout boundaries for operators.

Component workflow

1

Define the component

Write a new Go file in the plugin’s pkg/components directory. Implement the Definition struct with On and Off domain specs. Add structured YAML or generic file rules for the observable state that describe, set, and converge should detect.
2

Register it

Add the new definition to the plugin’s ordered definition set. Ensure the SDK registers that set and the plugin’s hidden component command tree exactly once.
3

Add it to the create flow

If the component requires a decision at install time, set PromptOnCreate: true, add any FollowUps needed, and thread the decision through the plugin’s create request/options object. The create path should apply the same behavior as top-level set so a new checkout and an existing checkout stay consistent.
4

Add the apply path

Use the repo’s existing component apply pattern. For simple components, structured YAML mutation helpers are usually enough. For larger generated config blocks, put the static config in pkg/create/assets/... and load it with Go embed instead of keeping inline YAML strings in Go code.
5

Write tests

Add focused tests for the component definition, create option resolution, the set/apply path, and any generated files. If the component is intentionally disabled during integration tests, make that explicit with a create flag instead of relying on an implicit default.
6

Document it

Add a page in plugins/isle/ (or the relevant plugin section), add it to docs.json, and link to it from the plugin overview. Document when to enable or disable the component, create-time flags, follow-up environment variables, generated files, and any production caveats.

Component Checklist

Before opening a component PR, check that it covers the full operator workflow:
  • Definition includes clear guidance, allowed dispositions, behavior summaries, data migration impact, and LocalOnly when changes should only happen on local checkouts.
  • PromptOnCreate is true for features operators should decide on during initial site creation.
  • Create flags and recreate-command output include the component so automated installs and setup commits are reproducible.
  • set, create, and converge/describe detection agree on what on, off, and drifted mean.
  • Every follow-up value is explicitly non-secret and safe to expose through process argv.
  • Generic file rules own the smallest JSON path or marked text block and preserve sentinel downstream content around it.
  • Static generated YAML or templates live under pkg/create/assets/... and are loaded with Go embed; avoid large inline YAML blocks in Go.
  • Runtime warnings are printed for insecure defaults, required credentials, destructive changes, or behavior that can surprise production operators.
  • Integration tests explicitly set the component state, even when the intended state is the default.
  • Plugin documentation explains the operational reason for any unusual design choice, such as vendoring or mounting local plugin source to avoid production startup dependencies on third-party services.