-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathopencost_integration.go
More file actions
301 lines (249 loc) · 8.84 KB
/
opencost_integration.go
File metadata and controls
301 lines (249 loc) · 8.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
sdk "github.com/monadic/devops-sdk"
)
// OpenCostClient provides integration with OpenCost API
type OpenCostClient struct {
baseURL string
client *http.Client
}
// NewOpenCostClient creates a new OpenCost client
func NewOpenCostClient(baseURL string) *OpenCostClient {
if baseURL == "" {
// Default to in-cluster service
baseURL = "http://opencost.opencost.svc.cluster.local:9003"
}
return &OpenCostClient{
baseURL: baseURL,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// OpenCostAllocation represents cost data from OpenCost
type OpenCostAllocation struct {
Name string `json:"name"`
Start string `json:"start"`
End string `json:"end"`
CPUCost float64 `json:"cpuCost"`
GPUCost float64 `json:"gpuCost"`
RAMCost float64 `json:"ramCost"`
PVCost float64 `json:"pvCost"`
TotalCost float64 `json:"totalCost"`
Properties map[string]interface{} `json:"properties"`
}
// OpenCostResponse represents the API response
type OpenCostResponse struct {
Code int `json:"code"`
Data []map[string]OpenCostAllocation `json:"data"`
Message string `json:"message,omitempty"`
}
// GetAllocationData fetches real cost data from OpenCost
func (oc *OpenCostClient) GetAllocationData(window string, aggregate string) (*OpenCostResponse, error) {
// Construct API URL
// Example: /allocation/compute?window=1d&aggregate=namespace
url := fmt.Sprintf("%s/allocation/compute?window=%s&aggregate=%s",
oc.baseURL, window, aggregate)
fmt.Printf("[OpenCost] Fetching allocation data from: %s\n", url)
resp, err := oc.client.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch OpenCost data: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OpenCost API error (status %d): %s",
resp.StatusCode, string(body))
}
var result OpenCostResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse OpenCost response: %v", err)
}
fmt.Printf("[OpenCost] Retrieved %d allocation entries\n", len(result.Data))
return &result, nil
}
// ConvertToResourceUsage converts OpenCost data to our ResourceUsage format
func (oc *OpenCostClient) ConvertToResourceUsage(allocations *OpenCostResponse) []ResourceUsage {
var resources []ResourceUsage
for _, dayData := range allocations.Data {
for name, allocation := range dayData {
// Extract namespace and deployment from name
namespace := "default"
if props, ok := allocation.Properties["namespace"].(string); ok {
namespace = props
}
// Convert OpenCost allocation to ResourceUsage
resource := ResourceUsage{
Name: name,
Type: "deployment",
Namespace: namespace,
MonthlyCost: allocation.TotalCost * 30, // Convert daily to monthly
CPUCost: allocation.CPUCost * 30,
MemoryCost: allocation.RAMCost * 30,
StorageCost: allocation.PVCost * 30,
GPUCost: allocation.GPUCost * 30,
// Extract utilization if available
CPUUtilization: extractUtilization(allocation.Properties, "cpuUtilization"),
MemUtilization: extractUtilization(allocation.Properties, "ramUtilization"),
}
resources = append(resources, resource)
}
}
return resources
}
// extractUtilization safely extracts utilization from properties
func extractUtilization(props map[string]interface{}, key string) float64 {
if val, ok := props[key].(float64); ok {
return val * 100 // Convert to percentage
}
return 0.0
}
// IntegrateWithOpenCost enhances cost optimizer with real OpenCost data
func (c *CostOptimizer) IntegrateWithOpenCost() error {
fmt.Println("\n🔌 Integrating with OpenCost for real cost data...")
// First check ConfigHub for OpenCost configuration
opencostConfig, err := c.getOpenCostConfig()
if err != nil {
fmt.Printf("[OpenCost] No configuration found in ConfigHub: %v\n", err)
}
// Check if OpenCost is available
opencostURL := os.Getenv("OPENCOST_URL")
if opencostURL == "" && opencostConfig != nil {
// Use URL from ConfigHub config if available
if url, ok := opencostConfig["url"].(string); ok {
opencostURL = url
}
}
if opencostURL == "" {
// Try to detect OpenCost service in cluster
opencostURL = "http://opencost.opencost.svc.cluster.local:9003"
}
oc := NewOpenCostClient(opencostURL)
// Test OpenCost connectivity
testURL := fmt.Sprintf("%s/healthz", opencostURL)
resp, err := oc.client.Get(testURL)
if err != nil {
fmt.Printf("[OpenCost] Not available at %s, using estimated costs\n", opencostURL)
return nil // Fallback to estimates
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("[OpenCost] Health check failed (status %d), using estimates\n",
resp.StatusCode)
return nil
}
fmt.Printf("[OpenCost] ✓ Connected to OpenCost at %s\n", opencostURL)
// Fetch real cost data from OpenCost
allocations, err := oc.GetAllocationData("1d", "namespace")
if err != nil {
fmt.Printf("[OpenCost] Error fetching data: %v\n", err)
return err
}
// Convert OpenCost data to our format
opencostResources := oc.ConvertToResourceUsage(allocations)
if len(opencostResources) > 0 {
fmt.Printf("[OpenCost] ✓ Using real cost data for %d resources\n",
len(opencostResources))
// Merge with existing resource data
mergedResources := c.mergeResourceData(c.resources, opencostResources)
c.resources = mergedResources
// Store OpenCost data in ConfigHub
c.storeOpenCostData(allocations)
}
return nil
}
// mergeResourceData merges Kubernetes metrics with OpenCost cost data
func (c *CostOptimizer) mergeResourceData(k8sResources, opencostResources []ResourceUsage) []ResourceUsage {
// Create map for quick lookup
opencostMap := make(map[string]ResourceUsage)
for _, res := range opencostResources {
key := fmt.Sprintf("%s/%s", res.Namespace, res.Name)
opencostMap[key] = res
}
// Merge data
var merged []ResourceUsage
for _, k8sRes := range k8sResources {
key := fmt.Sprintf("%s/%s", k8sRes.Namespace, k8sRes.Name)
if ocRes, found := opencostMap[key]; found {
// Use real costs from OpenCost
k8sRes.MonthlyCost = ocRes.MonthlyCost
k8sRes.CPUCost = ocRes.CPUCost
k8sRes.MemoryCost = ocRes.MemoryCost
k8sRes.StorageCost = ocRes.StorageCost
k8sRes.GPUCost = ocRes.GPUCost
// Use utilization from OpenCost if available
if ocRes.CPUUtilization > 0 {
k8sRes.CPUUtilization = ocRes.CPUUtilization
}
if ocRes.MemUtilization > 0 {
k8sRes.MemUtilization = ocRes.MemUtilization
}
fmt.Printf("[OpenCost] Updated %s with real costs: $%.2f/month\n",
key, k8sRes.MonthlyCost)
}
merged = append(merged, k8sRes)
}
return merged
}
// storeOpenCostData stores OpenCost data in ConfigHub for audit trail
func (c *CostOptimizer) storeOpenCostData(data *OpenCostResponse) error {
fmt.Println("[ConfigHub] Storing OpenCost data for audit trail...")
// Create unit with OpenCost data
unitName := fmt.Sprintf("opencost-data-%d", time.Now().Unix())
unitData := map[string]interface{}{
"source": "opencost",
"timestamp": time.Now().Format(time.RFC3339),
"data": data,
}
unitJSON, err := json.Marshal(unitData)
if err != nil {
return fmt.Errorf("failed to marshal OpenCost data: %v", err)
}
_, err = c.app.Cub.CreateUnit(c.spaceID, sdk.CreateUnitRequest{
Slug: unitName,
DisplayName: fmt.Sprintf("OpenCost Data - %s", time.Now().Format("2006-01-02")),
Data: string(unitJSON),
Labels: map[string]string{
"type": "opencost-data",
"source": "opencost",
},
})
if err != nil {
fmt.Printf("[ConfigHub] Warning: Could not store OpenCost data: %v\n", err)
return nil // Non-critical error
}
fmt.Printf("[ConfigHub] ✓ Stored OpenCost data as unit: %s\n", unitName)
return nil
}
// getOpenCostConfig retrieves OpenCost configuration from ConfigHub
func (c *CostOptimizer) getOpenCostConfig() (map[string]interface{}, error) {
// Try to get OpenCost config unit from ConfigHub
units, err := c.app.Cub.ListUnits(sdk.ListUnitsParams{
SpaceID: c.spaceID,
})
if err != nil {
return nil, fmt.Errorf("failed to list units: %v", err)
}
// Look for OpenCost config unit
for _, unit := range units {
if unit.Slug == "opencost-config" {
var config map[string]interface{}
if err := json.Unmarshal([]byte(unit.Data), &config); err != nil {
return nil, fmt.Errorf("failed to parse config: %v", err)
}
fmt.Printf("[ConfigHub] ✓ Found OpenCost config: enabled=%v, url=%v\n",
config["enabled"], config["url"])
return config, nil
}
}
return nil, fmt.Errorf("opencost-config unit not found")
}