almost empty

This commit is contained in:
2026-01-30 22:44:32 +01:00
commit b5320bf29a
22 changed files with 1576 additions and 0 deletions

13
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

198
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,198 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 3 * * *' # Nightly at 3 AM UTC
workflow_dispatch:
inputs:
run_conformance:
description: 'Run conformance tests'
required: false
default: 'false'
type: boolean
formae_version:
description: 'Formae version to test against (default: latest)'
required: false
default: 'latest'
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Set up Pkl
uses: pkl-community/setup-pkl@v0
with:
pkl-version: 0.30.0
- name: Build
run: make build
- name: Test
run: make test-unit
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: golangci-lint
uses: golangci/golangci-lint-action@v8.0.0
with:
version: latest
pkl-validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Pkl
uses: pkl-community/setup-pkl@v0
with:
pkl-version: 0.30.0
- name: Validate Pkl schemas
run: |
pkl eval formae-plugin.pkl --format json > /dev/null
echo "Manifest validated successfully"
# Integration tests run against real infrastructure.
# This job is disabled by default - enable it after configuring credentials.
integration-tests:
needs: [build, lint]
runs-on: ubuntu-latest
# Disabled by default - remove this condition after configuring credentials
if: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Set up Pkl
uses: pkl-community/setup-pkl@v0
with:
pkl-version: 0.30.0
# Configure credentials for your infrastructure here
# Example:
# env:
# MY_API_KEY: ${{ secrets.MY_API_KEY }}
- name: Run integration tests
run: make test-integration
# Conformance tests run against real cloud resources.
# This job is disabled by default - enable it after configuring credentials.
#
# To enable:
# 1. Configure credentials for your cloud provider (see below)
# 2. Implement scripts/ci/setup-credentials.sh for local credential verification
# 3. Implement scripts/ci/clean-environment.sh for test resource cleanup
# 4. Change the 'if' condition to enable the job
conformance-tests:
needs: [build, lint, pkl-validate]
runs-on: ubuntu-latest
# Disabled by default - change condition after configuring credentials:
# if: github.event_name == 'schedule' || github.event_name == 'push' || github.event.inputs.run_conformance == 'true'
if: ${{ github.event.inputs.run_conformance == 'true' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
# Test against these formae versions. Expand as needed:
# formae_version: ['0.78.0', '0.79.0', 'latest']
formae_version: ['latest']
# Uncomment and configure for your cloud provider:
# permissions:
# id-token: write # Required for OIDC
# contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Set up Pkl
uses: pkl-community/setup-pkl@v0
with:
pkl-version: 0.30.0
# =================================================================
# CREDENTIAL SETUP - Uncomment and configure for your provider
# =================================================================
# AWS (OIDC - recommended)
# - name: Configure AWS Credentials
# uses: aws-actions/configure-aws-credentials@v4
# with:
# aws-region: us-east-1
# role-to-assume: arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME
# role-session-name: ConformanceTests
# Azure (OIDC - recommended)
# - name: Configure Azure Credentials
# uses: azure/login@v2
# with:
# client-id: ${{ secrets.AZURE_CLIENT_ID }}
# tenant-id: ${{ secrets.AZURE_TENANT_ID }}
# subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# GCP (OIDC - recommended)
# - name: Configure GCP Credentials
# uses: google-github-actions/auth@v2
# with:
# workload_identity_provider: projects/PROJECT_ID/locations/global/workloadIdentityPools/POOL/providers/PROVIDER
# service_account: SA_NAME@PROJECT_ID.iam.gserviceaccount.com
# OpenStack (secrets-based)
# - name: Configure OpenStack Credentials
# run: |
# echo "OS_AUTH_URL=${{ secrets.OS_AUTH_URL }}" >> $GITHUB_ENV
# echo "OS_USERNAME=${{ secrets.OS_USERNAME }}" >> $GITHUB_ENV
# echo "OS_PASSWORD=${{ secrets.OS_PASSWORD }}" >> $GITHUB_ENV
# echo "OS_PROJECT_ID=${{ secrets.OS_PROJECT_ID }}" >> $GITHUB_ENV
# =================================================================
- name: Install plugin
run: make install
- name: Run conformance tests
env:
FORMAE_TEST_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
# Use input version if provided, otherwise use matrix version
VERSION="${{ inputs.formae_version }}"
if [ "$VERSION" = "" ] || [ "$VERSION" = "latest" ]; then
VERSION="${{ matrix.formae_version }}"
fi
make conformance-test VERSION="$VERSION"

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Binaries
bin/
dist/
*.so
*.exe
# Go
vendor/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Test coverage
coverage.out
coverage.html
# GoReleaser
dist/
# Pkl
.pkl-cache/
*.pkl-expected.pcf
PklProject.deps.json
# Claude Code
CLAUDE.md
.claude/

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 ManInDark
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

121
Makefile Normal file
View File

@@ -0,0 +1,121 @@
# Formae Plugin Makefile
#
# Targets:
# build - Build the plugin binary
# test - Run tests
# lint - Run linter
# clean - Remove build artifacts
# install - Build and install plugin locally (binary + schema + manifest)
# Plugin metadata - extracted from formae-plugin.pkl
PLUGIN_NAME := $(shell pkl eval -x 'name' formae-plugin.pkl 2>/dev/null || echo "example")
PLUGIN_VERSION := $(shell pkl eval -x 'version' formae-plugin.pkl 2>/dev/null || echo "0.0.0")
PLUGIN_NAMESPACE := $(shell pkl eval -x 'namespace' formae-plugin.pkl 2>/dev/null || echo "EXAMPLE")
# Build settings
GO := go
GOFLAGS := -trimpath
BINARY := $(PLUGIN_NAME)
# Installation paths
# Plugin discovery expects lowercase directory names matching the plugin name
PLUGIN_BASE_DIR := $(HOME)/.pel/formae/plugins
INSTALL_DIR := $(PLUGIN_BASE_DIR)/$(PLUGIN_NAME)/v$(PLUGIN_VERSION)
.PHONY: all build test test-unit test-integration lint verify-schema clean install help clean-environment conformance-test conformance-test-crud conformance-test-discovery
all: build
## build: Build the plugin binary
build:
$(GO) build $(GOFLAGS) -o bin/$(BINARY) .
## test: Run all tests
test:
$(GO) test -v ./...
## test-unit: Run unit tests only (tests with //go:build unit tag)
test-unit:
$(GO) test -v -tags=unit ./...
## test-integration: Run integration tests (requires cloud credentials)
## Add tests with //go:build integration tag
test-integration:
$(GO) test -v -tags=integration ./...
## lint: Run golangci-lint
lint:
golangci-lint run
## verify-schema: Validate PKL schema files
## Checks that schema files are well-formed and follow formae conventions.
verify-schema:
$(GO) run github.com/platform-engineering-labs/formae/pkg/plugin/testutil/cmd/verify-schema --namespace $(PLUGIN_NAMESPACE) ./schema/pkl
## clean: Remove build artifacts
clean:
rm -rf bin/ dist/
## install: Build and install plugin locally (binary + schema + manifest)
## Installs to ~/.pel/formae/plugins/<name>/v<version>/
## Removes any existing versions of the plugin first to ensure clean state.
install: build
@echo "Installing $(PLUGIN_NAME) v$(PLUGIN_VERSION) (namespace: $(PLUGIN_NAMESPACE))..."
@rm -rf $(PLUGIN_BASE_DIR)/$(PLUGIN_NAME)
@mkdir -p $(INSTALL_DIR)/schema/pkl
@cp bin/$(BINARY) $(INSTALL_DIR)/$(BINARY)
@cp -r schema/pkl/* $(INSTALL_DIR)/schema/pkl/
@cp formae-plugin.pkl $(INSTALL_DIR)/
@echo "Installed to $(INSTALL_DIR)"
@echo " - Binary: $(INSTALL_DIR)/$(BINARY)"
@echo " - Schema: $(INSTALL_DIR)/schema/pkl/"
@echo " - Manifest: $(INSTALL_DIR)/formae-plugin.pkl"
## help: Show this help message
help:
@echo "Available targets:"
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /'
## clean-environment: Clean up test resources in cloud environment
## Called before and after conformance tests. Edit scripts/ci/clean-environment.sh
## to configure for your provider.
clean-environment:
@./scripts/ci/clean-environment.sh
## conformance-test: Run all conformance tests (CRUD + discovery)
## Usage: make conformance-test [VERSION=0.80.0] [TEST=s3-bucket]
## Downloads the specified formae version (or latest) and runs conformance tests.
## Calls clean-environment before and after tests.
##
## Parameters:
## VERSION - Formae version to test against (default: latest)
## TEST - Filter tests by name pattern (e.g., TEST=s3-bucket)
conformance-test: conformance-test-crud conformance-test-discovery
## conformance-test-crud: Run only CRUD lifecycle tests
## Usage: make conformance-test-crud [VERSION=0.80.0] [TEST=s3-bucket]
conformance-test-crud: install
@echo "Pre-test cleanup..."
@./scripts/ci/clean-environment.sh || true
@echo ""
@echo "Running CRUD conformance tests..."
@FORMAE_TEST_FILTER="$(TEST)" FORMAE_TEST_TYPE=crud ./scripts/run-conformance-tests.sh $(VERSION); \
TEST_EXIT=$$?; \
echo ""; \
echo "Post-test cleanup..."; \
./scripts/ci/clean-environment.sh || true; \
exit $$TEST_EXIT
## conformance-test-discovery: Run only discovery tests
## Usage: make conformance-test-discovery [VERSION=0.80.0] [TEST=s3-bucket]
conformance-test-discovery: install
@echo "Pre-test cleanup..."
@./scripts/ci/clean-environment.sh || true
@echo ""
@echo "Running discovery conformance tests..."
@FORMAE_TEST_FILTER="$(TEST)" FORMAE_TEST_TYPE=discovery ./scripts/run-conformance-tests.sh $(VERSION); \
TEST_EXIT=$$?; \
echo ""; \
echo "Post-test cleanup..."; \
./scripts/ci/clean-environment.sh || true; \
exit $$TEST_EXIT

134
README.md Normal file
View File

@@ -0,0 +1,134 @@
> **⚠️ Do not clone this repository directly!**
>
> Use `formae plugin init` to create your plugin. This command scaffolds a new
> plugin from this template with proper naming and configuration.
>
> ```bash
> formae plugin init my-plugin
> ```
---
## Setup Checklist
*Remove this section and the warning above after completing setup.*
After creating your plugin with `formae plugin init`, complete these steps:
- [X] Update `formae-plugin.pkl` with your plugin metadata (name, namespace, description)
- [X] Define your resource types in `schema/pkl/*.pkl`
- [ ] Implement CRUD operations in `plugin.go`
- [ ] Update test fixtures in `testdata/*.pkl` to use your resources
- [ ] Update this README (replace title, description, resources table, etc.)
- [ ] Set up local credentials for testing
- [ ] Run conformance tests locally: `make conformance-test`
- [ ] Configure CI credentials in `.github/workflows/ci.yml` (optional)
- [ ] Remove this checklist section and the warning box above
For detailed guidance, see the [Plugin SDK Documentation](https://docs.formae.io/plugin-sdk).
---
# Example Plugin for formae
*TODO: Update title and description for your plugin*
Example Formae plugin template - replace this with a description of what your plugin manages.
## Installation
```bash
# Install the plugin
make install
```
## Supported Resources
*TODO: Document your supported resource types*
| Resource Type | Description |
| ------------------------------ | ----------------------------------------------------- |
| `PROXMOX::Service::Resource` | Example resource (replace with your actual resources) |
## Configuration
Configure a target in your Forma file:
```pkl
new formae.Target {
label = "my-target"
namespace = "PROXMOX" // TODO: Update with your namespace
config = new Mapping {
["region"] = "us-east-1"
// TODO: Add your provider-specific configuration
}
}
```
## Examples
See the [examples/](examples/) directory for usage examples.
```bash
# Evaluate an example
formae eval examples/basic/main.pkl
# Apply resources
formae apply --mode reconcile --watch examples/basic/main.pkl
```
## Development
### Prerequisites
- Go 1.25+
- [Pkl CLI](https://pkl-lang.org/main/current/pkl-cli/index.html)
- Cloud provider credentials (for conformance testing)
### Building
```bash
make build # Build plugin binary
make test # Run unit tests
make lint # Run linter
make install # Build + install locally
```
### Local Testing
```bash
# Install plugin locally
make install
# Start formae agent
formae agent start
# Apply example resources
formae apply --mode reconcile --watch examples/basic/main.pkl
```
### Conformance Testing
Conformance tests validate your plugin's CRUD lifecycle using the test fixtures in `testdata/`:
| File | Purpose |
| ------------------------ | -------------------------------- |
| `resource.pkl` | Initial resource creation |
| `resource-update.pkl` | In-place update (mutable fields) |
| `resource-replace.pkl` | Replacement (createOnly fields) |
The test harness sets `FORMAE_TEST_RUN_ID` for unique resource naming between runs.
```bash
make conformance-test # Latest formae version
make conformance-test VERSION=0.80.0 # Specific version
```
The `scripts/ci/clean-environment.sh` script cleans up test resources. It runs before and after conformance tests and should be idempotent.
## Licensing
Plugins are independent works and may be licensed under any license of the authors choosing.
See the formae plugin policy:
<https://docs.formae.io/plugin-sdk/

22
conformance_test.go Normal file
View File

@@ -0,0 +1,22 @@
// © 2025 Platform Engineering Labs Inc.
//
// SPDX-License-Identifier: FSL-1.1-ALv2
//go:build conformance
// Conformance tests for the plugin. Run with: make conformance-test
package main
import (
"testing"
conformance "github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests"
)
func TestPluginConformance(t *testing.T) {
conformance.RunCRUDTests(t)
}
func TestPluginDiscovery(t *testing.T) {
conformance.RunDiscoveryTests(t)
}

12
examples/basic/PklProject Normal file
View File

@@ -0,0 +1,12 @@
amends "pkl:Project"
dependencies {
// Reference local plugin schema during development
// IMPORTANT: The alias must match the package name from the schema's PklProject
["proxmox"] = import("../../schema/pkl/PklProject")
// Formae schema - fetched from public registry
["formae"] {
uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.80.0"
}
}

46
examples/basic/main.pkl Normal file
View File

@@ -0,0 +1,46 @@
/*
* Basic Example
*
* This example shows how to define resources using your plugin.
* Run with: formae apply --mode reconcile --watch examples/basic/main.pkl
*/
amends "@formae/forma.pkl"
import "@formae/formae.pkl"
import "@proxmox/proxmox.pkl"
forma {
new formae.Stack {
label = "default"
description = "Default stack for example resources"
}
new formae.Target {
label = "my-target"
namespace = "PROXMOX"
// Add your provider-specific configuration here
// For typed config, create a Config class in your schema:
// config = new example.Config { region = "us-east-1" }
config = new Mapping {
["region"] = "us-east-1"
}
}
new example.ExampleResource {
label = "my-resource"
name = "My Example Resource"
description = "This is an example resource"
region = "us-east-1"
endpoint = new example.Endpoint {
url = "https://api.example.com"
port = 8080
protocol = "https"
}
tags = new Listing {
new example.Tag { key = "Environment"; value = "development" }
new example.Tag { key = "Team"; value = "platform" }
}
}
}

22
formae-plugin.pkl Normal file
View File

@@ -0,0 +1,22 @@
/*
* Formae Plugin Manifest
*
* This file defines metadata for your Formae plugin.
* Update the values below to match your plugin.
*/
name = "proxmox"
version = "0.1.0"
namespace = "PROXMOX"
description = "Proxmox Plugin"
license = "Apache-2.0"
minFormaeVersion = "0.80.1"
output {
renderer = new JsonRenderer {}
}

46
go.mod Normal file
View File

@@ -0,0 +1,46 @@
module github.com/platform-engineering-labs/formae-plugin-proxmox
go 1.25
require (
github.com/platform-engineering-labs/formae/pkg/plugin v0.1.8
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.1.9
)
require (
ergo.services/actor/statemachine v0.0.0-20251202053101-c0aa08b403e5 // indirect
ergo.services/ergo v1.999.310 // indirect
github.com/apple/pkl-go v0.12.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/masterminds/semver v1.5.0 // indirect
github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 // indirect
github.com/platform-engineering-labs/formae/pkg/model v0.1.2 // indirect
github.com/theory/jsonpath v0.10.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

93
go.sum Normal file
View File

@@ -0,0 +1,93 @@
ergo.services/actor/statemachine v0.0.0-20251202053101-c0aa08b403e5 h1:8b8Y8gLBvQrDIuKFSCwT089l1iltqsTEWXzhm/pSguE=
ergo.services/actor/statemachine v0.0.0-20251202053101-c0aa08b403e5/go.mod h1:epgQDQTm93L3/plOnIejSYbl/SNhulI78aa1LC0tLn0=
ergo.services/ergo v1.999.310 h1:qHx35J5UxCheNJaFrOK/1K6/p7jir680+NTPZo6bhbI=
ergo.services/ergo v1.999.310/go.mod h1:bLQ6PoO6Mz/8gVuzvPv3xfMfo1P9w6rZV1WnMXMeMdg=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/apple/pkl-go v0.12.0 h1:0gnhEIXo6coSHPpxdOESfGn2GrSkBSaeitkZLwZAcWE=
github.com/apple/pkl-go v0.12.0/go.mod h1:EDQmYVtFBok/eLI+9rT0EoBBXNtMM1THwR+rwBcAH3I=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/masterminds/semver v1.5.0 h1:hTxJTTY7tjvnWMrl08O6u3G6BLlKVwxSz01lVac9P8U=
github.com/masterminds/semver v1.5.0/go.mod h1:s7KNT9fnd7edGzwwP7RBX4H0v/CYd5qdOLfkL1V75yg=
github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 h1:ZMTgKwSomy2cVcl/+NivSqopbWeHbmYeQ+BxoYq8bVY=
github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1/go.mod h1:0ncHFCsGA6b0w1kBm6m+QwJ823qAY2vL47GvoR0BTyU=
github.com/platform-engineering-labs/formae/pkg/model v0.1.2 h1:FgjN2mS/9bLAlRIbAfIiD1PYeZ8fxNFVP4JyTQF6FXc=
github.com/platform-engineering-labs/formae/pkg/model v0.1.2/go.mod h1:XmGJA7jNPX9cEGc8TxTiEDitBuEVJOddNakdTZ/bH4U=
github.com/platform-engineering-labs/formae/pkg/plugin v0.1.8 h1:bECZEPHo6I6P8Qmpes5kDW/RFco3P7Z5xB3vvEuDGTo=
github.com/platform-engineering-labs/formae/pkg/plugin v0.1.8/go.mod h1:KzNzkc67phbeqs61ji+r8Y1WCD5QnDEpU7rfUHkmmuQ=
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.1.9 h1:5geWOQgor+GR4nR3vEbU0DMxdOisyIpo18zgYwJ0ngE=
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.1.9/go.mod h1:NuoGHFF4WCI73scZ4odspPzZiKCWRvrKWTvy0rgoNec=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/theory/jsonpath v0.10.2 h1:i8GeMxnD6ftNWeSeaGb/Eb8XghGjsas1eDizaQNupuE=
github.com/theory/jsonpath v0.10.2/go.mod h1:ZOz+y6MxTEDcN/FOxf9AOgeHSoKHx2B+E0nD3HOtzGE=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

11
main.go Normal file
View File

@@ -0,0 +1,11 @@
// © 2025 Platform Engineering Labs Inc.
//
// SPDX-License-Identifier: Apache-2.0
package main
import "github.com/platform-engineering-labs/formae/pkg/plugin/sdk"
func main() {
sdk.RunWithManifest(&Plugin{}, sdk.RunConfig{})
}

186
proxmox.go Normal file
View File

@@ -0,0 +1,186 @@
// © 2025 Platform Engineering Labs Inc.
//
// SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"errors"
"github.com/platform-engineering-labs/formae/pkg/plugin"
"github.com/platform-engineering-labs/formae/pkg/plugin/resource"
)
// ErrNotImplemented is returned by stub methods that need implementation.
var ErrNotImplemented = errors.New("not implemented")
// Plugin implements the Formae ResourcePlugin interface.
// The SDK automatically provides identity methods (Name, Version, Namespace)
// by reading formae-plugin.pkl at startup.
type Plugin struct{}
// Compile-time check: Plugin must satisfy ResourcePlugin interface.
var _ plugin.ResourcePlugin = &Plugin{}
// =============================================================================
// Configuration Methods
// =============================================================================
// RateLimit returns the rate limiting configuration for this plugin.
// Adjust MaxRequestsPerSecondForNamespace based on your provider's API limits.
func (p *Plugin) RateLimit() plugin.RateLimitConfig {
return plugin.RateLimitConfig{
Scope: plugin.RateLimitScopeNamespace,
MaxRequestsPerSecondForNamespace: 10, // TODO: Adjust based on provider limits
}
}
// DiscoveryFilters returns filters to exclude certain resources from discovery.
// Resources matching ALL conditions in a filter are excluded.
// Return nil if you want to discover all resources.
func (p *Plugin) DiscoveryFilters() []plugin.MatchFilter {
// Example: exclude resources with a specific tag
// return []plugin.MatchFilter{
// {
// ResourceTypes: []string{"PROXMOX::Service::Resource"},
// Conditions: []plugin.FilterCondition{
// {PropertyPath: "$.Tags[?(@.Key=='skip-discovery')].Value", PropertyValue: "true"},
// },
// },
// }
return nil
}
// LabelConfig returns the configuration for extracting human-readable labels
// from discovered resources.
func (p *Plugin) LabelConfig() plugin.LabelConfig {
return plugin.LabelConfig{
// Default JSONPath query to extract label from resources
// Example for tagged resources: $.Tags[?(@.Key=='Name')].Value
DefaultQuery: "$.Name", // TODO: Adjust for your provider
// Override for specific resource types
ResourceOverrides: map[string]string{
// "PROXMOX::Service::SpecialResource": "$.DisplayName",
},
}
}
// =============================================================================
// CRUD Operations
// =============================================================================
// Create provisions a new resource.
func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) {
// TODO: Implement resource creation
//
// 1. Parse req.Properties to get resource configuration (json.RawMessage)
// 2. Parse req.TargetConfig to get provider credentials/config
// 3. Call your provider's API to create the resource
// 4. Return ProgressResult with:
// - NativeID: the provider's identifier for the resource
// - OperationStatus: Success, Failure, or InProgress
// - If InProgress, set RequestID for status polling
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: "Create not implemented",
},
}, ErrNotImplemented
}
// Read retrieves the current state of a resource.
func (p *Plugin) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResult, error) {
// TODO: Implement resource read
//
// 1. Use req.NativeID to identify the resource
// 2. Parse req.TargetConfig for provider credentials
// 3. Call your provider's API to get current state
// 4. Return ReadResult with Properties as JSON string
return &resource.ReadResult{
ResourceType: req.ResourceType,
ErrorCode: resource.OperationErrorCodeInternalFailure,
}, ErrNotImplemented
}
// Update modifies an existing resource.
func (p *Plugin) Update(ctx context.Context, req *resource.UpdateRequest) (*resource.UpdateResult, error) {
// TODO: Implement resource update
//
// 1. Use req.NativeID to identify the resource
// 2. Use req.PatchDocument for changes (JSON Patch format)
// Or compare req.PriorProperties with req.DesiredProperties
// 3. Call your provider's API to apply changes
// 4. Return ProgressResult with status
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: "Update not implemented",
},
}, ErrNotImplemented
}
// Delete removes a resource.
func (p *Plugin) Delete(ctx context.Context, req *resource.DeleteRequest) (*resource.DeleteResult, error) {
// TODO: Implement resource deletion
//
// 1. Use req.NativeID to identify the resource
// 2. Parse req.TargetConfig for provider credentials
// 3. Call your provider's API to delete the resource
// 4. Return ProgressResult with status
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationDelete,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: "Delete not implemented",
},
}, ErrNotImplemented
}
// Status checks the progress of an async operation.
// Called when Create/Update/Delete return InProgress status.
func (p *Plugin) Status(ctx context.Context, req *resource.StatusRequest) (*resource.StatusResult, error) {
// TODO: Implement status checking for async operations
//
// 1. Use req.RequestID to identify the operation
// 2. Call your provider's API to check operation status
// 3. Return ProgressResult with current status
//
// If your provider's operations are synchronous, return Success immediately.
return &resource.StatusResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCheckStatus,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: "Status not implemented",
},
}, ErrNotImplemented
}
// List returns all resource identifiers of a given type.
// Called during discovery to find unmanaged resources.
func (p *Plugin) List(ctx context.Context, req *resource.ListRequest) (*resource.ListResult, error) {
// TODO: Implement resource listing for discovery
//
// 1. Use req.ResourceType to determine what to list
// 2. Parse req.TargetConfig for provider credentials
// 3. Use req.PageToken/PageSize for pagination
// 4. Call your provider's API to list resources
// 5. Return NativeIDs and NextPageToken (if more pages)
return &resource.ListResult{
NativeIDs: []string{},
NextPageToken: nil,
}, ErrNotImplemented
}

