initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-11-02 01:39:56 +01:00
commit aff6c82553
Se han modificado 34 ficheros con 4744 adiciones y 0 borrados

264
internal/docker/client.go Archivo normal
Ver fichero

@@ -0,0 +1,264 @@
package docker
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/yourusername/buque/internal/models"
)
// Client wraps Docker client operations
type Client struct {
cli *client.Client
}
// NewClient creates a new Docker client
func NewClient() (*Client, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %w", err)
}
return &Client{cli: cli}, nil
}
// Close closes the Docker client connection
func (c *Client) Close() error {
return c.cli.Close()
}
// ListContainers lists all containers with optional filters
func (c *Client) ListContainers(ctx context.Context, all bool) ([]types.Container, error) {
options := container.ListOptions{
All: all,
}
containers, err := c.cli.ContainerList(ctx, options)
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}
return containers, nil
}
// GetContainerStats retrieves statistics for a container
func (c *Client) GetContainerStats(ctx context.Context, containerID string) (*models.ContainerStats, error) {
stats, err := c.cli.ContainerStats(ctx, containerID, false)
if err != nil {
return nil, fmt.Errorf("failed to get container stats: %w", err)
}
defer stats.Body.Close()
var v *types.StatsJSON
if err := json.NewDecoder(stats.Body).Decode(&v); err != nil {
return nil, fmt.Errorf("failed to decode stats: %w", err)
}
// Calculate CPU percentage
cpuDelta := float64(v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage)
systemDelta := float64(v.CPUStats.SystemUsage - v.PreCPUStats.SystemUsage)
cpuPercent := 0.0
if systemDelta > 0.0 && cpuDelta > 0.0 {
cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0
}
// Calculate memory percentage
memUsage := v.MemoryStats.Usage
memLimit := v.MemoryStats.Limit
memPercent := 0.0
if memLimit > 0 {
memPercent = (float64(memUsage) / float64(memLimit)) * 100.0
}
// Network stats
var networkRx, networkTx uint64
for _, network := range v.Networks {
networkRx += network.RxBytes
networkTx += network.TxBytes
}
// Block I/O stats
var blockRead, blockWrite uint64
for _, bioEntry := range v.BlkioStats.IoServiceBytesRecursive {
switch bioEntry.Op {
case "Read":
blockRead += bioEntry.Value
case "Write":
blockWrite += bioEntry.Value
}
}
containerStats := &models.ContainerStats{
ID: containerID,
Name: v.Name,
CPUPercentage: cpuPercent,
MemoryUsage: memUsage,
MemoryLimit: memLimit,
MemoryPercent: memPercent,
NetworkRx: networkRx,
NetworkTx: networkTx,
BlockRead: blockRead,
BlockWrite: blockWrite,
PIDs: v.PIDStats.Current,
}
return containerStats, nil
}
// InspectContainer returns detailed container information
func (c *Client) InspectContainer(ctx context.Context, containerID string) (types.ContainerJSON, error) {
container, err := c.cli.ContainerInspect(ctx, containerID)
if err != nil {
return types.ContainerJSON{}, fmt.Errorf("failed to inspect container: %w", err)
}
return container, nil
}
// PullImage pulls a Docker image
func (c *Client) PullImage(ctx context.Context, imageName string) error {
out, err := c.cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
return fmt.Errorf("failed to pull image %s: %w", imageName, err)
}
defer out.Close()
// Read output to completion
_, err = io.Copy(io.Discard, out)
return err
}
// ListImages lists Docker images
func (c *Client) ListImages(ctx context.Context) ([]types.ImageSummary, error) {
images, err := c.cli.ImageList(ctx, types.ImageListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list images: %w", err)
}
return images, nil
}
// RemoveImage removes a Docker image
func (c *Client) RemoveImage(ctx context.Context, imageID string, force bool) error {
_, err := c.cli.ImageRemove(ctx, imageID, types.ImageRemoveOptions{
Force: force,
})
if err != nil {
return fmt.Errorf("failed to remove image %s: %w", imageID, err)
}
return nil
}
// PruneImages removes unused images
func (c *Client) PruneImages(ctx context.Context) error {
_, err := c.cli.ImagesPrune(ctx, filters.Args{})
if err != nil {
return fmt.Errorf("failed to prune images: %w", err)
}
return nil
}
// CreateNetwork creates a Docker network
func (c *Client) CreateNetwork(ctx context.Context, name string) error {
_, err := c.cli.NetworkCreate(ctx, name, types.NetworkCreate{
Driver: "bridge",
})
if err != nil {
return fmt.Errorf("failed to create network %s: %w", name, err)
}
return nil
}
// ListNetworks lists Docker networks
func (c *Client) ListNetworks(ctx context.Context) ([]types.NetworkResource, error) {
networks, err := c.cli.NetworkList(ctx, types.NetworkListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list networks: %w", err)
}
return networks, nil
}
// NetworkExists checks if a network exists
func (c *Client) NetworkExists(ctx context.Context, name string) (bool, error) {
networks, err := c.ListNetworks(ctx)
if err != nil {
return false, err
}
for _, network := range networks {
if network.Name == name {
return true, nil
}
}
return false, nil
}
// GetContainersByLabel returns containers filtered by label
func (c *Client) GetContainersByLabel(ctx context.Context, label, value string) ([]types.Container, error) {
filterArgs := filters.NewArgs()
filterArgs.Add("label", fmt.Sprintf("%s=%s", label, value))
options := container.ListOptions{
All: true,
Filters: filterArgs,
}
containers, err := c.cli.ContainerList(ctx, options)
if err != nil {
return nil, fmt.Errorf("failed to list containers by label: %w", err)
}
return containers, nil
}
// StopContainer stops a container
func (c *Client) StopContainer(ctx context.Context, containerID string, timeout time.Duration) error {
timeoutSeconds := int(timeout.Seconds())
if err := c.cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeoutSeconds}); err != nil {
return fmt.Errorf("failed to stop container %s: %w", containerID, err)
}
return nil
}
// RestartContainer restarts a container
func (c *Client) RestartContainer(ctx context.Context, containerID string, timeout time.Duration) error {
timeoutSeconds := int(timeout.Seconds())
if err := c.cli.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeoutSeconds}); err != nil {
return fmt.Errorf("failed to restart container %s: %w", containerID, err)
}
return nil
}
// Ping checks if Docker daemon is reachable
func (c *Client) Ping(ctx context.Context) error {
_, err := c.cli.Ping(ctx)
if err != nil {
return fmt.Errorf("failed to ping Docker daemon: %w", err)
}
return nil
}
// GetDockerVersion returns Docker version information
func (c *Client) GetDockerVersion(ctx context.Context) (types.Version, error) {
version, err := c.cli.ServerVersion(ctx)
if err != nil {
return types.Version{}, fmt.Errorf("failed to get Docker version: %w", err)
}
return version, nil
}

