package ionet import ( "encoding/json" "fmt" "strings" "time" "github.com/samber/lo" ) // ListContainers retrieves all containers for a specific deployment func (c *Client) ListContainers(deploymentID string) (*ContainerList, error) { if deploymentID == "" { return nil, fmt.Errorf("deployment ID cannot be empty") } endpoint := fmt.Sprintf("/deployment/%s/containers", deploymentID) resp, err := c.makeRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to list containers: %w", err) } var containerList ContainerList if err := decodeDataWithFlexibleTimes(resp.Body, &containerList); err != nil { return nil, fmt.Errorf("failed to parse containers list: %w", err) } return &containerList, nil } // GetContainerDetails retrieves detailed information about a specific container func (c *Client) GetContainerDetails(deploymentID, containerID string) (*Container, error) { if deploymentID == "" { return nil, fmt.Errorf("deployment ID cannot be empty") } if containerID == "" { return nil, fmt.Errorf("container ID cannot be empty") } endpoint := fmt.Sprintf("/deployment/%s/container/%s", deploymentID, containerID) resp, err := c.makeRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to get container details: %w", err) } // API response format not documented, assuming direct format var container Container if err := decodeWithFlexibleTimes(resp.Body, &container); err != nil { return nil, fmt.Errorf("failed to parse container details: %w", err) } return &container, nil } // GetContainerJobs retrieves containers jobs for a specific container (similar to containers endpoint) func (c *Client) GetContainerJobs(deploymentID, containerID string) (*ContainerList, error) { if deploymentID == "" { return nil, fmt.Errorf("deployment ID cannot be empty") } if containerID == "" { return nil, fmt.Errorf("container ID cannot be empty") } endpoint := fmt.Sprintf("/deployment/%s/containers-jobs/%s", deploymentID, containerID) resp, err := c.makeRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to get container jobs: %w", err) } var containerList ContainerList if err := decodeDataWithFlexibleTimes(resp.Body, &containerList); err != nil { return nil, fmt.Errorf("failed to parse container jobs: %w", err) } return &containerList, nil } // buildLogEndpoint constructs the request path for fetching logs func buildLogEndpoint(deploymentID, containerID string, opts *GetLogsOptions) (string, error) { if deploymentID == "" { return "", fmt.Errorf("deployment ID cannot be empty") } if containerID == "" { return "", fmt.Errorf("container ID cannot be empty") } params := make(map[string]interface{}) if opts != nil { if opts.Level != "" { params["level"] = opts.Level } if opts.Stream != "" { params["stream"] = opts.Stream } if opts.Limit > 0 { params["limit"] = opts.Limit } if opts.Cursor != "" { params["cursor"] = opts.Cursor } if opts.Follow { params["follow"] = true } if opts.StartTime != nil { params["start_time"] = opts.StartTime } if opts.EndTime != nil { params["end_time"] = opts.EndTime } } endpoint := fmt.Sprintf("/deployment/%s/log/%s", deploymentID, containerID) endpoint += buildQueryParams(params) return endpoint, nil } // GetContainerLogs retrieves logs for containers in a deployment and normalizes them func (c *Client) GetContainerLogs(deploymentID, containerID string, opts *GetLogsOptions) (*ContainerLogs, error) { raw, err := c.GetContainerLogsRaw(deploymentID, containerID, opts) if err != nil { return nil, err } logs := &ContainerLogs{ ContainerID: containerID, } if raw == "" { return logs, nil } normalized := strings.ReplaceAll(raw, "\r\n", "\n") lines := strings.Split(normalized, "\n") logs.Logs = lo.FilterMap(lines, func(line string, _ int) (LogEntry, bool) { if strings.TrimSpace(line) == "" { return LogEntry{}, false } return LogEntry{Message: line}, true }) return logs, nil } // GetContainerLogsRaw retrieves the raw text logs for a specific container func (c *Client) GetContainerLogsRaw(deploymentID, containerID string, opts *GetLogsOptions) (string, error) { endpoint, err := buildLogEndpoint(deploymentID, containerID, opts) if err != nil { return "", err } resp, err := c.makeRequest("GET", endpoint, nil) if err != nil { return "", fmt.Errorf("failed to get container logs: %w", err) } return string(resp.Body), nil } // StreamContainerLogs streams real-time logs for a specific container // This method uses a callback function to handle incoming log entries func (c *Client) StreamContainerLogs(deploymentID, containerID string, opts *GetLogsOptions, callback func(*LogEntry) error) error { if deploymentID == "" { return fmt.Errorf("deployment ID cannot be empty") } if containerID == "" { return fmt.Errorf("container ID cannot be empty") } if callback == nil { return fmt.Errorf("callback function cannot be nil") } // Set follow to true for streaming if opts == nil { opts = &GetLogsOptions{} } opts.Follow = true endpoint, err := buildLogEndpoint(deploymentID, containerID, opts) if err != nil { return err } // Note: This is a simplified implementation. In a real scenario, you might want to use // Server-Sent Events (SSE) or WebSocket for streaming logs for { resp, err := c.makeRequest("GET", endpoint, nil) if err != nil { return fmt.Errorf("failed to stream container logs: %w", err) } var logs ContainerLogs if err := decodeWithFlexibleTimes(resp.Body, &logs); err != nil { return fmt.Errorf("failed to parse container logs: %w", err) } // Call the callback for each log entry for _, logEntry := range logs.Logs { if err := callback(&logEntry); err != nil { return fmt.Errorf("callback error: %w", err) } } // If there are no more logs or we have a cursor, continue polling if !logs.HasMore && logs.NextCursor == "" { break } // Update cursor for next request if logs.NextCursor != "" { opts.Cursor = logs.NextCursor endpoint, err = buildLogEndpoint(deploymentID, containerID, opts) if err != nil { return err } } // Wait a bit before next poll to avoid overwhelming the API time.Sleep(2 * time.Second) } return nil } // RestartContainer restarts a specific container (if supported by the API) func (c *Client) RestartContainer(deploymentID, containerID string) error { if deploymentID == "" { return fmt.Errorf("deployment ID cannot be empty") } if containerID == "" { return fmt.Errorf("container ID cannot be empty") } endpoint := fmt.Sprintf("/deployment/%s/container/%s/restart", deploymentID, containerID) _, err := c.makeRequest("POST", endpoint, nil) if err != nil { return fmt.Errorf("failed to restart container: %w", err) } return nil } // StopContainer stops a specific container (if supported by the API) func (c *Client) StopContainer(deploymentID, containerID string) error { if deploymentID == "" { return fmt.Errorf("deployment ID cannot be empty") } if containerID == "" { return fmt.Errorf("container ID cannot be empty") } endpoint := fmt.Sprintf("/deployment/%s/container/%s/stop", deploymentID, containerID) _, err := c.makeRequest("POST", endpoint, nil) if err != nil { return fmt.Errorf("failed to stop container: %w", err) } return nil } // ExecuteInContainer executes a command in a specific container (if supported by the API) func (c *Client) ExecuteInContainer(deploymentID, containerID string, command []string) (string, error) { if deploymentID == "" { return "", fmt.Errorf("deployment ID cannot be empty") } if containerID == "" { return "", fmt.Errorf("container ID cannot be empty") } if len(command) == 0 { return "", fmt.Errorf("command cannot be empty") } reqBody := map[string]interface{}{ "command": command, } endpoint := fmt.Sprintf("/deployment/%s/container/%s/exec", deploymentID, containerID) resp, err := c.makeRequest("POST", endpoint, reqBody) if err != nil { return "", fmt.Errorf("failed to execute command in container: %w", err) } var result map[string]interface{} if err := json.Unmarshal(resp.Body, &result); err != nil { return "", fmt.Errorf("failed to parse execution result: %w", err) } if output, ok := result["output"].(string); ok { return output, nil } return string(resp.Body), nil }