16
schema/pkl/PklProject Normal file
View File

@@ -0,0 +1,16 @@
amends "pkl:Project"
// Package configuration for local development.
// Update baseUri and packageZipUrl when publishing to a registry.
package {
name = "proxmox"
baseUri = "package://localhost/plugins/example/schema/pkl/example"
version = "0.1.0"
packageZipUrl = "https://localhost/plugins/example/schema/pkl/example@\(version).zip"
}
dependencies {
["formae"] {
uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.80.1"
}
}

21
schema/pkl/proxmox.pkl Normal file
View File

@@ -0,0 +1,21 @@
module proxmox.lxc
import "@formae/formae.pkl"
@formae.ResourceHint {
type = "PROXMOX::Service::LXC"
identifier = "$.vmid"
}
class ExampleResource extends formae.Resource {
fixed hidden type: String = "PROXMOX::Service::LXC"
@formae.FieldHint { createOnly = true }
vmid: String
@formae.FieldHint {}
name: String
@formae.FieldHint {}
description: String = "No description"
}

61
scripts/ci/clean-environment.sh Executable file
View File

@@ -0,0 +1,61 @@
#!/bin/bash
# © 2025 Platform Engineering Labs Inc.
# SPDX-License-Identifier: Apache-2.0
#
# Clean Environment Hook
# ======================
# This script is called before AND after conformance tests to clean up
# test resources in your cloud environment.
#
# Purpose:
# - Before tests: Remove orphaned resources from previous failed runs
# - After tests: Clean up resources created during the test run
#
# The script should be idempotent - safe to run multiple times.
# It should delete all resources matching the test resource prefix.
#
# Test resources typically use a naming convention like:
# formae-plugin-sdk-test-{run-id}-*
#
# Implementation varies by provider. Examples:
#
# AWS:
# - List and delete resources with test prefix using AWS CLI
# - Use resource tagging for easier identification
#
# OpenStack:
# - Use openstack CLI to list and delete test resources
# - Clean up in order: instances, volumes, networks, security groups, etc.
#
# Exit with non-zero status only for unexpected errors.
# Missing resources (already cleaned) should not cause failures.
set -euo pipefail
# Prefix used for test resources - should match what conformance tests create
TEST_PREFIX="${TEST_PREFIX:-formae-plugin-sdk-test-}"
echo "clean-environment.sh: Cleaning resources with prefix '${TEST_PREFIX}'"
echo ""
echo "To implement cleanup for your provider, edit this script."
echo "See comments in this file for examples."
echo ""
# Uncomment and modify for your provider:
#
# # AWS - clean up S3 buckets with test prefix
# echo "Cleaning S3 buckets..."
# aws s3api list-buckets --query "Buckets[?starts_with(Name, '${TEST_PREFIX}')].Name" --output text | \
# xargs -r -n1 aws s3 rb --force s3://
#
# # OpenStack - clean up instances
# echo "Cleaning instances..."
# openstack server list --name "^${TEST_PREFIX}" -f value -c ID | \
# xargs -r -n1 openstack server delete --wait
#
# # OpenStack - clean up volumes
# echo "Cleaning volumes..."
# openstack volume list --name "^${TEST_PREFIX}" -f value -c ID | \
# xargs -r -n1 openstack volume delete
echo "clean-environment.sh: Cleanup complete (no-op - not configured)"

