Phase3
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -10,6 +13,7 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
@@ -189,3 +193,134 @@ func mapSoftwareFamily(family string) string {
|
||||
return "VANILLA"
|
||||
}
|
||||
}
|
||||
|
||||
// StreamLogs streams container logs and invokes callback for each line.
|
||||
func (c *Client) StreamLogs(ctx context.Context, containerID string, tail int, follow bool, callback func(stream, line string)) error {
|
||||
opts := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: follow,
|
||||
Timestamps: false,
|
||||
}
|
||||
if tail > 0 {
|
||||
opts.Tail = strconv.Itoa(tail)
|
||||
} else {
|
||||
opts.Tail = "all"
|
||||
}
|
||||
|
||||
reader, err := c.cli.ContainerLogs(ctx, containerID, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("container logs: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
return readMultiplexedLogLines(ctx, reader, callback)
|
||||
}
|
||||
|
||||
// ExecCommand runs a command inside a container and returns stdout/stderr.
|
||||
func (c *Client) ExecCommand(ctx context.Context, containerID string, cmd []string) (stdout, stderr string, err error) {
|
||||
execResp, err := c.cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
|
||||
Cmd: cmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("exec create: %w", err)
|
||||
}
|
||||
|
||||
attachResp, err := c.cli.ContainerExecAttach(ctx, execResp.ID, container.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("exec attach: %w", err)
|
||||
}
|
||||
defer attachResp.Close()
|
||||
|
||||
var outBuf, errBuf bytes.Buffer
|
||||
if _, err := stdcopy.StdCopy(&outBuf, &errBuf, attachResp.Reader); err != nil {
|
||||
return outBuf.String(), errBuf.String(), fmt.Errorf("exec copy: %w", err)
|
||||
}
|
||||
|
||||
inspect, err := c.cli.ContainerExecInspect(ctx, execResp.ID)
|
||||
if err != nil {
|
||||
return outBuf.String(), errBuf.String(), fmt.Errorf("exec inspect: %w", err)
|
||||
}
|
||||
if inspect.ExitCode != 0 {
|
||||
msg := strings.TrimSpace(errBuf.String())
|
||||
if msg == "" {
|
||||
msg = strings.TrimSpace(outBuf.String())
|
||||
}
|
||||
if msg == "" {
|
||||
msg = fmt.Sprintf("exit code %d", inspect.ExitCode)
|
||||
}
|
||||
return outBuf.String(), errBuf.String(), fmt.Errorf("exec failed: %s", msg)
|
||||
}
|
||||
|
||||
return outBuf.String(), errBuf.String(), nil
|
||||
}
|
||||
|
||||
// ExecRcon runs a Minecraft console command via rcon-cli inside the container.
|
||||
func (c *Client) ExecRcon(ctx context.Context, containerID, command string) (stdout, stderr string, err error) {
|
||||
return c.ExecCommand(ctx, containerID, []string{"rcon-cli", command})
|
||||
}
|
||||
|
||||
func readMultiplexedLogLines(ctx context.Context, r io.Reader, callback func(stream, line string)) error {
|
||||
header := make([]byte, 8)
|
||||
var partial []byte
|
||||
var partialStream string
|
||||
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := io.ReadFull(r, header)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream := "stdout"
|
||||
if header[0] == 2 {
|
||||
stream = "stderr"
|
||||
}
|
||||
|
||||
size := binary.BigEndian.Uint32(header[4:8])
|
||||
payload := make([]byte, size)
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if partialStream != "" && partialStream != stream {
|
||||
if len(partial) > 0 {
|
||||
callback(partialStream, string(partial))
|
||||
partial = nil
|
||||
}
|
||||
}
|
||||
partialStream = stream
|
||||
partial = append(partial, payload...)
|
||||
|
||||
for {
|
||||
idx := bytes.IndexByte(partial, '\n')
|
||||
if idx < 0 {
|
||||
break
|
||||
}
|
||||
line := string(partial[:idx])
|
||||
partial = partial[idx+1:]
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
if line != "" {
|
||||
callback(stream, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(partial) > 0 {
|
||||
stream := partialStream
|
||||
if stream == "" {
|
||||
stream = "stdout"
|
||||
}
|
||||
callback(stream, string(partial))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user