Compare commits

..

12 Commits

Author SHA1 Message Date
d6953c8a78 fix: downgrade ergo.services/ergo to v1.999.310 2026-02-28 22:56:33 +01:00
557948a633 fix: update versions 2026-02-28 22:34:41 +01:00
727013e42a fix: remove github workflow 2026-02-26 23:11:47 +01:00
821f0e9f59 fix(LXC): network deletion
Some checks failed
CI / build (push) Failing after 3m44s
CI / lint (push) Failing after 3m14s
CI / pkl-validate (push) Successful in 11s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-23 22:44:30 +01:00
b8b1bbcdf7 feat(LXC): add networks; closes #2
Some checks failed
CI / build (push) Failing after 3m42s
CI / lint (push) Failing after 3m8s
CI / pkl-validate (push) Successful in 13s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-17 23:13:27 +01:00
48451c6717 feat(LXC): add ssh-keys
Some checks failed
CI / build (push) Failing after 3m54s
CI / lint (push) Failing after 3m24s
CI / pkl-validate (push) Successful in 15s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-14 20:59:42 +01:00
7f84ceaafd fix: force delete LXC and purge; test log messages
Some checks failed
CI / build (push) Failing after 4m2s
CI / lint (push) Failing after 3m23s
CI / pkl-validate (push) Successful in 11s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-14 20:30:10 +01:00
959257eac8 fix: add creation password
Some checks failed
CI / build (push) Failing after 3m45s
CI / lint (push) Failing after 3m20s
CI / pkl-validate (push) Successful in 11s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-12 23:14:19 +01:00
476906fe97 feat: add setting custom log level
Some checks failed
CI / build (push) Failing after 3m53s
CI / lint (push) Failing after 3m33s
CI / pkl-validate (push) Successful in 13s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-12 22:51:49 +01:00
6fd95c24fb fix: replace log with slog
Some checks failed
CI / build (push) Failing after 4m25s
CI / lint (push) Failing after 3m33s
CI / pkl-validate (push) Successful in 12s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-11 21:22:02 +01:00
638e9f42d5 fix: add status
Some checks failed
CI / build (push) Failing after 4m37s
CI / lint (push) Failing after 3m39s
CI / pkl-validate (push) Successful in 24s
CI / integration-tests (push) Has been skipped
CI / conformance-tests (latest) (push) Has been skipped
2026-02-05 18:36:53 +01:00
417c304f2d fix: moved lxc methods to seperate file 2026-02-05 18:03:59 +01:00
16 changed files with 699 additions and 655 deletions

View File

@@ -1,198 +0,0 @@
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"

View File

@@ -123,3 +123,6 @@ conformance-test-discovery: install
test-request:
curl -k -H "Authorization: PVEAPIToken=$$PROXMOX_USERNAME=$$PROXMOX_TOKEN" "https://proxmox.mid:8006/api2/json/nodes/proxmox/lxc" | jq
curl -k -H "Authorization: PVEAPIToken=$$PROXMOX_USERNAME=$$PROXMOX_TOKEN" "https://proxmox.mid:8006/api2/json/nodes/proxmox/lxc/200/config" | jq
test-delete:
curl -k -H "Authorization: PVEAPIToken=$$PROXMOX_USERNAME=$$PROXMOX_TOKEN" "https://proxmox.mid:8006/api2/json/nodes/proxmox/lxc/210/config" -X PUT --data "delete=net0,net1,net2" | jq

View File