174
scripts/run-conformance-tests.sh Executable file
View File

@@ -0,0 +1,174 @@
#!/bin/bash
# © 2025 Platform Engineering Labs Inc.
# SPDX-License-Identifier: FSL-1.1-ALv2
#
# Script to run conformance tests against a specific version of formae.
#
# Usage:
# ./scripts/run-conformance-tests.sh [VERSION]
#
# Arguments:
# VERSION - Optional formae version (e.g., 0.76.0). Defaults to "latest".
#
# Environment variables:
# FORMAE_BINARY - Path to formae binary (skips download if set)
# FORMAE_INSTALL_PREFIX - Installation directory (default: temp directory)
# FORMAE_TEST_FILTER - Filter tests by name pattern (e.g., "s3-bucket")
# FORMAE_TEST_TYPE - Select test type: "all" (default), "crud", or "discovery"
set -euo pipefail
# Cross-platform sed in-place edit (macOS vs Linux)
sed_inplace() {
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "$@"
else
sed -i "$@"
fi
}
VERSION="${1:-latest}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# =============================================================================
# Setup Formae Binary
# =============================================================================
# Check if FORMAE_BINARY is already set and valid
if [[ -n "${FORMAE_BINARY:-}" ]] && [[ -x "${FORMAE_BINARY}" ]]; then
echo "Using FORMAE_BINARY from environment: ${FORMAE_BINARY}"
# Extract version from binary if not explicitly provided
if [[ "${VERSION}" == "latest" ]]; then
VERSION=$("${FORMAE_BINARY}" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ -z "${VERSION}" ]]; then
echo "Warning: Could not extract version from FORMAE_BINARY, using 'latest'"
VERSION="latest"
else
echo "Detected formae version: ${VERSION}"
fi
fi
else
# Always download formae to temp directory for conformance tests
# Don't use system-installed formae to ensure version consistency
INSTALL_DIR=$(mktemp -d -t formae-conformance-XXXXXX)
echo "Using temp directory: ${INSTALL_DIR}"
trap "rm -rf ${INSTALL_DIR}" EXIT
# Determine OS and architecture
DETECTED_OS=$(uname | tr '[:upper:]' '[:lower:]')
DETECTED_ARCH=$(uname -m | tr -d '_')
# Resolve version if "latest"
if [[ "${VERSION}" == "latest" ]]; then
echo "Resolving latest version..."
VERSION=$(curl -s https://hub.platform.engineering/binaries/repo.json | \
jq -r "[.Packages[] | select(.Version | index(\"-\") | not) | select(.OsArch.OS == \"${DETECTED_OS}\" and .OsArch.Arch == \"${DETECTED_ARCH}\")][0].Version")
if [[ -z "${VERSION}" || "${VERSION}" == "null" ]]; then
echo "Error: Could not determine latest version for ${DETECTED_OS}-${DETECTED_ARCH}"
exit 1
fi
fi
echo "Downloading formae version ${VERSION}..."
PKGNAME="formae@${VERSION}_${DETECTED_OS}-${DETECTED_ARCH}.tgz"
DOWNLOAD_URL="https://hub.platform.engineering/binaries/pkgs/${PKGNAME}"
if ! curl -fsSL "${DOWNLOAD_URL}" -o "${INSTALL_DIR}/${PKGNAME}"; then
echo "Error: Failed to download ${DOWNLOAD_URL}"
exit 1
fi
# Extract to install directory
echo "Extracting..."
tar -xzf "${INSTALL_DIR}/${PKGNAME}" -C "${INSTALL_DIR}"
# Find the formae binary
FORMAE_BINARY="${INSTALL_DIR}/formae/bin/formae"
if [[ ! -x "${FORMAE_BINARY}" ]]; then
# Try alternative locations
if [[ -x "${INSTALL_DIR}/bin/formae" ]]; then
FORMAE_BINARY="${INSTALL_DIR}/bin/formae"
elif [[ -x "${INSTALL_DIR}/formae" ]]; then
FORMAE_BINARY="${INSTALL_DIR}/formae"
else
echo "Error: formae binary not found in ${INSTALL_DIR}"
find "${INSTALL_DIR}" -name "formae" -type f 2>/dev/null || ls -laR "${INSTALL_DIR}"
exit 1
fi
fi
fi
echo ""
echo "Using formae binary: ${FORMAE_BINARY}"
"${FORMAE_BINARY}" --version
# Export environment variables for the tests
# FORMAE_VERSION is required by the plugin SDK to resolve PKL schema paths
export FORMAE_BINARY
export FORMAE_VERSION="${VERSION}"
# Pass through test filter and type if set
if [[ -n "${FORMAE_TEST_FILTER:-}" ]]; then
export FORMAE_TEST_FILTER
echo "Test filter: ${FORMAE_TEST_FILTER}"
fi
if [[ -n "${FORMAE_TEST_TYPE:-}" ]]; then
export FORMAE_TEST_TYPE
echo "Test type: ${FORMAE_TEST_TYPE}"
fi
# =============================================================================
# Update and Resolve PKL Dependencies
# =============================================================================
# Update testdata/PklProject with the resolved formae version, then resolve
# dependencies from the public package registry.
# =============================================================================
echo ""
echo "Updating PKL dependencies for formae version ${VERSION}..."
# Update PklProject files with the resolved formae version
if [[ "${VERSION}" != "latest" ]]; then
# Update schema/pkl/PklProject (plugin schema depends on formae)
if [[ -f "${PROJECT_ROOT}/schema/pkl/PklProject" ]]; then
echo "Updating schema/pkl/PklProject to use formae@${VERSION}..."
sed_inplace "s|formae/formae@[0-9a-zA-Z.\-]*\"|formae/formae@${VERSION}\"|g" "${PROJECT_ROOT}/schema/pkl/PklProject"
fi
# Update testdata/PklProject (test files depend on formae)
if [[ -f "${PROJECT_ROOT}/testdata/PklProject" ]]; then
echo "Updating testdata/PklProject to use formae@${VERSION}..."
sed_inplace "s|formae/formae@[0-9a-zA-Z.\-]*\"|formae/formae@${VERSION}\"|g" "${PROJECT_ROOT}/testdata/PklProject"
fi
fi
# Resolve schema dependencies (if any)
if [[ -f "${PROJECT_ROOT}/schema/pkl/PklProject" ]]; then
echo "Resolving schema/pkl dependencies..."
if ! pkl project resolve "${PROJECT_ROOT}/schema/pkl" 2>&1; then
echo "Error: Failed to resolve schema/pkl dependencies"
echo "Make sure the formae PKL package is accessible at the configured URL"
exit 1
fi
fi
# Resolve testdata dependencies
if [[ -f "${PROJECT_ROOT}/testdata/PklProject" ]]; then
echo "Resolving testdata dependencies..."
if ! pkl project resolve "${PROJECT_ROOT}/testdata" 2>&1; then
echo "Error: Failed to resolve testdata dependencies"
exit 1
fi
fi
echo "PKL dependencies resolved successfully"
# =============================================================================
# Run Conformance Tests
# =============================================================================
echo ""
echo "Running conformance tests..."
cd "${PROJECT_ROOT}"
go test -tags=conformance -v -timeout 30m ./...

