Skip to content

Commit 8409ee8

Browse files
authored
feat(utilization-metering-consumer): Add configurable tenant-to-account mapping (#640)
1 parent 880d2c0 commit 8409ee8

5 files changed

Lines changed: 170 additions & 6 deletions

File tree

services/utilization-metering-consumer/app/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ type Config struct {
3232
// Tenant Zero configuration
3333
TenantZeroID string // Required: Tenant ID for Meridian's platform billing tenant
3434

35+
// Tenant-to-Account Mapping
36+
TenantAccountMapping string // Optional: JSON mapping of tenant UUIDs to account UUIDs
37+
3538
// HTTP server configuration
3639
HTTPPort string // HTTP port for health checks and metrics (default: "8080")
3740
}
@@ -55,6 +58,7 @@ func LoadConfig() (*Config, error) {
5558
AuditTopics: defaultAuditTopics, // Use default topics
5659
PositionKeepingEndpoint: env.GetEnvOrDefault("POSITION_KEEPING_ENDPOINT", ""),
5760
TenantZeroID: env.GetEnvOrDefault("TENANT_ZERO_ID", ""),
61+
TenantAccountMapping: env.GetEnvOrDefault("TENANT_ACCOUNT_MAPPING", ""),
5862
HTTPPort: env.GetEnvOrDefault("HTTP_PORT", "8080"),
5963
}
6064

services/utilization-metering-consumer/cmd/main.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/meridianhub/meridian/services/utilization-metering-consumer/adapters/grpc"
2020
"github.com/meridianhub/meridian/services/utilization-metering-consumer/adapters/messaging"
2121
"github.com/meridianhub/meridian/services/utilization-metering-consumer/app"
22+
"github.com/meridianhub/meridian/services/utilization-metering-consumer/domain"
2223
"github.com/meridianhub/meridian/shared/platform/bootstrap"
2324
"github.com/meridianhub/meridian/shared/platform/defaults"
2425
"github.com/meridianhub/meridian/shared/platform/env"
@@ -182,12 +183,21 @@ func run(logger *slog.Logger) error {
182183
return fmt.Errorf("invalid TENANT_ZERO_ID: %w", err)
183184
}
184185

185-
// For now, we map all tenants to tenant-zero's billing account
186-
// In a real implementation, this would be loaded from configuration or a database
187-
// TODO: Load tenant-to-account mapping from configuration or database
188-
tenantAccountMap := make(map[uuid.UUID]uuid.UUID)
189-
// Map tenant-zero to itself for self-billing
190-
tenantAccountMap[tenantZeroID] = tenantZeroID
186+
// Load tenant-to-account mapping from configuration
187+
tenantAccountMap, err := domain.ParseTenantAccountMapping(config.TenantAccountMapping)
188+
if err != nil {
189+
return fmt.Errorf("failed to load tenant account mapping: %w", err)
190+
}
191+
192+
// Ensure tenant-zero maps to itself if not explicitly configured
193+
if _, exists := tenantAccountMap[tenantZeroID]; !exists {
194+
logger.Info("tenant-zero not found in TENANT_ACCOUNT_MAPPING, mapping to itself",
195+
"tenant_zero_id", tenantZeroID)
196+
tenantAccountMap[tenantZeroID] = tenantZeroID
197+
}
198+
199+
logger.Info("tenant account mapping loaded",
200+
"mapping_count", len(tenantAccountMap))
191201

192202
// Initialize transformer with tenant account mapping
193203
transformer := auditdomain.NewAuditEventTransformer(tenantAccountMap)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Package domain provides core domain logic for the utilization-metering-consumer service.
2+
package domain
3+
4+
import (
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/google/uuid"
9+
)
10+
11+
// ParseTenantAccountMapping parses a JSON string mapping tenant UUIDs to account UUIDs.
12+
// Expected format: {"tenant_id_1": "account_id_1", "tenant_id_2": "account_id_2"}
13+
// Returns an empty map if the input string is empty.
14+
func ParseTenantAccountMapping(jsonStr string) (map[uuid.UUID]uuid.UUID, error) {
15+
if jsonStr == "" {
16+
return make(map[uuid.UUID]uuid.UUID), nil
17+
}
18+
19+
var rawMap map[string]string
20+
if err := json.Unmarshal([]byte(jsonStr), &rawMap); err != nil {
21+
return nil, fmt.Errorf("failed to parse tenant account mapping JSON: %w", err)
22+
}
23+
24+
result := make(map[uuid.UUID]uuid.UUID)
25+
for tenantIDStr, accountIDStr := range rawMap {
26+
tenantID, err := uuid.Parse(tenantIDStr)
27+
if err != nil {
28+
return nil, fmt.Errorf("invalid tenant_id '%s': %w", tenantIDStr, err)
29+
}
30+
accountID, err := uuid.Parse(accountIDStr)
31+
if err != nil {
32+
return nil, fmt.Errorf("invalid account_id '%s' for tenant %s: %w", accountIDStr, tenantIDStr, err)
33+
}
34+
result[tenantID] = accountID
35+
}
36+
37+
return result, nil
38+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package domain
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/uuid"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestParseTenantAccountMapping_ValidJSON(t *testing.T) {
12+
tenantID1 := uuid.MustParse("00000000-0000-0000-0000-000000000001")
13+
accountID1 := uuid.MustParse("11111111-1111-1111-1111-111111111111")
14+
tenantID2 := uuid.MustParse("00000000-0000-0000-0000-000000000002")
15+
accountID2 := uuid.MustParse("22222222-2222-2222-2222-222222222222")
16+
17+
json := `{
18+
"00000000-0000-0000-0000-000000000001": "11111111-1111-1111-1111-111111111111",
19+
"00000000-0000-0000-0000-000000000002": "22222222-2222-2222-2222-222222222222"
20+
}`
21+
22+
result, err := ParseTenantAccountMapping(json)
23+
24+
require.NoError(t, err)
25+
assert.Len(t, result, 2)
26+
assert.Equal(t, accountID1, result[tenantID1])
27+
assert.Equal(t, accountID2, result[tenantID2])
28+
}
29+
30+
func TestParseTenantAccountMapping_SingleMapping(t *testing.T) {
31+
tenantID := uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
32+
accountID := uuid.MustParse("12345678-1234-1234-1234-123456789012")
33+
34+
json := `{"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee": "12345678-1234-1234-1234-123456789012"}`
35+
36+
result, err := ParseTenantAccountMapping(json)
37+
38+
require.NoError(t, err)
39+
assert.Len(t, result, 1)
40+
assert.Equal(t, accountID, result[tenantID])
41+
}
42+
43+
func TestParseTenantAccountMapping_EmptyString(t *testing.T) {
44+
result, err := ParseTenantAccountMapping("")
45+
46+
require.NoError(t, err)
47+
assert.NotNil(t, result)
48+
assert.Empty(t, result)
49+
}
50+
51+
func TestParseTenantAccountMapping_EmptyJSON(t *testing.T) {
52+
result, err := ParseTenantAccountMapping("{}")
53+
54+
require.NoError(t, err)
55+
assert.NotNil(t, result)
56+
assert.Empty(t, result)
57+
}
58+
59+
func TestParseTenantAccountMapping_InvalidJSON(t *testing.T) {
60+
_, err := ParseTenantAccountMapping(`{invalid json}`)
61+
62+
assert.Error(t, err)
63+
assert.Contains(t, err.Error(), "failed to parse tenant account mapping JSON")
64+
}
65+
66+
func TestParseTenantAccountMapping_InvalidTenantUUID(t *testing.T) {
67+
json := `{"not-a-uuid": "11111111-1111-1111-1111-111111111111"}`
68+
69+
_, err := ParseTenantAccountMapping(json)
70+
71+
assert.Error(t, err)
72+
assert.Contains(t, err.Error(), "invalid tenant_id")
73+
}
74+
75+
func TestParseTenantAccountMapping_InvalidAccountUUID(t *testing.T) {
76+
json := `{"00000000-0000-0000-0000-000000000001": "not-a-uuid"}`
77+
78+
_, err := ParseTenantAccountMapping(json)
79+
80+
assert.Error(t, err)
81+
assert.Contains(t, err.Error(), "invalid account_id")
82+
}
83+
84+
func TestParseTenantAccountMapping_WhitespacePreserved(t *testing.T) {
85+
// Ensure whitespace in JSON is handled correctly
86+
tenantID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
87+
accountID := uuid.MustParse("11111111-1111-1111-1111-111111111111")
88+
89+
json := `
90+
{
91+
"00000000-0000-0000-0000-000000000001" : "11111111-1111-1111-1111-111111111111"
92+
}
93+
`
94+
95+
result, err := ParseTenantAccountMapping(json)
96+
97+
require.NoError(t, err)
98+
assert.Len(t, result, 1)
99+
assert.Equal(t, accountID, result[tenantID])
100+
}

services/utilization-metering-consumer/k8s/configmap.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ data:
4141
# WARNING: This should be set via Secret in production, not ConfigMap
4242
TENANT_ZERO_ID: "00000000-0000-0000-0000-000000000000"
4343

44+
# Tenant-to-Account Mapping (optional)
45+
# JSON mapping of tenant UUIDs to account UUIDs for multi-tenant billing.
46+
# If not specified or empty, tenant-zero will automatically map to itself.
47+
# Format: {"tenant_id": "account_id", ...}
48+
# Example:
49+
# TENANT_ACCOUNT_MAPPING: |
50+
# {
51+
# "00000000-0000-0000-0000-000000000001": "11111111-1111-1111-1111-111111111111",
52+
# "22222222-2222-2222-2222-222222222222": "33333333-3333-3333-3333-333333333333"
53+
# }
54+
TENANT_ACCOUNT_MAPPING: ""
55+
4456
# OpenTelemetry configuration
4557
OTEL_SERVICE_NAME: "utilization-metering-consumer"
4658
OTEL_SAMPLING_RATE: "0.1"

0 commit comments

Comments
 (0)