Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions manifest
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
datadog
mongodb-atlas
openai
cloudamqp
44 changes: 44 additions & 0 deletions pkg/plugins/cloudamqp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# CloudAMQP OpenCost Plugin

The CloudAMQP plugin imports CloudAMQP Customer API invoice data into OpenCost Custom Costs.

It uses the Customer API invoice endpoint:

```text
GET /invoices/period?year=<year>&month=<month>
```

CloudAMQP invoices are monthly, so the plugin downloads each required monthly invoice once per OpenCost request and prorates invoice line totals across daily Custom Cost windows.

## Configuration

Create a plugin config JSON file:

```json
{
"api_key": "<cloudamqp-customer-api-key>",
"log_level": "info"
}
```

Optional fields:

```json
{
"api_base_url": "https://customer.cloudamqp.com/api",
"account_name": "cloudamqp",
"currency": "USD"
}
```

The Customer API uses HTTP Basic Auth with an empty username and the API key as the password.

## Cost Mapping

- invoice line `amount`, `total`, `subtotal`, `price`, or `cost` -> monthly line amount
- monthly line amount divided by days in the month -> daily billed and list cost
- invoice `currency` -> response currency
- invoice customer name -> account name and account ID
- invoice line description/name/plan/product -> resource fields

If invoice line amounts are not present, the plugin falls back to prorating the invoice `total`.
48 changes: 48 additions & 0 deletions pkg/plugins/cloudamqp/cloudamqpplugin/cloudamqpconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cloudamqpplugin

import (
"encoding/json"
"fmt"
"os"
"strings"
)

const DefaultAPIBaseURL = "https://customer.cloudamqp.com/api"

type CloudAMQPConfig struct {
APIKey string `json:"api_key"`
APIBaseURL string `json:"api_base_url"`
AccountName string `json:"account_name"`
Currency string `json:"currency"`
LogLevel string `json:"log_level"`
}

func GetCloudAMQPConfig(configFilePath string) (*CloudAMQPConfig, error) {
bytes, err := os.ReadFile(configFilePath)
if err != nil {
return nil, fmt.Errorf("error reading CloudAMQP config file at %s: %v", configFilePath, err)
}

var result CloudAMQPConfig
if err := json.Unmarshal(bytes, &result); err != nil {
return nil, fmt.Errorf("error unmarshaling CloudAMQP config: %v", err)
}

if strings.TrimSpace(result.APIKey) == "" {
return nil, fmt.Errorf("api_key is required")
}
if strings.TrimSpace(result.APIBaseURL) == "" {
result.APIBaseURL = DefaultAPIBaseURL
}
if strings.TrimSpace(result.AccountName) == "" {
result.AccountName = "cloudamqp"
}
if strings.TrimSpace(result.Currency) == "" {
result.Currency = "USD"
}
if strings.TrimSpace(result.LogLevel) == "" {
result.LogLevel = "info"
}

return &result, nil
}
47 changes: 47 additions & 0 deletions pkg/plugins/cloudamqp/cloudamqpplugin/cloudamqpconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cloudamqpplugin

import (
"os"
"path/filepath"
"testing"
)

func TestGetCloudAMQPConfigDefaults(t *testing.T) {
path := writeConfig(t, `{"api_key": "test-key"}`)

config, err := GetCloudAMQPConfig(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if config.APIBaseURL != DefaultAPIBaseURL {
t.Fatalf("unexpected api base url: %s", config.APIBaseURL)
}
if config.AccountName != "cloudamqp" {
t.Fatalf("unexpected account name: %s", config.AccountName)
}
if config.Currency != "USD" {
t.Fatalf("unexpected currency: %s", config.Currency)
}
if config.LogLevel != "info" {
t.Fatalf("unexpected log level: %s", config.LogLevel)
}
}

func TestGetCloudAMQPConfigValidatesRequiredFields(t *testing.T) {
path := writeConfig(t, `{}`)

_, err := GetCloudAMQPConfig(path)
if err == nil {
t.Fatalf("expected validation error")
}
}

func writeConfig(t *testing.T, contents string) string {
t.Helper()

path := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
t.Fatalf("write config: %v", err)
}
return path
}
Loading