12
testdata/PklProject vendored Normal file
View File

@@ -0,0 +1,12 @@
amends "pkl:Project"
dependencies {
// Reference local plugin schema during development
// IMPORTANT: The alias must match the package name from the schema's PklProject
["proxmox"] = import("../schema/pkl/PklProject")
// Formae schema - fetched from public registry
["formae"] {
uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.80.0"
}
}

50
testdata/resource-replace.pkl vendored Normal file
View File

@@ -0,0 +1,50 @@
/*
* Conformance Test: Replace Resource
*
* This file modifies a createOnly field to trigger resource replacement.
* Changes from resource.pkl:
* - region: changed from "us-east-1" to "us-west-2"
*
* The region field has createOnly=true, so formae should delete the
* existing resource and create a new one with the new region.
*/
amends "@formae/forma.pkl"
import "@formae/formae.pkl"
import "@proxmox/proxmox.pkl"
local stackName = "plugin-sdk-test-stack"
local testRunID = read("env:FORMAE_TEST_RUN_ID")
forma {
new formae.Stack {
label = stackName
description = "Plugin SDK conformance test stack"
}
new formae.Target {
label = "example-target"
namespace = "PROXMOX"
config = new Mapping {
["region"] = "us-west-2" // CHANGED to match resource
}
}
new example.ExampleResource {
label = "plugin-sdk-test-resource"
name = "formae-plugin-sdk-test-\(testRunID)"
description = "Test resource for plugin SDK conformance tests"
region = "us-west-2" // CHANGED - triggers replacement
endpoint = new example.Endpoint {
url = "https://api.example.com"
port = 8080
protocol = "https"
}
tags = new Listing {
new example.Tag { key = "Environment"; value = "test" }
new example.Tag { key = "ManagedBy"; value = "formae" }
}
}
}