@@ -15,7 +15,7 @@ description = "Proxmox Plugin"
license = "Apache-2.0"
minFormaeVersion = "0.80.1"
minFormaeVersion = "0.82.2"
output {
renderer = new JsonRenderer {}

52
go.mod
View File

@@ -1,53 +1,63 @@
module github.com/platform-engineering-labs/formae-plugin-proxmox
go 1.25
go 1.25.0
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
github.com/platform-engineering-labs/formae/pkg/plugin v0.1.14
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.1.21
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v4 v4.26.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/contrib/instrumentation/host v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
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/apple/pkl-go v0.13.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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // 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/platform-engineering-labs/formae/pkg/model v0.1.5 // indirect
github.com/stretchr/testify v1.11.1
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/match v1.2.0 // 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/otel v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.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
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

111
go.sum
View File

@@ -4,39 +4,56 @@ 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/apple/pkl-go v0.13.0 h1:dWaTtgVtatyiECJbdsVmZ/n3JJZTDOyTum/QBHEFv5o=
github.com/apple/pkl-go v0.13.0/go.mod h1:Ko3AgXOKd/vVYtsRZgoCZhymymz9RxqCIcfdZhOX85I=
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/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
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/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
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/platform-engineering-labs/formae/pkg/model v0.1.5 h1:MNOLv5gXT3hCcVXgpJWwQz8c3ORH3e1c5jXhpuVCVVo=
github.com/platform-engineering-labs/formae/pkg/model v0.1.5/go.mod h1:XmGJA7jNPX9cEGc8TxTiEDitBuEVJOddNakdTZ/bH4U=
github.com/platform-engineering-labs/formae/pkg/plugin v0.1.14 h1:pzTjKuNtFZ/3I+f8F6U7tJtV7jMo64YdAY6v589ZPOA=
github.com/platform-engineering-labs/formae/pkg/plugin v0.1.14/go.mod h1:heydN5suh9RwjdCmZhttA9Yni0pRJswSx/tHfzwBERk=
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.1.21 h1:H117sydtz0RG1eIMLTb+1QW6IpwxdJSK/lic/fiXKS4=
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.1.21/go.mod h1:GbypGizmbHw2Tl6TvLPuu3v7CqOmKZxIMx+NMgUvUkw=
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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
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=
@@ -44,51 +61,67 @@ github.com/theory/jsonpath v0.10.2/go.mod h1:ZOz+y6MxTEDcN/FOxf9AOgeHSoKHx2B+E0n
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/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/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/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
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=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
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/contrib/instrumentation/host v0.65.0 h1:cR4LpCn/2xDNdW3saBLrGJW7vWmrYlHYIhfuklhrlUc=
go.opentelemetry.io/contrib/instrumentation/host v0.65.0/go.mod h1:laAqufqDgLYaaewUBpolv8GePmhIVqIeHyudbmi9KYk=
go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ=
go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0/go.mod h1:Z1pjGxUL3nJ/IbDDfL6rBD0Xbz7ZOViRqrIUg4l1CYE=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
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=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/url"
"os"
@@ -37,18 +37,13 @@ func getCredentials() (username, token string, err error) {
return username, token, nil
}
func parseLXCProperties(data json.RawMessage) (*LXCProperties, error) {
var props LXCProperties
if err := json.Unmarshal(data, &props); err != nil {
return nil, fmt.Errorf("invalid file properties: %w", err)
}
if props.VMID == "" {
return nil, fmt.Errorf("vmid missing")
}
if props.Hostname == "" {
return nil, fmt.Errorf("name missing")
}
return &props, nil
func setupLogging() {
programLevel := new(slog.LevelVar)
env := os.Getenv("PROXMOX_LOG_LEVEL")
programLevel.UnmarshalText([]byte(env))
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
slog.Info("Set log level", "level", programLevel)
slog.SetDefault(logger)
}
func createAuthorizationString(username, token string) string {
@@ -64,24 +59,24 @@ func authenticatedRequest(method, url, authorization string, urlparams url.Value
request, err := http.NewRequest(method, url, body)
if err != nil {
log.Println("Error: ", err)
slog.Error("Error creating request", "err", err)
return nil, err
}
request.Header.Set("Authorization", authorization)
resp, err := client.Do(request)
if err != nil {
log.Println("Error: ", err)
slog.Error("Error executing request", "err", err)
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("Error: ", err)
slog.Error("Error reading response", "err", err)
return nil, err
}
log.Println("URL: ", method, "Status Code:", resp.Status, "Body: ", string(data))
slog.Debug("Executed Request", "url", method, "params", urlparams, "status code", resp.Status, "body", string(data))
return data, nil
}

512
lxc.go Normal file
View File

@@ -0,0 +1,512 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/platform-engineering-labs/formae/pkg/plugin/resource"
)
const MAX_NETWORK_COUNT = 10
func parseLXCProperties(data json.RawMessage) (*LXCProperties, error) {
var props LXCProperties
if err := json.Unmarshal(data, &props); err != nil {
return nil, fmt.Errorf("invalid file properties: %w", err)
}
if props.VMID == "" {
return nil, fmt.Errorf("vmid missing")
}
if props.Hostname == "" {
return nil, fmt.Errorf("name missing")
}
return &props, nil
}
func (p *Plugin) CreateLXC(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) {
props, err := parseLXCProperties(req.Properties)
if err != nil {
slog.Error(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: err.Error(),
},
}, err
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
slog.Error(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
username, token, err := getCredentials()
if err != nil {
slog.Error(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
urlparams := url.Values{
"vmid": {props.VMID},
"ostemplate": {props.OSTemplate},
"password": {props.Password},
"hostname": {props.Hostname},
"cores": {strconv.Itoa(props.Cores)},
"memory": {strconv.Itoa(props.Memory)},
"ssh-public-keys": {strings.Join(props.SSHKeys, "\n")},
}
if props.Description != "" {
urlparams.Add("description", props.Description)
}
if props.OnBoot != 0 {
urlparams.Add("onboot", strconv.Itoa(props.OnBoot))
}
for i := range min(MAX_NETWORK_COUNT, len(props.Networks)) {
urlparams.Add(fmt.Sprintf("net%d", i), props.Networks[i])
}
data, err := authenticatedRequest(http.MethodPost, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc", createAuthorizationString(username, token), urlparams)
if err != nil {
slog.Error(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
var taskData ProxmoxDataResponse
err = json.Unmarshal(data, &taskData)
if err != nil {
slog.Error(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusSuccess,
RequestID: taskData.Data,
NativeID: props.VMID,
},
}, nil
}
func (p *Plugin) ReadLXC(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResult, error) {
username, token, err := getCredentials()
if err != nil {
slog.Error(err.Error())
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeInvalidRequest,
}, err
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
slog.Error(err.Error())
return &resource.ReadResult{}, nil
}
data, err := authenticatedRequest(http.MethodGet, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc/"+req.NativeID+"/config", createAuthorizationString(username, token), nil)
if err != nil {
slog.Error(err.Error())
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeNetworkFailure,
}, err
}
var props StatusLXCConfigResponse
err = json.Unmarshal(data, &props)
if err != nil {
slog.Error("Error unmarshalling json", "data", data, "err", err.Error())
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeInvalidRequest,
}, err
}
lxcdata := props.Data
networks := []string{}
if len(lxcdata.Net0) > 0 {
networks = append(networks, lxcdata.Net0)
}
if len(lxcdata.Net1) > 0 {
networks = append(networks, lxcdata.Net1)
}
if len(lxcdata.Net2) > 0 {
networks = append(networks, lxcdata.Net2)
}
if len(lxcdata.Net3) > 0 {
networks = append(networks, lxcdata.Net3)
}
if len(lxcdata.Net4) > 0 {
networks = append(networks, lxcdata.Net4)
}
if len(lxcdata.Net5) > 0 {
networks = append(networks, lxcdata.Net5)
}
if len(lxcdata.Net6) > 0 {
networks = append(networks, lxcdata.Net6)
}
if len(lxcdata.Net7) > 0 {
networks = append(networks, lxcdata.Net7)
}
if len(lxcdata.Net8) > 0 {
networks = append(networks, lxcdata.Net8)
}
if len(lxcdata.Net9) > 0 {
networks = append(networks, lxcdata.Net9)
}
properties := LXCProperties{
VMID: req.NativeID,
Hostname: lxcdata.Hostname,
Description: lxcdata.Description,
Cores: lxcdata.Cores,
Memory: lxcdata.Memory,
OnBoot: lxcdata.OnBoot,
Networks: networks,
}
propsJSON, err := json.Marshal(properties)
if err != nil {
slog.Error(err.Error())
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeInternalFailure,
}, err
}
return &resource.ReadResult{
ResourceType: req.ResourceType,
Properties: string(propsJSON),
}, nil
}
func (p *Plugin) UpdateLXC(ctx context.Context, req *resource.UpdateRequest) (*resource.UpdateResult, error) {
prior, err := parseLXCProperties(req.PriorProperties)
if err != nil {
slog.Error(err.Error())
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: err.Error(),
},
}, err
}
desir, err := parseLXCProperties(req.DesiredProperties)
if err != nil {
slog.Error(err.Error())
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: err.Error(),
},
}, err
}
if prior == nil {
p.Create(ctx, &resource.CreateRequest{
ResourceType: req.ResourceType,
Properties: req.DesiredProperties,
TargetConfig: req.TargetConfig,
})
}
if prior.VMID != desir.VMID {
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: "can't change vmid",
},
}, fmt.Errorf("can't change vmid")
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
slog.Error(err.Error())
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
username, token, err := getCredentials()
if err != nil {
slog.Error(err.Error())
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeAccessDenied,
StatusMessage: err.Error(),
},
}, err
}
urlparams := url.Values{
"vmid": {desir.VMID},
"hostname": {desir.Hostname},
"cores": {strconv.Itoa(desir.Cores)},
"memory": {strconv.Itoa(desir.Memory)},
"description": {desir.Description},
}
if prior.OnBoot != desir.OnBoot {
urlparams.Add("onboot", strconv.Itoa(desir.OnBoot))
}
toDelete := []string{}
for i := range min(MAX_NETWORK_COUNT, max(len(desir.Networks), len(prior.Networks))) {
if i < len(desir.Networks) {
urlparams.Add(fmt.Sprintf("net%d", i), desir.Networks[i])
} else {
toDelete = append(toDelete, fmt.Sprintf("net%d", i))
}
}
urlparams.Add("delete", strings.Join(toDelete, ","))
_, err = authenticatedRequest("PUT", config.URL+"/api2/json/nodes/"+config.NODE+"/lxc/"+desir.VMID+"/config", createAuthorizationString(username, token), urlparams)
if err != nil {
slog.Error(err.Error())
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
result, err := p.Read(ctx, &resource.ReadRequest{
NativeID: req.NativeID,
ResourceType: req.ResourceType,
TargetConfig: req.TargetConfig,
})
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusSuccess,
NativeID: req.NativeID,
ResourceProperties: json.RawMessage(result.Properties),
},
}, nil
}
func (p *Plugin) DeleteLXC(ctx context.Context, req *resource.DeleteRequest) (*resource.DeleteResult, error) {
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
slog.Error(err.Error())
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
username, token, err := getCredentials()
if err != nil {
slog.Error(err.Error())
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
_, err = authenticatedRequest(http.MethodDelete, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc/"+req.NativeID+"?force=1&purge=1", createAuthorizationString(username, token), nil)
if err != nil {
slog.Error(err.Error())
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusSuccess,
NativeID: req.NativeID,
},
}, nil
}
type RequestStatusProxmoxResponse struct {
PId int `json:"pid"`
UpId string `json:"upid"`
Node string `json:"node"`
PStart int `json:"pstart"`
Status string `json:"status"`
Id string `json:"id"`
StartTime int `json:"starttime"`
ExitStatus string `json:"exitstatus"`
User string `json:"user"`
Type string `json:"type"`
}
func (p *Plugin) StatusLXC(ctx context.Context, req *resource.StatusRequest) (*resource.StatusResult, error) {
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
slog.Error(err.Error())
return &resource.StatusResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCheckStatus,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
username, token, err := getCredentials()
if err != nil {
slog.Error(err.Error())
return &resource.StatusResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCheckStatus,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
var proxmoxResponse RequestStatusProxmoxResponse
data, err := authenticatedRequest(http.MethodDelete, config.URL+"/api2/json/nodes/"+config.NODE+"/tasks/"+req.RequestID+"/status", createAuthorizationString(username, token), nil)
err = json.Unmarshal(data, &proxmoxResponse)
if err != nil {
slog.Error(err.Error())
return &resource.StatusResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCheckStatus,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
var status resource.OperationStatus
switch proxmoxResponse.Status {
case "running":
status = resource.OperationStatusInProgress
case "stopped":
status = resource.OperationStatusSuccess
}
return &resource.StatusResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCheckStatus,
OperationStatus: status,
},
}, nil
}
func (p *Plugin) ListLXC(ctx context.Context, req *resource.ListRequest) (*resource.ListResult, error) {
username, token, err := getCredentials()
if err != nil {
slog.Error(err.Error())
return &resource.ListResult{
NativeIDs: []string{},
}, err
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
slog.Error(err.Error())
return &resource.ListResult{
NativeIDs: []string{},
}, err
}
var props StatusGeneralResponse
data, err := authenticatedRequest(http.MethodGet, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc", createAuthorizationString(username, token), nil)
if err != nil {
slog.Error(err.Error())
return &resource.ListResult{
NativeIDs: []string{},
}, err
}
json.Unmarshal(data, &props)
nativeIds := make([]string, 0, len(props.Data))
for _, value := range props.Data {
nativeIds = append(nativeIds, strconv.Itoa(value.VMID))
}
return &resource.ListResult{
NativeIDs: nativeIds,
NextPageToken: nil,
}, nil
}

View File

@@ -6,13 +6,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"github.com/platform-engineering-labs/formae/pkg/plugin"
"github.com/platform-engineering-labs/formae/pkg/plugin/resource"
@@ -20,7 +14,6 @@ import (
// https://pve.proxmox.com/pve-docs/api-viewer/
// ErrNotImplemented is returned by stub methods that need implementation.
var ErrNotImplemented = errors.New("not implemented")
// Plugin implements the Formae ResourcePlugin interface.
@@ -35,28 +28,14 @@ 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
MaxRequestsPerSecondForNamespace: 10,
}
}
// 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
}
@@ -64,11 +43,7 @@ func (p *Plugin) DiscoveryFilters() []plugin.MatchFilter {
// 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: "$.hostname",
// Override for specific resource types
ResourceOverrides: map[string]string{
// "PROXMOX::Service::SpecialResource": "$.DisplayName",
},
@@ -79,358 +54,33 @@ func (p *Plugin) LabelConfig() plugin.LabelConfig {
// CRUD Operations
// =============================================================================
// Create provisions a new resource.
func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) {
props, err := parseLXCProperties(req.Properties)
if err != nil {
log.Println(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: err.Error(),
},
}, err
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
log.Println(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
username, token, err := getCredentials()
if err != nil {
log.Println(err.Error())
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
urlparams := url.Values{
"vmid": {props.VMID},
"ostemplate": {props.OSTemplate},
"hostname": {props.Hostname},
"cores": {strconv.Itoa(props.Cores)},
"memory": {strconv.Itoa(props.Memory)},
}
if props.Description != "" {
urlparams.Add("description", props.Description)
}
if props.OnBoot != 0 {
urlparams.Add("onboot", strconv.Itoa(props.OnBoot))
}
_, err = authenticatedRequest(http.MethodPost, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc", createAuthorizationString(username, token), urlparams)
if err != nil {
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
return &resource.CreateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusSuccess,
NativeID: props.VMID,
},
}, nil
setupLogging()
return p.CreateLXC(ctx, req)
}
func (p *Plugin) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResult, error) {
username, token, err := getCredentials()
if err != nil {
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeInvalidRequest,
}, err
setupLogging()
return p.ReadLXC(ctx, req)
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
return &resource.ReadResult{}, nil
}
data, err := authenticatedRequest(http.MethodGet, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc/"+req.NativeID+"/config", createAuthorizationString(username, token), nil)
if err != nil {
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeNetworkFailure,
}, err
}
var props StatusLXCConfigResponse
err = json.Unmarshal(data, &props)
if err != nil {
log.Println("Error unmarshaling json: ", data)
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeInvalidRequest,
}, err
}
lxcdata := props.Data
properties := LXCProperties{
VMID: req.NativeID,
Hostname: lxcdata.Hostname,
Description: lxcdata.Description,
Cores: lxcdata.Cores,
Memory: lxcdata.Memory,
OnBoot: lxcdata.OnBoot,
}
propsJSON, err := json.Marshal(properties)
if err != nil {
return &resource.ReadResult{
ErrorCode: resource.OperationErrorCodeInternalFailure,
}, err
}
return &resource.ReadResult{
ResourceType: req.ResourceType,
Properties: string(propsJSON),
}, nil
}
// Update modifies an existing resource.
func (p *Plugin) Update(ctx context.Context, req *resource.UpdateRequest) (*resource.UpdateResult, error) {
prior, err := parseLXCProperties(req.PriorProperties)
if err != nil {
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: err.Error(),
},
}, err
setupLogging()
return p.UpdateLXC(ctx, req)
}
desir, err := parseLXCProperties(req.DesiredProperties)
if err != nil {
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: err.Error(),
},
}, err
}
if prior == nil {
p.Create(ctx, &resource.CreateRequest{
ResourceType: req.ResourceType,
Properties: req.DesiredProperties,
TargetConfig: req.TargetConfig,
})
}
if prior.VMID != desir.VMID {
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInvalidRequest,
StatusMessage: "can't change vmid",
},
}, fmt.Errorf("can't change vmid")
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
log.Println(err.Error())
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
username, token, err := getCredentials()
if err != nil {
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeAccessDenied,
StatusMessage: err.Error(),
},
}, err
}
urlparams := url.Values{
"vmid": {desir.VMID},
"hostname": {desir.Hostname},
"cores": {strconv.Itoa(desir.Cores)},
"memory": {strconv.Itoa(desir.Memory)},
"description": {desir.Description},
}
if prior.OnBoot != desir.OnBoot {
urlparams.Add("onboot", strconv.Itoa(desir.OnBoot))
}
_, err = authenticatedRequest("PUT", config.URL+"/api2/json/nodes/"+config.NODE+"/lxc/"+desir.VMID+"/config", createAuthorizationString(username, token), urlparams)
if err != nil {
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
result, err := p.Read(ctx, &resource.ReadRequest{
NativeID: req.NativeID,
ResourceType: req.ResourceType,
TargetConfig: req.TargetConfig,
})
return &resource.UpdateResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationUpdate,
OperationStatus: resource.OperationStatusSuccess,
NativeID: req.NativeID,
ResourceProperties: json.RawMessage(result.Properties),
},
}, nil
}
// Delete removes a resource.
func (p *Plugin) Delete(ctx context.Context, req *resource.DeleteRequest) (*resource.DeleteResult, error) {
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
log.Println(err.Error())
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
setupLogging()
return p.DeleteLXC(ctx, req)
}
username, token, err := getCredentials()
if err != nil {
log.Println(err.Error())
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
_, err = authenticatedRequest(http.MethodDelete, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc/"+req.NativeID, createAuthorizationString(username, token), nil)
if err != nil {
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusFailure,
ErrorCode: resource.OperationErrorCodeInternalFailure,
StatusMessage: err.Error(),
},
}, err
}
return &resource.DeleteResult{
ProgressResult: &resource.ProgressResult{
Operation: resource.OperationCreate,
OperationStatus: resource.OperationStatusSuccess,
NativeID: req.NativeID,
},
}, nil
}
// 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
setupLogging()
return p.StatusLXC(ctx, req)
}
// 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) {
username, token, err := getCredentials()
if err != nil {
return &resource.ListResult{
NativeIDs: []string{},
}, err
}
config, err := parseTargetConfig(req.TargetConfig)
if err != nil {
return &resource.ListResult{
NativeIDs: []string{},
}, err
}
var props StatusGeneralResponse
data, err := authenticatedRequest(http.MethodGet, config.URL+"/api2/json/nodes/"+config.NODE+"/lxc", createAuthorizationString(username, token), nil)
if err != nil {
return &resource.ListResult{
NativeIDs: []string{},
}, err
}
json.Unmarshal(data, &props)
nativeIds := make([]string, 0, len(props.Data))
for _, value := range props.Data {
nativeIds = append(nativeIds, strconv.Itoa(value.VMID))
}
return &resource.ListResult{
NativeIDs: nativeIds,
NextPageToken: nil,
}, nil
setupLogging()
return p.ListLXC(ctx, req)
}

