File size: 11,620 Bytes
daa8246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
package ionet

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

	"github.com/samber/lo"
)

// DeployContainer deploys a new container with the specified configuration
func (c *Client) DeployContainer(req *DeploymentRequest) (*DeploymentResponse, error) {
	if req == nil {
		return nil, fmt.Errorf("deployment request cannot be nil")
	}

	// Validate required fields
	if req.ResourcePrivateName == "" {
		return nil, fmt.Errorf("resource_private_name is required")
	}
	if len(req.LocationIDs) == 0 {
		return nil, fmt.Errorf("location_ids is required")
	}
	if req.HardwareID <= 0 {
		return nil, fmt.Errorf("hardware_id is required")
	}
	if req.RegistryConfig.ImageURL == "" {
		return nil, fmt.Errorf("registry_config.image_url is required")
	}
	if req.GPUsPerContainer < 1 {
		return nil, fmt.Errorf("gpus_per_container must be at least 1")
	}
	if req.DurationHours < 1 {
		return nil, fmt.Errorf("duration_hours must be at least 1")
	}
	if req.ContainerConfig.ReplicaCount < 1 {
		return nil, fmt.Errorf("container_config.replica_count must be at least 1")
	}

	resp, err := c.makeRequest("POST", "/deploy", req)
	if err != nil {
		return nil, fmt.Errorf("failed to deploy container: %w", err)
	}

	// API returns direct format:
	// {"status": "string", "deployment_id": "..."}
	var deployResp DeploymentResponse
	if err := json.Unmarshal(resp.Body, &deployResp); err != nil {
		return nil, fmt.Errorf("failed to parse deployment response: %w", err)
	}

	return &deployResp, nil
}

// ListDeployments retrieves a list of deployments with optional filtering
func (c *Client) ListDeployments(opts *ListDeploymentsOptions) (*DeploymentList, error) {
	params := make(map[string]interface{})

	if opts != nil {
		params["status"] = opts.Status
		params["location_id"] = opts.LocationID
		params["page"] = opts.Page
		params["page_size"] = opts.PageSize
		params["sort_by"] = opts.SortBy
		params["sort_order"] = opts.SortOrder
	}

	endpoint := "/deployments" + buildQueryParams(params)

	resp, err := c.makeRequest("GET", endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to list deployments: %w", err)
	}

	var deploymentList DeploymentList
	if err := decodeData(resp.Body, &deploymentList); err != nil {
		return nil, fmt.Errorf("failed to parse deployments list: %w", err)
	}

	deploymentList.Deployments = lo.Map(deploymentList.Deployments, func(deployment Deployment, _ int) Deployment {
		deployment.GPUCount = deployment.HardwareQuantity
		deployment.Replicas = deployment.HardwareQuantity // Assuming 1:1 mapping for now
		return deployment
	})

	return &deploymentList, nil
}

// GetDeployment retrieves detailed information about a specific deployment
func (c *Client) GetDeployment(deploymentID string) (*DeploymentDetail, error) {
	if deploymentID == "" {
		return nil, fmt.Errorf("deployment ID cannot be empty")
	}

	endpoint := fmt.Sprintf("/deployment/%s", deploymentID)

	resp, err := c.makeRequest("GET", endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get deployment details: %w", err)
	}

	var deploymentDetail DeploymentDetail
	if err := decodeDataWithFlexibleTimes(resp.Body, &deploymentDetail); err != nil {
		return nil, fmt.Errorf("failed to parse deployment details: %w", err)
	}

	return &deploymentDetail, nil
}

// UpdateDeployment updates the configuration of an existing deployment
func (c *Client) UpdateDeployment(deploymentID string, req *UpdateDeploymentRequest) (*UpdateDeploymentResponse, error) {
	if deploymentID == "" {
		return nil, fmt.Errorf("deployment ID cannot be empty")
	}
	if req == nil {
		return nil, fmt.Errorf("update request cannot be nil")
	}

	endpoint := fmt.Sprintf("/deployment/%s", deploymentID)

	resp, err := c.makeRequest("PATCH", endpoint, req)
	if err != nil {
		return nil, fmt.Errorf("failed to update deployment: %w", err)
	}

	// API returns direct format:
	// {"status": "string", "deployment_id": "..."}
	var updateResp UpdateDeploymentResponse
	if err := json.Unmarshal(resp.Body, &updateResp); err != nil {
		return nil, fmt.Errorf("failed to parse update deployment response: %w", err)
	}

	return &updateResp, nil
}

