Spaces:
Runtime error
Runtime error
| 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 | |
| } | |