diff --git a/Makefile b/Makefile index 46e9462..15c92bc 100644 --- a/Makefile +++ b/Makefile @@ -119,3 +119,7 @@ conformance-test-discovery: install echo "Post-test cleanup..."; \ ./scripts/ci/clean-environment.sh || true; \ exit $$TEST_EXIT + +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 diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..07e6f88 --- /dev/null +++ b/helper.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +func parseTargetConfig(data json.RawMessage) (*TargetConfig, error) { + var cfg TargetConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("invalid target config: %w", err) + } + if cfg.URL == "" { + return nil, fmt.Errorf("target config missing 'url'") + } + if cfg.NODE == "" { + return nil, fmt.Errorf("target config missing 'node'") + } + return &cfg, nil +} + +func getCredentials() (username, token string, err error) { + username = os.Getenv("PROXMOX_USERNAME") + token = os.Getenv("PROXMOX_TOKEN") + if username == "" { + return "", "", fmt.Errorf("PROXMOX_USERNAME not set") + } + if token == "" { + return "", "", fmt.Errorf("PROXMOX_TOKEN not set") + } + 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") + } + if props.OSTemplate == "" { + return nil, fmt.Errorf("ostemplate missing") + } + return &props, nil +} diff --git a/proxmox.go b/proxmox.go index d81d0df..158e562 100644 --- a/proxmox.go +++ b/proxmox.go @@ -9,72 +9,15 @@ import ( "context" "encoding/json" "errors" - "fmt" "io" "log" "net/http" - "os" - "strconv" "github.com/platform-engineering-labs/formae/pkg/plugin" "github.com/platform-engineering-labs/formae/pkg/plugin/resource" ) // https://pve.proxmox.com/pve-docs/api-viewer/ -type TargetConfig struct { - URL string `json:"url"` - NODE string `json:"node"` -} - -type LXCProperties struct { - VMID int `json:"vmid"` - NAME string `json:"name"` - DESCRIPTION string `json:"description"` - OSTEMPLATE string `json:"ostemplate"` -} - -func parseTargetConfig(data json.RawMessage) (*TargetConfig, error) { - var cfg TargetConfig - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("invalid target config: %w", err) - } - if cfg.URL == "" { - return nil, fmt.Errorf("target config missing 'url'") - } - if cfg.NODE == "" { - return nil, fmt.Errorf("target config missing 'node'") - } - return &cfg, nil -} - -func getCredentials() (username, token string, err error) { - username = os.Getenv("PROXMOX_USERNAME") - token = os.Getenv("PROXMOX_TOKEN") - if username == "" { - return "", "", fmt.Errorf("PROXMOX_USERNAME not set") - } - if token == "" { - return "", "", fmt.Errorf("PROXMOX_TOKEN not set") - } - 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 == 0 { - return nil, fmt.Errorf("vmid missing") - } - if props.NAME == "" { - return nil, fmt.Errorf("name missing") - } - if props.OSTEMPLATE == "" { - return nil, fmt.Errorf("ostemplate missing") - } - return &props, nil -} // ErrNotImplemented is returned by stub methods that need implementation. var ErrNotImplemented = errors.New("not implemented") @@ -138,8 +81,6 @@ func (p *Plugin) LabelConfig() plugin.LabelConfig { // Create provisions a new resource. func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) { - log.Println(req.Properties) - props, err := parseLXCProperties(req.Properties) if err != nil { log.Println(err.Error()) @@ -150,13 +91,12 @@ func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*reso ErrorCode: resource.OperationErrorCodeInvalidRequest, StatusMessage: err.Error(), }, - }, nil + }, err } - log.Println("LXC Properties: ", props.VMID, props.NAME, props.OSTEMPLATE, props.DESCRIPTION) - config, err := parseTargetConfig(req.TargetConfig) if err != nil { + log.Println(err.Error()) return &resource.CreateResult{ ProgressResult: &resource.ProgressResult{ Operation: resource.OperationCreate, @@ -164,11 +104,12 @@ func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*reso ErrorCode: resource.OperationErrorCodeInternalFailure, StatusMessage: err.Error(), }, - }, nil + }, err } username, token, err := getCredentials() if err != nil { + log.Println(err.Error()) return &resource.CreateResult{ ProgressResult: &resource.ProgressResult{ Operation: resource.OperationCreate, @@ -176,15 +117,12 @@ func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*reso ErrorCode: resource.OperationErrorCodeInternalFailure, StatusMessage: err.Error(), }, - }, nil + }, err } client := &http.Client{} - // data := map[string]any{"node": config.NODE, "ostemplate": props.OSTEMPLATE, "id": props.VMID, "hostname": props.NAME, "description": props.DESCRIPTION} - // jsonBody, err := json.Marshal(data) - - arguments := "vmid=" + strconv.Itoa(props.VMID) + "&ostemplate=" + props.OSTEMPLATE + "&hostname=" + props.NAME + arguments := "vmid=" + props.VMID + "&ostemplate=" + props.OSTemplate + "&hostname=" + props.Hostname request, err := http.NewRequest("POST", config.URL+"/api2/json/nodes/"+config.NODE+"/lxc", bytes.NewBuffer([]byte(arguments))) request.Header.Set("Authorization", "PVEAPIToken="+username+"="+token) @@ -199,10 +137,20 @@ func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*reso ErrorCode: resource.OperationErrorCodeInternalFailure, StatusMessage: err.Error(), }, - }, nil + }, err } body, err := io.ReadAll(resp.Body) + if err != nil { + return &resource.CreateResult{ + ProgressResult: &resource.ProgressResult{ + Operation: resource.OperationCreate, + OperationStatus: resource.OperationStatusFailure, + ErrorCode: resource.OperationErrorCodeInternalFailure, + StatusMessage: err.Error(), + }, + }, err + } log.Println("Response StatusCode: ", resp.Status) log.Println("Response Body: ", string(body)) @@ -211,24 +159,66 @@ func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*reso ProgressResult: &resource.ProgressResult{ Operation: resource.OperationCreate, OperationStatus: resource.OperationStatusSuccess, - NativeID: strconv.Itoa(props.VMID), + NativeID: props.VMID, }, }, nil } -// 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 + username, token, err := getCredentials() + if err != nil { + return &resource.ReadResult{ + ErrorCode: resource.OperationErrorCodeInvalidRequest, + }, err + } + + config, err := parseTargetConfig(req.TargetConfig) + if err != nil { + return &resource.ReadResult{}, nil + } + + client := &http.Client{} + + request, err := http.NewRequest("GET", config.URL+"/api2/json/nodes/"+config.NODE+"/lxc/"+req.NativeID+"/config", nil) + if err != nil { + return &resource.ReadResult{ + ErrorCode: resource.OperationErrorCodeNetworkFailure, + }, err + } + request.Header.Set("Authorization", "PVEAPIToken="+username+"="+token) + + resp, err := client.Do(request) + + data, err := io.ReadAll(resp.Body) + + var props StatusLXCConfigResponse + + err = json.Unmarshal(data, &props) + if err != nil { + return &resource.ReadResult{ + ErrorCode: resource.OperationErrorCodeInvalidRequest, + }, err + } + + lxcdata := props.Data + + properties := LXCProperties{ + VMID: req.NativeID, + Hostname: lxcdata.Hostname, + Description: lxcdata.Description, + } + + propsJSON, err := json.Marshal(properties) + if err != nil { + return &resource.ReadResult{ + ErrorCode: resource.OperationErrorCodeInternalFailure, + }, err + } return &resource.ReadResult{ ResourceType: req.ResourceType, - ErrorCode: resource.OperationErrorCodeInternalFailure, - }, ErrNotImplemented + Properties: string(propsJSON), + }, nil } // Update modifies an existing resource. diff --git a/proxmox_test.go b/proxmox_test.go index 8d64400..20c8cb3 100644 --- a/proxmox_test.go +++ b/proxmox_test.go @@ -5,7 +5,7 @@ import ( "encoding/json" "io" "net/http" - "os" + "strconv" "testing" "time" @@ -17,31 +17,18 @@ func testTargetConfig() json.RawMessage { return json.RawMessage(`{"url": "https://proxmox.mid:8006", "node": "proxmox"}`) } -type LXC struct { - VMID int `json:"vmid"` - NAME string `json:"name"` -} - -type RESP_DATA struct { - DATA []LXC `json:"data"` -} - func TestCreate(t *testing.T) { - username := os.Getenv("PROXMOX_USERNAME") - token := os.Getenv("PROXMOX_TOKEN") - if username == "" { - t.Skip("PROXMOX_USERNAME not set") - } - if token == "" { - t.Skip("PROXMOX_TOKEN not set") + username, token, err := getCredentials() + if err != nil { + t.Skip(err) } plugin := &Plugin{} ctx := context.Background() properties := map[string]any{ - "vmid": 200, - "name": "testlxc", + "vmid": "200", + "hostname": "testlxc", "description": "none", "ostemplate": "local:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz", } @@ -67,7 +54,7 @@ func TestCreate(t *testing.T) { require.Eventually(t, func() bool { client := &http.Client{} - var props RESP_DATA + var props StatusGeneralResponse request, err := http.NewRequest("GET", config.URL+"/api2/json/nodes/"+config.NODE+"/lxc", nil) if err != nil { @@ -79,13 +66,13 @@ func TestCreate(t *testing.T) { resp, err := client.Do(request) data, err := io.ReadAll(resp.Body) + json.Unmarshal(data, &props) - for i := 0; i < len(props.DATA); i++ { - lxccontainer := props.DATA[i] - t.Logf("Found container: %s", lxccontainer.NAME) + for i := 0; i < len(props.Data); i++ { + lxccontainer := props.Data[i] if lxccontainer.VMID == 200 { - t.Logf("Created Successfully: %s", lxccontainer.NAME) + t.Logf("Created Successfully: %s", lxccontainer.Name) return true } } @@ -93,3 +80,30 @@ func TestCreate(t *testing.T) { return false }, 10*time.Second, time.Second, "Create operation should complete successfully") } + +func TestRead(t *testing.T) { + ctx := context.Background() + + plugin := &Plugin{} + + req := &resource.ReadRequest{ + NativeID: strconv.Itoa(120), + ResourceType: "PROXMOX::Service::LXC", + TargetConfig: testTargetConfig(), + } + + result, err := plugin.Read(ctx, req) + + require.NoError(t, err, "Read should not return error") + require.Empty(t, result.ErrorCode, "Read should not return error code") + require.NotEmpty(t, result.Properties, "Read should return properties") + + var props map[string]any + + err = json.Unmarshal([]byte(result.Properties), &props) + require.NoError(t, err, "json should be parsable") + + require.NoError(t, err, "Properties should be valid JSON") + require.Equal(t, "ntfy", props["hostname"], "hostname should match") + require.Equal(t, strconv.Itoa(120), props["vmid"], "vmid should match") +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..e8017ea --- /dev/null +++ b/types.go @@ -0,0 +1,53 @@ +package main + +import "encoding/json" + +type TargetConfig struct { + URL string `json:"url"` + NODE string `json:"node"` +} + +type LXCProperties struct { + VMID string `json:"vmid"` + Hostname string `json:"hostname"` + Description string `json:"description"` + OSTemplate string `json:"ostemplate"` +} + +type ReadRequest struct { + NativeID string + ResourceType string + TargetConfig json.RawMessage +} + +type StatusLXCGeneral struct { + Status string `json:"status"` + NetIn int `json:"netin"` + NetOut int `json:"netout"` + MaxDisk int `json:"maxdisk"` + Cpus int `json:"cpus"` + Name string `json:"name"` + Memory int `json:"maxmem"` + VMID int `json:"vmid"` + Type string `json:"type"` + Swap int `json:"maxswap"` +} + +type StatusGeneralResponse struct { + Data []StatusLXCGeneral `json:"data"` +} + +type StatusLXCConfig struct { + Arch string `json:"arch"` + OSType string `json:"ostype"` + RootFS string `json:"rootfs"` + Hostname string `json:"hostname"` + Memory int `json:"memory"` + Swap int `json:"swap"` + Description string `json:"description"` + Digest string `json:"digest"` +} + +type StatusLXCConfigResponse struct { + Data StatusLXCConfig `json:"data"` +}