container.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. package ionet
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "time"
  7. "github.com/samber/lo"
  8. )
  9. // ListContainers retrieves all containers for a specific deployment
  10. func (c *Client) ListContainers(deploymentID string) (*ContainerList, error) {
  11. if deploymentID == "" {
  12. return nil, fmt.Errorf("deployment ID cannot be empty")
  13. }
  14. endpoint := fmt.Sprintf("/deployment/%s/containers", deploymentID)
  15. resp, err := c.makeRequest("GET", endpoint, nil)
  16. if err != nil {
  17. return nil, fmt.Errorf("failed to list containers: %w", err)
  18. }
  19. var containerList ContainerList
  20. if err := decodeDataWithFlexibleTimes(resp.Body, &containerList); err != nil {
  21. return nil, fmt.Errorf("failed to parse containers list: %w", err)
  22. }
  23. return &containerList, nil
  24. }
  25. // GetContainerDetails retrieves detailed information about a specific container
  26. func (c *Client) GetContainerDetails(deploymentID, containerID string) (*Container, error) {
  27. if deploymentID == "" {
  28. return nil, fmt.Errorf("deployment ID cannot be empty")
  29. }
  30. if containerID == "" {
  31. return nil, fmt.Errorf("container ID cannot be empty")
  32. }
  33. endpoint := fmt.Sprintf("/deployment/%s/container/%s", deploymentID, containerID)
  34. resp, err := c.makeRequest("GET", endpoint, nil)
  35. if err != nil {
  36. return nil, fmt.Errorf("failed to get container details: %w", err)
  37. }
  38. // API response format not documented, assuming direct format
  39. var container Container
  40. if err := decodeWithFlexibleTimes(resp.Body, &container); err != nil {
  41. return nil, fmt.Errorf("failed to parse container details: %w", err)
  42. }
  43. return &container, nil
  44. }
  45. // GetContainerJobs retrieves containers jobs for a specific container (similar to containers endpoint)
  46. func (c *Client) GetContainerJobs(deploymentID, containerID string) (*ContainerList, error) {
  47. if deploymentID == "" {
  48. return nil, fmt.Errorf("deployment ID cannot be empty")
  49. }
  50. if containerID == "" {
  51. return nil, fmt.Errorf("container ID cannot be empty")
  52. }
  53. endpoint := fmt.Sprintf("/deployment/%s/containers-jobs/%s", deploymentID, containerID)
  54. resp, err := c.makeRequest("GET", endpoint, nil)
  55. if err != nil {
  56. return nil, fmt.Errorf("failed to get container jobs: %w", err)
  57. }
  58. var containerList ContainerList
  59. if err := decodeDataWithFlexibleTimes(resp.Body, &containerList); err != nil {
  60. return nil, fmt.Errorf("failed to parse container jobs: %w", err)
  61. }
  62. return &containerList, nil
  63. }
  64. // buildLogEndpoint constructs the request path for fetching logs
  65. func buildLogEndpoint(deploymentID, containerID string, opts *GetLogsOptions) (string, error) {
  66. if deploymentID == "" {
  67. return "", fmt.Errorf("deployment ID cannot be empty")
  68. }
  69. if containerID == "" {
  70. return "", fmt.Errorf("container ID cannot be empty")
  71. }
  72. params := make(map[string]interface{})
  73. if opts != nil {
  74. if opts.Level != "" {
  75. params["level"] = opts.Level
  76. }
  77. if opts.Stream != "" {
  78. params["stream"] = opts.Stream
  79. }
  80. if opts.Limit > 0 {
  81. params["limit"] = opts.Limit
  82. }
  83. if opts.Cursor != "" {
  84. params["cursor"] = opts.Cursor
  85. }
  86. if opts.Follow {
  87. params["follow"] = true
  88. }
  89. if opts.StartTime != nil {
  90. params["start_time"] = opts.StartTime
  91. }
  92. if opts.EndTime != nil {
  93. params["end_time"] = opts.EndTime
  94. }
  95. }
  96. endpoint := fmt.Sprintf("/deployment/%s/log/%s", deploymentID, containerID)
  97. endpoint += buildQueryParams(params)
  98. return endpoint, nil
  99. }
  100. // GetContainerLogs retrieves logs for containers in a deployment and normalizes them
  101. func (c *Client) GetContainerLogs(deploymentID, containerID string, opts *GetLogsOptions) (*ContainerLogs, error) {
  102. raw, err := c.GetContainerLogsRaw(deploymentID, containerID, opts)
  103. if err != nil {
  104. return nil, err
  105. }
  106. logs := &ContainerLogs{
  107. ContainerID: containerID,
  108. }
  109. if raw == "" {
  110. return logs, nil
  111. }
  112. normalized := strings.ReplaceAll(raw, "\r\n", "\n")
  113. lines := strings.Split(normalized, "\n")
  114. logs.Logs = lo.FilterMap(lines, func(line string, _ int) (LogEntry, bool) {
  115. if strings.TrimSpace(line) == "" {
  116. return LogEntry{}, false
  117. }
  118. return LogEntry{Message: line}, true
  119. })
  120. return logs, nil
  121. }
  122. // GetContainerLogsRaw retrieves the raw text logs for a specific container
  123. func (c *Client) GetContainerLogsRaw(deploymentID, containerID string, opts *GetLogsOptions) (string, error) {
  124. endpoint, err := buildLogEndpoint(deploymentID, containerID, opts)
  125. if err != nil {
  126. return "", err
  127. }
  128. resp, err := c.makeRequest("GET", endpoint, nil)
  129. if err != nil {
  130. return "", fmt.Errorf("failed to get container logs: %w", err)
  131. }
  132. return string(resp.Body), nil
  133. }
  134. // StreamContainerLogs streams real-time logs for a specific container
  135. // This method uses a callback function to handle incoming log entries
  136. func (c *Client) StreamContainerLogs(deploymentID, containerID string, opts *GetLogsOptions, callback func(*LogEntry) error) error {
  137. if deploymentID == "" {
  138. return fmt.Errorf("deployment ID cannot be empty")
  139. }
  140. if containerID == "" {
  141. return fmt.Errorf("container ID cannot be empty")
  142. }
  143. if callback == nil {
  144. return fmt.Errorf("callback function cannot be nil")
  145. }
  146. // Set follow to true for streaming
  147. if opts == nil {
  148. opts = &GetLogsOptions{}
  149. }
  150. opts.Follow = true
  151. endpoint, err := buildLogEndpoint(deploymentID, containerID, opts)
  152. if err != nil {
  153. return err
  154. }
  155. // Note: This is a simplified implementation. In a real scenario, you might want to use
  156. // Server-Sent Events (SSE) or WebSocket for streaming logs
  157. for {
  158. resp, err := c.makeRequest("GET", endpoint, nil)
  159. if err != nil {
  160. return fmt.Errorf("failed to stream container logs: %w", err)
  161. }
  162. var logs ContainerLogs
  163. if err := decodeWithFlexibleTimes(resp.Body, &logs); err != nil {
  164. return fmt.Errorf("failed to parse container logs: %w", err)
  165. }
  166. // Call the callback for each log entry
  167. for _, logEntry := range logs.Logs {
  168. if err := callback(&logEntry); err != nil {
  169. return fmt.Errorf("callback error: %w", err)
  170. }
  171. }
  172. // If there are no more logs or we have a cursor, continue polling
  173. if !logs.HasMore && logs.NextCursor == "" {
  174. break
  175. }
  176. // Update cursor for next request
  177. if logs.NextCursor != "" {
  178. opts.Cursor = logs.NextCursor
  179. endpoint, err = buildLogEndpoint(deploymentID, containerID, opts)
  180. if err != nil {
  181. return err
  182. }
  183. }
  184. // Wait a bit before next poll to avoid overwhelming the API
  185. time.Sleep(2 * time.Second)
  186. }
  187. return nil
  188. }
  189. // RestartContainer restarts a specific container (if supported by the API)
  190. func (c *Client) RestartContainer(deploymentID, containerID string) error {
  191. if deploymentID == "" {
  192. return fmt.Errorf("deployment ID cannot be empty")
  193. }
  194. if containerID == "" {
  195. return fmt.Errorf("container ID cannot be empty")
  196. }
  197. endpoint := fmt.Sprintf("/deployment/%s/container/%s/restart", deploymentID, containerID)
  198. _, err := c.makeRequest("POST", endpoint, nil)
  199. if err != nil {
  200. return fmt.Errorf("failed to restart container: %w", err)
  201. }
  202. return nil
  203. }
  204. // StopContainer stops a specific container (if supported by the API)
  205. func (c *Client) StopContainer(deploymentID, containerID string) error {
  206. if deploymentID == "" {
  207. return fmt.Errorf("deployment ID cannot be empty")
  208. }
  209. if containerID == "" {
  210. return fmt.Errorf("container ID cannot be empty")
  211. }
  212. endpoint := fmt.Sprintf("/deployment/%s/container/%s/stop", deploymentID, containerID)
  213. _, err := c.makeRequest("POST", endpoint, nil)
  214. if err != nil {
  215. return fmt.Errorf("failed to stop container: %w", err)
  216. }
  217. return nil
  218. }
  219. // ExecuteInContainer executes a command in a specific container (if supported by the API)
  220. func (c *Client) ExecuteInContainer(deploymentID, containerID string, command []string) (string, error) {
  221. if deploymentID == "" {
  222. return "", fmt.Errorf("deployment ID cannot be empty")
  223. }
  224. if containerID == "" {
  225. return "", fmt.Errorf("container ID cannot be empty")
  226. }
  227. if len(command) == 0 {
  228. return "", fmt.Errorf("command cannot be empty")
  229. }
  230. reqBody := map[string]interface{}{
  231. "command": command,
  232. }
  233. endpoint := fmt.Sprintf("/deployment/%s/container/%s/exec", deploymentID, containerID)
  234. resp, err := c.makeRequest("POST", endpoint, reqBody)
  235. if err != nil {
  236. return "", fmt.Errorf("failed to execute command in container: %w", err)
  237. }
  238. var result map[string]interface{}
  239. if err := json.Unmarshal(resp.Body, &result); err != nil {
  240. return "", fmt.Errorf("failed to parse execution result: %w", err)
  241. }
  242. if output, ok := result["output"].(string); ok {
  243. return output, nil
  244. }
  245. return string(resp.Body), nil
  246. }