// ExtendDeployment extends the duration of an existing deployment
func (c *Client) ExtendDeployment(deploymentID string, req *ExtendDurationRequest) (*DeploymentDetail, error) {
	if deploymentID == "" {
		return nil, fmt.Errorf("deployment ID cannot be empty")
	}
	if req == nil {
		return nil, fmt.Errorf("extend request cannot be nil")
	}
	if req.DurationHours < 1 {
		return nil, fmt.Errorf("duration_hours must be at least 1")
	}

	endpoint := fmt.Sprintf("/deployment/%s/extend", deploymentID)

	resp, err := c.makeRequest("POST", endpoint, req)
	if err != nil {
		return nil, fmt.Errorf("failed to extend deployment: %w", err)
	}

	var deploymentDetail DeploymentDetail
	if err := decodeDataWithFlexibleTimes(resp.Body, &deploymentDetail); err != nil {
		return nil, fmt.Errorf("failed to parse extended deployment details: %w", err)
	}

	return &deploymentDetail, nil
}

// DeleteDeployment deletes an active deployment
func (c *Client) DeleteDeployment(deploymentID string) (*UpdateDeploymentResponse, error) {
	if deploymentID == "" {
		return nil, fmt.Errorf("deployment ID cannot be empty")
	}

	endpoint := fmt.Sprintf("/deployment/%s", deploymentID)

	resp, err := c.makeRequest("DELETE", endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to delete deployment: %w", err)
	}

	// API returns direct format:
	// {"status": "string", "deployment_id": "..."}
	var deleteResp UpdateDeploymentResponse
	if err := json.Unmarshal(resp.Body, &deleteResp); err != nil {
		return nil, fmt.Errorf("failed to parse delete deployment response: %w", err)
	}

	return &deleteResp, nil
}

