Adding a Type
This guide documents the process for adding a new resource type to CCM. It uses the archive type as a reference implementation.
Overview
Adding a new resource type requires changes across several packages:
- Model definitions - Properties, state, and metadata structs
- Resource type implementation - Core logic and provider interface
- Provider implementation - Platform-specific operations
- Integration points - Factory functions and registry
- CLI commands - User-facing command line interface
- JSON schemas - Validation for manifests and API requests
- Documentation - User and design documentation
- CCM Studio - Web-based manifest designer
File Checklist
| File | Action | Purpose |
|---|---|---|
model/resource_<type>.go | Create | Properties, state, metadata structs |
model/resource_<type>_test.go | Create | Property validation tests |
model/resource.go | Modify | Add case to factory function |
resources/<type>/<type>.go | Create | Provider interface definition |
resources/<type>/type.go | Create | Resource type implementation |
resources/<type>/type_test.go | Create | Resource type tests |
resources/<type>/provider_mock_test.go | Generate | Mock provider for tests |
resources/<type>/<provider>/factory.go | Create | Provider factory |
resources/<type>/<provider>/<provider>.go | Create | Provider implementation |
resources/<type>/<provider>/<provider>_test.go | Create | Provider tests |
resources/resources.go | Modify | Add case to NewResourceFromProperties |
cmd/ensure_<type>.go | Create | CLI command handler |
cmd/ensure.go | Modify | Register CLI command |
internal/fs/schemas/manifest.json | Modify | Add resource schema definitions |
internal/fs/schemas/resource_ensure_request.json | Modify | Add API request schema |
docs/content/resources/<type>.md | Create | User documentation |
docs/content/design/<type>/_index.md | Create | Design documentation |
docs/content/design/<type>/<provider>.md | Create | Provider documentation |
Step 1: Model Definitions
Create model/resource_<type>.go with the following components.
Constants
Properties Struct
The properties struct must satisfy model.ResourceProperties:
Structure:
Key points:
- Embed
CommonResourcePropertieswithyaml:",inline"tag - Use JSON and YAML struct tags for serialization
- In
Validate(), callp.CommonResourceProperties.Validate()first, then add type-specific validation - Template resolution is handled automatically via reflection - see the Template Resolution section for details
ResolveDeferredTemplates()is called after control evaluation (if/unless). Override it only if you havetemplate:"deferred"fields that need post-processing (e.g.filepath.Clean). The default no-op fromCommonResourcePropertiesis sufficient for most types. See the file resource for an example whereContentsandSourceare deferred
State Struct
The state struct must satisfy model.ResourceState:
Structure:
Factory Function
Provide a factory function for YAML parsing:
Step 2: Resource Type Implementation
Provider Interface (resources/<type>/<type>.go)
Define a type-specific provider interface that embeds model.Provider and adds type-specific methods:
Type Implementation (resources/<type>/type.go)
The Type struct must satisfy both model.Resource and base.EmbeddedResource:
Embedding *base.Base provides implementations for Apply(), Healthcheck(), Type(), Name(), Properties(), and NewTransactionEvent(). The type must implement:
ApplyResource()- core resource application logicSelectProvider()- provider selectionProvider()- return current provider nameInfo()- return resource information
Structure:
See resources/archive/type.go for a complete constructor example.
ApplyResource Method
The ApplyResource method (part of base.EmbeddedResource) contains the core logic. It should follow this pattern:
- Get initial state via
provider.Status() - Check if already in desired state (implement
isDesiredState()helper) - If stable, call
t.FinalizeState()and return early - Apply changes, respecting
t.mgr.NoopMode() - Get final state and verify desired state was achieved
- Call
t.FinalizeState()with appropriate flags
See resources/archive/type.go:ApplyResource() for a complete example.
Provider Selection Methods
The SelectProvider() method should use registry.FindSuitableProvider() to select an appropriate provider. See resources/archive/type.go for the standard implementation pattern.
Step 3: Provider Implementation
Factory (resources/<type>/<provider>/factory.go)
The factory must satisfy model.ProviderFactory:
The IsManageable method returns:
bool- whether this provider can manage the resourceint- priority (higher wins when multiple providers match)error- any error encountered
Structure:
See resources/archive/http/factory.go for a complete example.
Provider Implementation (resources/<type>/<provider>/<provider>.go)
The provider must satisfy the type-specific provider interface defined in Step 2 (which embeds model.Provider):
Structure:
See resources/archive/http/http.go for a complete example.
Step 4: Integration Points
Update resources/resources.go
Add the import and case statement:
Update model/resource.go
Add the case to NewResourcePropertiesFromYaml:
Step 5: CLI Command
Create cmd/ensure_<type>.go:
Update cmd/ensure.go:
Step 6: JSON Schemas
Update internal/fs/schemas/manifest.json
Add to the $defs/resource properties:
Add resource list definition:
Add properties definitions:
Update internal/fs/schemas/resource_ensure_request.json
Add to the type enum:
Add to properties.oneOf:
Add properties definition under $defs:
Copy Schemas to Documentation Site
After updating the schema files in internal/fs/schemas/, copy them to docs/static/schemas/v1/ so they are available on the documentation website:
Step 7: Generate Mocks
Generate the provider mock for tests:
Or use the project command:
Step 8: Testing
Model Tests (model/resource_<type>_test.go)
Test property validation:
Type Tests (resources/<type>/type_test.go)
Use the mock manager helper:
Key Patterns
State Checking
Always check current state before making changes:
Noop Mode
All resources must respect noop mode:
Error Handling
Use sentinel errors from model/errors.go:
Wrap errors with context:
Template Resolution
Template resolution uses a reflection-based struct walker (templates.ResolveStructTemplates) that automatically resolves {{ expression }} placeholders in all string-typed fields. The walker recurses into all composite types including slices, maps, nested structs, and pointer fields.
By default, all fields are template-resolved. You control behavior with the template struct tag:
| Tag | Behavior |
|---|---|
| (none) | Resolved during ResolveTemplates() (phase 1) |
template:"-" | Never resolved - use for enum values, literal delimiters, resource references, or fields evaluated separately (like control expressions) |
template:"deferred" | Skipped in phase 1, resolved during ResolveDeferredTemplates() (phase 2, after control evaluation) |
template:"resolve_keys" | For map fields, also resolve map keys (rebuilds the map). By default only map values are resolved |
Fields tagged json:"-" are automatically skipped (these are internal computed fields like ParsedTimeout).
Supported types (resolved recursively):
stringand named string types (e.g.type MyType string)[]string,[]anymap[string]string,map[string]any,map[string][]string, and other map variants with string keys[]map[string]string,[]map[string]any- Nested and embedded structs,
*structpointers any/interface{}fields holding any of the above- Arbitrary nesting depth
Types that are not resolved: bool, int, float, time.Duration, []byte / yaml.RawMessage, nil pointers.
Implementation pattern - most resource types need only:
The resolveRegistrations call (inherited from CommonResourceProperties) handles RegisterWhenStable entries which need special typed resolution for the Port field.
Deferred resolution is used for fields whose template evaluation may fail when the resource would be skipped by a control (if/unless). Tag these fields with template:"deferred" and override ResolveDeferredTemplates():
This method is called by base.Base after control evaluation passes, so templates are only evaluated for resources that will actually be applied. Because deferred resolution happens at apply time rather than during manifest parsing, templates using functions like file() can access content created by earlier resources in the same run. The default no-op implementation inherited from CommonResourceProperties is sufficient for types that have no template:"deferred" fields.
Provider Selection
Providers declare manageability via IsManageable on the factory (see model.ProviderFactory in Step 3). Multiple providers can match; the one with highest priority is selected.
Documentation
Create user documentation in docs/content/resources/<type>.md covering:
- Overview and use cases
- Ensure states table
- Properties table with descriptions
- Usage examples (manifest, CLI, API)
Create design documentation in docs/content/design/<type>/_index.md covering:
- Provider interface specification
- State checking logic
- Apply logic flowchart
Create provider documentation in docs/content/design/<type>/<provider>.md covering:
- Provider selection criteria
- Platform requirements
- Implementation details
CCM Studio
CCM Studio is a web-based manifest designer. After adding a new resource type, update CCM Studio to support it:
Note
CCM Studio is a closed-source project. The maintainers will complete this step.
- Add the new resource type to the resource palette
- Create property editors for type-specific fields
- Add validation matching the JSON schema definitions
- Update any resource type documentation or help text