Phase3
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 9s
CI / Go — edge-gateway build (push) Successful in 13s

This commit is contained in:
smueller
2026-06-26 12:17:26 +02:00
parent c4077d4673
commit 9b061c3ee7
92 changed files with 4128 additions and 146 deletions

View File

@@ -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
}