// GetPriceEstimation calculates the estimated cost for a deployment
func (c *Client) GetPriceEstimation(req *PriceEstimationRequest) (*PriceEstimationResponse, error) {
	if req == nil {
		return nil, fmt.Errorf("price estimation request cannot be nil")
	}

	// Validate required fields
	if len(req.LocationIDs) == 0 {
		return nil, fmt.Errorf("location_ids is required")
	}
	if req.HardwareID == 0 {
		return nil, fmt.Errorf("hardware_id is required")
	}
	if req.ReplicaCount < 1 {
		return nil, fmt.Errorf("replica_count must be at least 1")
	}

	currency := strings.TrimSpace(req.Currency)
	if currency == "" {
		currency = "usdc"
	}

	durationType := strings.TrimSpace(req.DurationType)
	if durationType == "" {
		durationType = "hour"
	}
	durationType = strings.ToLower(durationType)

	apiDurationType := ""

	durationQty := req.DurationQty
	if durationQty < 1 {
		durationQty = req.DurationHours
	}
	if durationQty < 1 {
		return nil, fmt.Errorf("duration_qty must be at least 1")
	}

	hardwareQty := req.HardwareQty
	if hardwareQty < 1 {
		hardwareQty = req.GPUsPerContainer
	}
	if hardwareQty < 1 {
		return nil, fmt.Errorf("hardware_qty must be at least 1")
	}

	durationHoursForRate := req.DurationHours
	if durationHoursForRate < 1 {
		durationHoursForRate = durationQty
	}
	switch durationType {
	case "hour", "hours", "hourly":
		durationHoursForRate = durationQty
		apiDurationType = "hourly"
	case "day", "days", "daily":
		durationHoursForRate = durationQty * 24
		apiDurationType = "daily"
	case "week", "weeks", "weekly":
		durationHoursForRate = durationQty * 24 * 7
		apiDurationType = "weekly"
	case "month", "months", "monthly":
		durationHoursForRate = durationQty * 24 * 30
		apiDurationType = "monthly"
	}
	if durationHoursForRate < 1 {
		durationHoursForRate = 1
	}
	if apiDurationType == "" {
		apiDurationType = "hourly"
	}

	params := map[string]interface{}{
		"location_ids":       req.LocationIDs,
		"hardware_id":        req.HardwareID,
		"hardware_qty":       hardwareQty,
		"gpus_per_container": req.GPUsPerContainer,
		"duration_type":      apiDurationType,
		"duration_qty":       durationQty,
		"duration_hours":     req.DurationHours,
		"replica_count":      req.ReplicaCount,
		"currency":           currency,
	}

	endpoint := "/price" + buildQueryParams(params)

	resp, err := c.makeRequest("GET", endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get price estimation: %w", err)
	}

	// Parse according to the actual API response format from docs:
	// {
	//   "data": {
	//     "replica_count": 0,
	//     "gpus_per_container": 0,
	//     "available_replica_count": [0],
	//     "discount": 0,
	//     "ionet_fee": 0,
	//     "ionet_fee_percent": 0,
	//     "currency_conversion_fee": 0,
	//     "currency_conversion_fee_percent": 0,
	//     "total_cost_usdc": 0
	//   }
	// }
	var pricingData struct {
		ReplicaCount                 int     `json:"replica_count"`
		GPUsPerContainer             int     `json:"gpus_per_container"`
		AvailableReplicaCount        []int   `json:"available_replica_count"`
		Discount                     float64 `json:"discount"`
		IonetFee                     float64 `json:"ionet_fee"`
		IonetFeePercent              float64 `json:"ionet_fee_percent"`
		CurrencyConversionFee        float64 `json:"currency_conversion_fee"`
		CurrencyConversionFeePercent float64 `json:"currency_conversion_fee_percent"`
		TotalCostUSDC                float64 `json:"total_cost_usdc"`
	}

	if err := decodeData(resp.Body, &pricingData); err != nil {
		return nil, fmt.Errorf("failed to parse price estimation response: %w", err)
	}

	// Convert to our internal format
	durationHoursFloat := float64(durationHoursForRate)
	if durationHoursFloat <= 0 {
		durationHoursFloat = 1
	}

	priceResp := &PriceEstimationResponse{
		EstimatedCost:   pricingData.TotalCostUSDC,
		Currency:        strings.ToUpper(currency),
		EstimationValid: true,
		PriceBreakdown: PriceBreakdown{
			ComputeCost: pricingData.TotalCostUSDC - pricingData.IonetFee - pricingData.CurrencyConversionFee,
			TotalCost:   pricingData.TotalCostUSDC,
			HourlyRate:  pricingData.TotalCostUSDC / durationHoursFloat,
		},
	}

	return priceResp, nil
}

// CheckClusterNameAvailability checks if a cluster name is available
func (c *Client) CheckClusterNameAvailability(clusterName string) (bool, error) {
	if clusterName == "" {
		return false, fmt.Errorf("cluster name cannot be empty")
	}

	params := map[string]interface{}{
		"cluster_name": clusterName,
	}

	endpoint := "/clusters/check_cluster_name_availability" + buildQueryParams(params)

	resp, err := c.makeRequest("GET", endpoint, nil)
	if err != nil {
		return false, fmt.Errorf("failed to check cluster name availability: %w", err)
	}

	var availabilityResp bool
	if err := json.Unmarshal(resp.Body, &availabilityResp); err != nil {
		return false, fmt.Errorf("failed to parse cluster name availability response: %w", err)
	}

	return availabilityResp, nil
}

// UpdateClusterName updates the name of an existing cluster/deployment
func (c *Client) UpdateClusterName(clusterID string, req *UpdateClusterNameRequest) (*UpdateClusterNameResponse, error) {
	if clusterID == "" {
		return nil, fmt.Errorf("cluster ID cannot be empty")
	}
	if req == nil {
		return nil, fmt.Errorf("update cluster name request cannot be nil")
	}
	if req.Name == "" {
		return nil, fmt.Errorf("cluster name cannot be empty")
	}

	endpoint := fmt.Sprintf("/clusters/%s/update-name", clusterID)

	resp, err := c.makeRequest("PUT", endpoint, req)
	if err != nil {
		return nil, fmt.Errorf("failed to update cluster name: %w", err)
	}

	// Parse the response directly without data wrapper based on API docs
	var updateResp UpdateClusterNameResponse
	if err := json.Unmarshal(resp.Body, &updateResp); err != nil {
		return nil, fmt.Errorf("failed to parse update cluster name response: %w", err)
	}

	return &updateResp, nil
}