54
testdata/resource-update.pkl vendored Normal file
View File

@@ -0,0 +1,54 @@
/*
* Conformance Test: Update Resource (in-place)
*
* This file modifies mutable fields to trigger an in-place update.
* Changes from resource.pkl:
* - name: added "-updated" suffix
* - description: changed text
* - endpoint.port: changed from 8080 to 9090
* - tags: added a new tag
*
* These fields do NOT have createOnly=true, so formae should update
* the resource in place without replacement.
*/
amends "@formae/forma.pkl"
import "@formae/formae.pkl"
import "@proxmox/proxmox.pkl"
local stackName = "plugin-sdk-test-stack"
local testRunID = read("env:FORMAE_TEST_RUN_ID")
forma {
new formae.Stack {
label = stackName
description = "Plugin SDK conformance test stack"
}
new formae.Target {
label = "example-target"
namespace = "PROXMOX"
config = new Mapping {
["region"] = "us-east-1"
}
}
new example.ExampleResource {
label = "plugin-sdk-test-resource"
name = "formae-plugin-sdk-test-\(testRunID)-updated" // CHANGED
description = "Test resource - UPDATED" // CHANGED
region = "us-east-1" // unchanged (createOnly)
endpoint = new example.Endpoint {
url = "https://api.example.com"
port = 9090 // CHANGED from 8080
protocol = "https"
}
tags = new Listing {
new example.Tag { key = "Environment"; value = "test" }
new example.Tag { key = "ManagedBy"; value = "formae" }
new example.Tag { key = "UpdatedAt"; value = "conformance-test" } // ADDED
}
}
}