View File

@@ -3,10 +3,9 @@ package main
import (
"context"
"encoding/json"
"log"
"log/slog"
"net/http"
"strconv"
"strings"
"testing"
"time"
@@ -32,8 +31,10 @@ func TestCreate(t *testing.T) {
"hostname": "testlxc",
"description": "none",
"ostemplate": "local:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz",
"password": "password",
"cores": 1,
"memory": 512,
"networks": []string{"name=test,hwaddr=BC:24:11:FD:90:BF,bridge=internal,firewall=1"},
}
propertiesJSON, err := json.Marshal(properties)
@@ -103,6 +104,7 @@ func TestRead(t *testing.T) {
require.Equal(t, mem_num, props["memory"], "memory should match")
const onboot float64 = 0
require.Equal(t, onboot, props["onboot"], "memory should match")
require.NotEqual(t, nil, props["networks"], "network should be defined")
}
func TestUpdate(t *testing.T) {
@@ -116,6 +118,7 @@ func TestUpdate(t *testing.T) {
"ostemplate": "local:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz",
"cores": 1,
"memory": 512,
"networks": []string{"name=test,hwaddr=BC:24:11:FD:90:BF,bridge=internal,firewall=1"},
})
desiredProperties, _ := json.Marshal(map[string]any{
@@ -126,6 +129,7 @@ func TestUpdate(t *testing.T) {
"cores": 2,
"memory": 1024,
"onboot": 1,
"networks": []string{"name=test1,hwaddr=BC:24:11:FD:90:BF,bridge=internal,firewall=1"},
})
req := &resource.UpdateRequest{
@@ -170,7 +174,7 @@ func TestList(t *testing.T) {
})
require.NoError(t, err, "ListRequest should not return an error")
log.Printf("Received Ids: %s", strings.Join(result.NativeIDs, ", "))
slog.Info("Received Ids", "ids", result.NativeIDs)
require.Contains(t, result.NativeIDs, "200", "List should include created LXC")
}
@@ -195,8 +199,8 @@ func TestDelete(t *testing.T) {
NativeID: "200",
})
require.NoError(t, err, "Create should not return error")
require.NotNil(t, result.ProgressResult, "Create should return ProgressResult")
require.NoError(t, err, "Delete should not return error")
require.NotNil(t, result.ProgressResult, "Delete should return ProgressResult")
require.Eventually(t, func() bool {
var props StatusGeneralResponse
@@ -213,5 +217,5 @@ func TestDelete(t *testing.T) {
}
return true
}, 10*time.Second, time.Second, "Create operation should complete successfully")
}, 10*time.Second, time.Second, "Delete operation should complete successfully")
}

