container.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package docker
  2. import (
  3. "archive/tar"
  4. "bytes"
  5. "context"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/netip"
  10. "strings"
  11. "time"
  12. fw "git.buran.team/main/fairwind"
  13. "github.com/docker/docker/pkg/stdcopy"
  14. mobycontainer "github.com/moby/moby/api/types/container"
  15. mobynetwork "github.com/moby/moby/api/types/network"
  16. moby "github.com/moby/moby/client"
  17. )
  18. var ErrContainerExecReturnedNonZero = errors.New("container exec returned non-zero")
  19. var ErrContainerNotExited = errors.New("container not exited")
  20. type ExecutionResult struct {
  21. Code int
  22. Stdout []byte
  23. Stderr []byte
  24. Error error
  25. }
  26. type ContainerHook func(ctx context.Context) error
  27. type ContainerSettingsPermissions struct {
  28. Privileged bool
  29. Capabilities []string
  30. }
  31. type ContainerSettingsNetwork struct {
  32. IP string
  33. }
  34. type ContainerResources struct {
  35. CPU int64
  36. Memory int64
  37. // TODO: disk iops, network iops
  38. }
  39. type Variable struct {
  40. Key string
  41. Value string
  42. }
  43. type ContainerSettingsHost struct {
  44. Name string
  45. IP string
  46. }
  47. type ContainerSettings struct {
  48. Name string
  49. Command string
  50. Variables []Variable
  51. Hosts []ContainerSettingsHost
  52. Binds []string
  53. Permissions ContainerSettingsPermissions
  54. Network ContainerSettingsNetwork
  55. Resources ContainerResources
  56. }
  57. type Container struct {
  58. ctx context.Context
  59. log *fw.Log
  60. client *Docker
  61. network *Network
  62. image *Image
  63. settings ContainerSettings
  64. }
  65. func NewContainer(ctx context.Context, log *fw.Log, client *Docker, network *Network, image *Image, settings ContainerSettings) *Container {
  66. return &Container{
  67. ctx: ctx,
  68. log: log,
  69. client: client,
  70. network: network,
  71. image: image,
  72. settings: settings,
  73. }
  74. }
  75. func (this *Container) Image() *Image {
  76. return this.image
  77. }
  78. func (this *Container) Start() error {
  79. err := this.image.Pull()
  80. if err != nil {
  81. return fmt.Errorf("can't start container: %w", err)
  82. }
  83. binds := []string{}
  84. for _, bind := range this.settings.Binds {
  85. binds = append(binds, fmt.Sprintf("%s:%s", bind, bind))
  86. }
  87. command := []string{}
  88. if len(this.settings.Command) > 0 {
  89. command = strings.Split(this.settings.Command, " ")
  90. } else {
  91. command = nil
  92. }
  93. env := []string{}
  94. if this.settings.Variables != nil {
  95. for _, environment := range this.settings.Variables {
  96. env = append(
  97. env,
  98. fmt.Sprintf("%s=%s", environment.Key, environment.Value),
  99. )
  100. }
  101. }
  102. hosts := []string{}
  103. if this.settings.Hosts != nil {
  104. for _, host := range this.settings.Hosts {
  105. hosts = append(
  106. hosts,
  107. fmt.Sprintf("%s:%s", host.Name, host.IP),
  108. )
  109. }
  110. }
  111. resources := mobycontainer.Resources{}
  112. if this.settings.Resources.CPU > 0 {
  113. resources.NanoCPUs = this.settings.Resources.CPU
  114. }
  115. if this.settings.Resources.Memory > 0 {
  116. resources.Memory = this.settings.Resources.Memory
  117. }
  118. // Create container
  119. _, err = this.client.Docker.ContainerCreate(
  120. this.ctx,
  121. moby.ContainerCreateOptions{
  122. Name: this.settings.Name,
  123. Config: &mobycontainer.Config{
  124. Image: this.image.Name(),
  125. Cmd: command,
  126. Env: env,
  127. },
  128. HostConfig: &mobycontainer.HostConfig{
  129. Binds: binds,
  130. RestartPolicy: mobycontainer.RestartPolicy{
  131. Name: mobycontainer.RestartPolicyDisabled,
  132. },
  133. Privileged: this.settings.Permissions.Privileged,
  134. CapAdd: this.settings.Permissions.Capabilities,
  135. NetworkMode: "default",
  136. Resources: resources,
  137. ExtraHosts: hosts,
  138. },
  139. NetworkingConfig: &mobynetwork.NetworkingConfig{
  140. EndpointsConfig: map[string]*mobynetwork.EndpointSettings{
  141. this.network.Name(): {
  142. IPAMConfig: &mobynetwork.EndpointIPAMConfig{
  143. IPv4Address: netip.MustParseAddr(this.settings.Network.IP),
  144. },
  145. },
  146. },
  147. },
  148. },
  149. )
  150. if err != nil {
  151. return fmt.Errorf("can't start container: %w", err)
  152. }
  153. // Start
  154. _, err = this.client.Docker.ContainerStart(
  155. this.ctx,
  156. this.settings.Name,
  157. moby.ContainerStartOptions{},
  158. )
  159. if err != nil {
  160. return fmt.Errorf("can't start container: %w", err)
  161. }
  162. return nil
  163. }
  164. func (this *Container) Stop() error {
  165. _, err := this.client.Docker.ContainerRemove(
  166. this.ctx,
  167. this.settings.Name,
  168. moby.ContainerRemoveOptions{
  169. Force: true,
  170. },
  171. )
  172. if err != nil {
  173. return err
  174. }
  175. return nil
  176. }
  177. func (this *Container) Status(shouldExit bool) ExecutionResult {
  178. // Get status
  179. result0, err := this.client.Docker.ContainerInspect(
  180. this.ctx,
  181. this.settings.Name,
  182. moby.ContainerInspectOptions{},
  183. )
  184. if err != nil {
  185. return ExecutionResult{
  186. Error: err,
  187. }
  188. }
  189. if shouldExit {
  190. if result0.Container.State.Status != "exited" {
  191. return ExecutionResult{
  192. Error: ErrContainerNotExited,
  193. }
  194. }
  195. }
  196. // Get logs
  197. result1, err := this.client.Docker.ContainerLogs(
  198. this.ctx,
  199. this.settings.Name,
  200. moby.ContainerLogsOptions{
  201. ShowStdout: true,
  202. ShowStderr: true,
  203. },
  204. )
  205. if err != nil {
  206. return ExecutionResult{
  207. Error: err,
  208. }
  209. }
  210. var stdout bytes.Buffer
  211. var stderr bytes.Buffer
  212. _, err = stdcopy.StdCopy(&stdout, &stderr, result1)
  213. if err != nil {
  214. return ExecutionResult{
  215. Error: err,
  216. }
  217. }
  218. // Done
  219. return ExecutionResult{
  220. Code: result0.Container.State.ExitCode,
  221. Stdout: stdout.Bytes(),
  222. Stderr: stderr.Bytes(),
  223. }
  224. }
  225. func (this *Container) Execute(timeout int, command string) ExecutionResult {
  226. // Context
  227. ctx, cancel := context.WithTimeout(
  228. this.ctx,
  229. time.Duration(timeout)*time.Millisecond,
  230. )
  231. defer cancel()
  232. // Create
  233. resultCreate, err := this.client.Docker.ExecCreate(
  234. ctx,
  235. this.settings.Name,
  236. moby.ExecCreateOptions{
  237. AttachStdin: false,
  238. AttachStdout: true,
  239. AttachStderr: true,
  240. Cmd: strings.Split(command, " "),
  241. },
  242. )
  243. if err != nil {
  244. return ExecutionResult{
  245. Error: fmt.Errorf("can't exec command in container: %w", err),
  246. }
  247. }
  248. resultAttach, err := this.client.Docker.ExecAttach(
  249. ctx,
  250. resultCreate.ID,
  251. moby.ExecAttachOptions{},
  252. )
  253. if err != nil {
  254. return ExecutionResult{
  255. Error: fmt.Errorf("can't exec command in container: %w", err),
  256. }
  257. }
  258. defer resultAttach.Close()
  259. // Start
  260. _, err = this.client.Docker.ExecStart(
  261. ctx,
  262. resultCreate.ID,
  263. moby.ExecStartOptions{},
  264. )
  265. if err != nil {
  266. return ExecutionResult{
  267. Error: fmt.Errorf("can't exec command in container: %w", err),
  268. }
  269. }
  270. // Wait
  271. exitCode := 0
  272. for {
  273. resultInspect, err := this.client.Docker.ExecInspect(
  274. ctx,
  275. resultCreate.ID,
  276. moby.ExecInspectOptions{},
  277. )
  278. if err != nil {
  279. return ExecutionResult{
  280. Error: fmt.Errorf("can't exec command in container: %w", err),
  281. }
  282. }
  283. if resultInspect.Running {
  284. time.Sleep(10 * time.Millisecond)
  285. continue
  286. }
  287. exitCode = resultInspect.ExitCode
  288. break
  289. }
  290. var stdout bytes.Buffer
  291. var stderr bytes.Buffer
  292. _, err = stdcopy.StdCopy(&stdout, &stderr, resultAttach.Reader)
  293. if err != nil {
  294. return ExecutionResult{
  295. Error: fmt.Errorf("can't exec command in container: %w", err),
  296. }
  297. }
  298. // Done
  299. return ExecutionResult{
  300. Code: exitCode,
  301. Stdout: stdout.Bytes(),
  302. Stderr: stderr.Bytes(),
  303. }
  304. }
  305. func (this *Container) Read(timeout int, path string) ([]byte, error) {
  306. // Context
  307. ctx, cancel := context.WithTimeout(
  308. this.ctx,
  309. time.Duration(timeout)*time.Millisecond,
  310. )
  311. defer cancel()
  312. // Read
  313. result, err := this.client.Docker.CopyFromContainer(
  314. ctx,
  315. this.settings.Name,
  316. moby.CopyFromContainerOptions{
  317. SourcePath: path,
  318. },
  319. )
  320. if err != nil {
  321. return nil, fmt.Errorf("can't copy from container: %w", err)
  322. }
  323. defer result.Content.Close()
  324. // Tar
  325. buffer := []byte{}
  326. reader := tar.NewReader(result.Content)
  327. _, err = reader.Next()
  328. if err == io.EOF {
  329. return nil, fmt.Errorf("can't copy from container: %w", err)
  330. }
  331. if err != nil {
  332. return nil, fmt.Errorf("can't copy from container: %w", err)
  333. }
  334. buffer, err = io.ReadAll(reader)
  335. if err != nil {
  336. return nil, fmt.Errorf("can't copy from container: %w", err)
  337. }
  338. // Done
  339. return buffer, nil
  340. }