258
internal/docker/compose.go Archivo normal
Ver fichero

@@ -0,0 +1,258 @@
package docker
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/yourusername/buque/internal/models"
)
// ComposeManager manages Docker Compose operations
type ComposeManager struct {
composeCommand string
}
// NewComposeManager creates a new Docker Compose manager
func NewComposeManager() (*ComposeManager, error) {
// Try to find docker compose command
cmd := "docker"
if err := exec.Command(cmd, "compose", "version").Run(); err == nil {
return &ComposeManager{composeCommand: cmd}, nil
}
// Fallback to docker-compose
cmd = "docker-compose"
if err := exec.Command(cmd, "version").Run(); err == nil {
return &ComposeManager{composeCommand: cmd}, nil
}
return nil, fmt.Errorf("docker compose not found. Please install Docker and Docker Compose")
}
// Up starts services in an environment
func (cm *ComposeManager) Up(ctx context.Context, env models.Environment, detach bool) error {
args := cm.buildComposeArgs(env)
args = append(args, "up")
if detach {
args = append(args, "-d")
}
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = env.Path
return cmd.Run()
}
// Down stops and removes services in an environment
func (cm *ComposeManager) Down(ctx context.Context, env models.Environment, removeVolumes bool) error {
args := cm.buildComposeArgs(env)
args = append(args, "down")
if removeVolumes {
args = append(args, "-v")
}
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = env.Path
return cmd.Run()
}
// Restart restarts services in an environment
func (cm *ComposeManager) Restart(ctx context.Context, env models.Environment) error {
args := cm.buildComposeArgs(env)
args = append(args, "restart")
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = env.Path
return cmd.Run()
}
// Pull pulls images for an environment
func (cm *ComposeManager) Pull(ctx context.Context, env models.Environment) error {
args := cm.buildComposeArgs(env)
args = append(args, "pull")
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = env.Path
return cmd.Run()
}
// PS lists services in an environment
func (cm *ComposeManager) PS(ctx context.Context, env models.Environment) (string, error) {
args := cm.buildComposeArgs(env)
args = append(args, "ps", "--format", "json")
cmd := cm.createCommand(ctx, args...)
cmd.Dir = env.Path
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to list services: %w\n%s", err, string(output))
}
return string(output), nil
}
// Logs retrieves logs from an environment
func (cm *ComposeManager) Logs(ctx context.Context, env models.Environment, follow bool, tail string) error {
args := cm.buildComposeArgs(env)
args = append(args, "logs")
if follow {
args = append(args, "-f")
}
if tail != "" {
args = append(args, "--tail", tail)
}
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = env.Path
return cmd.Run()
}
// Update updates images and recreates services
func (cm *ComposeManager) Update(ctx context.Context, env models.Environment) error {
// Pull latest images
if err := cm.Pull(ctx, env); err != nil {
return fmt.Errorf("failed to pull images: %w", err)
}
// Recreate services with new images
args := cm.buildComposeArgs(env)
args = append(args, "up", "-d", "--force-recreate")
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = env.Path
return cmd.Run()
}
// Build builds images for an environment
func (cm *ComposeManager) Build(ctx context.Context, env models.Environment) error {
args := cm.buildComposeArgs(env)
args = append(args, "build")
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = env.Path
return cmd.Run()
}
// ValidateComposeFile checks if the compose file is valid
func (cm *ComposeManager) ValidateComposeFile(env models.Environment) error {
composeFilePath := filepath.Join(env.Path, env.ComposeFile)
if _, err := os.Stat(composeFilePath); os.IsNotExist(err) {
return fmt.Errorf("compose file not found: %s", composeFilePath)
}
args := cm.buildComposeArgs(env)
args = append(args, "config", "--quiet")
cmd := cm.createCommand(context.Background(), args...)
cmd.Dir = env.Path
if err := cmd.Run(); err != nil {
return fmt.Errorf("invalid compose file: %w", err)
}
return nil
}
// GetConfig returns the resolved compose configuration
func (cm *ComposeManager) GetConfig(ctx context.Context, env models.Environment) (string, error) {
args := cm.buildComposeArgs(env)
args = append(args, "config")
cmd := cm.createCommand(ctx, args...)
cmd.Dir = env.Path
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to get config: %w\n%s", err, string(output))
}
return string(output), nil
}
// buildComposeArgs builds the base arguments for docker compose commands
func (cm *ComposeManager) buildComposeArgs(env models.Environment) []string {
args := []string{}
if cm.composeCommand == "docker" {
args = append(args, "compose")
}
if env.ComposeFile != "" && env.ComposeFile != "docker-compose.yml" {
args = append(args, "-f", env.ComposeFile)
}
return args
}
// createCommand creates an exec.Cmd with the given arguments
func (cm *ComposeManager) createCommand(ctx context.Context, args ...string) *exec.Cmd {
if ctx == nil {
ctx = context.Background()
}
if cm.composeCommand == "docker-compose" {
return exec.CommandContext(ctx, cm.composeCommand, args...)
}
return exec.CommandContext(ctx, cm.composeCommand, args...)
}
// ExecInService executes a command in a running service
func (cm *ComposeManager) ExecInService(ctx context.Context, env models.Environment, service string, command []string) error {
args := cm.buildComposeArgs(env)
args = append(args, "exec", service)
args = append(args, command...)
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Dir = env.Path
return cmd.Run()
}
// CopyLogs copies logs from a service to a writer
func (cm *ComposeManager) CopyLogs(ctx context.Context, env models.Environment, service string, writer io.Writer) error {
args := cm.buildComposeArgs(env)
args = append(args, "logs", service)
cmd := cm.createCommand(ctx, args...)
cmd.Stdout = writer
cmd.Stderr = writer
cmd.Dir = env.Path
return cmd.Run()
}