View File

@@ -11,6 +11,6 @@ package {
dependencies {
["formae"] {
uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.80.1"
uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.82.2"
}
}

View File

@@ -28,6 +28,9 @@ class LXC extends formae.Resource {
@formae.FieldHint {}
ostemplate: String?
@formae.FieldHint {}
password: String?
@formae.FieldHint {}
hostname: String
@@ -43,4 +46,10 @@ class LXC extends formae.Resource {
@formae.FieldHint {}
onboot: Int = 0
@formae.FieldHint {}
sshkeys: Listing<String>?
@formae.FieldHint {}
networks: Listing<String>?
}

2
testdata/PklProject vendored
View File

@@ -7,6 +7,6 @@ dependencies {
// Formae schema - fetched from public registry
["formae"] {
uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.80.1"
uri = "package://hub.platform.engineering/plugins/pkl/schema/pkl/formae/formae@0.82.2"
}
}

View File

@@ -31,6 +31,7 @@ forma {
hostname = "test-lxc"
description = "some other description"
ostemplate = "local:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz"
password = "test"
cores = 1
memory = 512
}

View File

@@ -34,5 +34,8 @@ forma {
cores = 2
memory = 1024
onboot = 1
networks = new Listing<String> {
"name=first,hwaddr=BC:24:11:FD:90:BF,bridge=internal"
}
}
}

View File

@@ -33,7 +33,12 @@ forma {
hostname = "test-lxc"
description = "no description provided"
ostemplate = "local:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz"
password = "abcd"
cores = 1
memory = 512
networks = new Listing<String> {
"name=first,hwaddr=BC:24:11:FD:90:BF,bridge=internal"
"name=second,hwaddr=BC:24:11:FD:90:BF,bridge=internal"
}
}
}