49
testdata/resource.pkl vendored Normal file
View File

@@ -0,0 +1,49 @@
/*
* Conformance Test: Create Resource
*
* This file defines the initial resource state for conformance testing.
* The conformance test harness will apply this file first.
*/
amends "@formae/forma.pkl"
import "@formae/formae.pkl"
import "@proxmox/proxmox.pkl"
local stackName = "plugin-sdk-test-stack"
// Read the test run ID from environment variable set by the test harness
// This ensures consistent naming within a test run but unique names between runs
local testRunID = read("env:FORMAE_TEST_RUN_ID")
forma {
new formae.Stack {
label = stackName
description = "Plugin SDK conformance test stack"
}
new formae.Target {
label = "example-target"
namespace = "PROXMOX"
// TODO: Add your provider-specific configuration
config = new Mapping {
["region"] = "us-east-1"
}
}
new example.ExampleResource {
label = "plugin-sdk-test-resource"
name = "formae-plugin-sdk-test-\(testRunID)"
description = "Test resource for plugin SDK conformance tests"
region = "us-east-1"
endpoint = new example.Endpoint {
url = "https://api.example.com"
port = 8080
protocol = "https"
}
tags = new Listing {
new example.Tag { key = "Environment"; value = "test" }
new example.Tag { key = "ManagedBy"; value = "formae" }
}
}
}