Template repository for creating formae resource plugins.
Note: Don't use GitHub's "Use this template" button. Instead, use the Formae CLI which will prompt for your plugin details and set everything up correctly:
formae plugin init my-plugin
- Create plugin:
formae plugin init <name>(prompts for namespace, license, etc.) - Define resources in
schema/pkl/*.pkl - Implement CRUD operations in
plugin.go - Build and test:
make build && make test
.
├── formae-plugin.pkl # Plugin manifest (name, version, namespace)
├── plugin.go # Your ResourcePlugin implementation
├── main.go # Entry point (don't modify)
├── schema/pkl/ # Pkl resource schemas
│ ├── PklProject
│ └── example.pkl
├── examples/ # Usage examples
├── scripts/
│ ├── ci/ # CI hook scripts
│ │ ├── setup-credentials.sh
│ │ └── clean-environment.sh
│ └── run-conformance-tests.sh
├── go.mod
├── Makefile
└── README.md
You only implement the ResourcePlugin interface in plugin.go:
type Plugin struct{}
// Configuration
func (p *Plugin) RateLimit() plugin.RateLimitConfig { ... }
func (p *Plugin) DiscoveryFilters() []plugin.MatchFilter { ... }
func (p *Plugin) LabelConfig() plugin.LabelConfig { ... }
// CRUD Operations
func (p *Plugin) Create(ctx, req) (*CreateResult, error) { ... }
func (p *Plugin) Read(ctx, req) (*ReadResult, error) { ... }
func (p *Plugin) Update(ctx, req) (*UpdateResult, error) { ... }
func (p *Plugin) Delete(ctx, req) (*DeleteResult, error) { ... }
func (p *Plugin) Status(ctx, req) (*StatusResult, error) { ... }
func (p *Plugin) List(ctx, req) (*ListResult, error) { ... }The SDK handles everything else:
- Plugin identity (name, version, namespace) → read from
formae-plugin.pkl - Schema extraction → auto-discovered from
schema/pkl/ - Resource descriptors → generated from Pkl schemas
- Go 1.25+
- Pkl CLI
make build # Build plugin binary
make test # Run unit tests
make lint # Run linter (requires golangci-lint)
make install # Build + install locally for testing# Copy .env.example and configure your credentials
cp .env.example .env
# Load environment variables
export $(cat .env | xargs)
# Install plugin and schemas locally
make install
# Start formae agent (discovers the plugin)
formae agent start
# Apply example resources
formae apply examples/basic/main.pkl| Variable | Description | Required |
|---|---|---|
GCP_PROJECT_ID |
GCP project ID | Yes |
GCP_PROJECT_NUMBER |
GCP project number | Yes |
GCP_REGION |
GCP region (e.g., europe-central2) |
Yes |
GCP_ZONE |
GCP zone (e.g., europe-central2-b) |
Yes |
GCP_CREDENTIALS_FILE |
Path to service account JSON key | Local only |
For local development, set GCP_CREDENTIALS_FILE to your service account key path.
In CI with Workload Identity Federation, leave it unset to use Application Default Credentials (ADC).
Run the full conformance test suite (CRUD lifecycle + discovery) against a specific formae version:
# Run conformance tests with latest stable version
make conformance-test
# Run conformance tests with a specific version
make conformance-test VERSION=0.77.0The conformance tests:
- Call
setup-credentialsto provision cloud credentials - Call
clean-environmentto remove orphaned resources from previous runs - Build and install the plugin locally
- Download the specified formae version (defaults to latest)
- Run CRUD lifecycle tests for each resource type
- Run discovery tests to verify resource detection
- Call
clean-environmentto clean up test resources
The template includes hook scripts that you customize for your cloud provider:
Provisions credentials for your cloud provider. Called before running conformance tests.
Examples:
- AWS: Verify
AWS_ACCESS_KEY_IDis set or use OIDC - OpenStack: Source your RC file and verify required env vars
- Azure: Run
az loginor verify OIDC credentials - GCP: Run
gcloud author verify workload identity
Cleans up test resources in your cloud environment. Called before AND after conformance tests to:
- Remove orphaned resources from previous failed runs (pre-cleanup)
- Clean up resources created during the test run (post-cleanup)
The script should be idempotent and delete all resources under the /testdata folder
The .github/workflows/ci.yml workflow includes a conformance-tests job that is
disabled by default. To enable it:
- Configure Workload Identity Federation in GCP (see docs/gcp-github-actions-setup.md)
- Add the required GitHub secrets (see below)
- Set
run_conformancetotruewhen triggering the workflow
| Secret | Description |
|---|---|
GCP_WORKLOAD_IDENTITY_PROVIDER |
Full WIF provider path |
GCP_SERVICE_ACCOUNT |
Service account email |
GCP_PROJECT_ID |
GCP project ID |
GCP_PROJECT_NUMBER |
GCP project number |
GCP_REGION |
GCP region |
GCP_ZONE |
GCP zone |
For detailed WIF setup instructions, see docs/gcp-workload-identity-federation-github-actions.md.
Create resource classes in schema/pkl/:
@formae.ResourceHint {
type = "MYPROVIDER::Service::Resource"
identifier = "$.Id"
}
class MyResource extends formae.Resource {
@formae.FieldHint {}
name: String
@formae.FieldHint { createOnly = true }
region: String?
}All plugin metadata lives in formae-plugin.pkl:
amends "@formae/plugin-manifest.pkl"
name = "myprovider" # Plugin identifier
version = "1.0.0" # Semantic version
description = "My cloud provider plugin"
spec {
protocolVersion = 1 # SDK protocol version
namespace = "MYPROVIDER" # Resource type prefix
capabilities { "create"; "read"; "update"; "delete"; "list"; "discovery" }
}All plugin operations return the ProgressResult struct. For async (long-running) operations
return InProgress with a RequestID. The formae agent will call the Status method on
a regular interval to request the status of the operation.
func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) {
operationID := startAsyncCreate(...)
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusInProgress,
RequestID: operationID,
},
}, nil
}
func (p *Plugin) Status(ctx context.Context, req *resource.StatusRequest) (*resource.StatusResult, error) {
status := checkOperation(req.RequestID)
if status.Complete {
return &resource.StatusResult{
ProgressResult: &resource.ProgressResult{
OperationStatus: resource.OperationStatusSuccess,
NativeID: status.ResourceID,
},
}, nil
}
// Still in progress - return InProgress status
}This template is licensed under FSL-1.1-ALv2 - See LICENSE
When creating your own plugin, choose an appropriate license for your project. Common choices include:
- MIT - Most permissive
- Apache-2.0 - Permissive with patent grant (recommended)
- MPL-2.0 - Weak copyleft
- FSL-1.1-ALv2 - Functional Source License
Replace the LICENSE file with your chosen license when you create your plugin.