View File

@@ -12,9 +12,12 @@ type LXCProperties struct {
Hostname string `json:"hostname"`
Description string `json:"description,omitempty"`
OSTemplate string `json:"ostemplate,omitempty"`
Password string `json:"password,omitempty"`
Cores int `json:"cores"`
Memory int `json:"memory"`
OnBoot int `json:"onboot"`
SSHKeys []string `json:"sshkeys"`
Networks []string `json:"networks"`
}
type ReadRequest struct {
@@ -73,8 +76,22 @@ type StatusLXCConfig struct {
Description string `json:"description"`
Digest string `json:"digest"`
OnBoot int `json:"onboot"`
Net0 string `json:"net0,omitempty"`
Net1 string `json:"net1,omitempty"`
Net2 string `json:"net2,omitempty"`
Net3 string `json:"net3,omitempty"`
Net4 string `json:"net4,omitempty"`
Net5 string `json:"net5,omitempty"`
Net6 string `json:"net6,omitempty"`
Net7 string `json:"net7,omitempty"`
Net8 string `json:"net8,omitempty"`
Net9 string `json:"net9,omitempty"`
}
type StatusLXCConfigResponse struct {
Data StatusLXCConfig `json:"data"`
}
type ProxmoxDataResponse struct {
Data string `json:"data"`
}