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 }