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

207
internal/cmd/compose.go Archivo normal
Ver fichero

@@ -0,0 +1,207 @@
package cmd
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/yourusername/buque/internal/docker"
)
var upCmd = &cobra.Command{
Use: "up [environment...]",
Short: "Start environments",
Long: `Start one or more environments. If no environment is specified, starts all enabled environments.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
compose, err := docker.NewComposeManager()
if err != nil {
return err
}
detach, _ := cmd.Flags().GetBool("detach")
build, _ := cmd.Flags().GetBool("build")
ctx := context.Background()
environments := getEnvironmentsToProcess(args, cfg.Environments)
for _, env := range environments {
if !env.Enabled {
fmt.Printf("Skipping disabled environment: %s\n", env.Name)
continue
}
fmt.Printf("Starting environment: %s\n", env.Name)
if build {
if err := compose.Build(ctx, env); err != nil {
fmt.Printf("Warning: failed to build %s: %v\n", env.Name, err)
}
}
if err := compose.Up(ctx, env, detach); err != nil {
return fmt.Errorf("failed to start %s: %w", env.Name, err)
}
fmt.Printf("Environment '%s' started successfully!\n", env.Name)
}
return nil
},
}
var downCmd = &cobra.Command{
Use: "down [environment...]",
Short: "Stop environments",
Long: `Stop one or more environments. If no environment is specified, stops all environments.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
compose, err := docker.NewComposeManager()
if err != nil {
return err
}
removeVolumes, _ := cmd.Flags().GetBool("volumes")
ctx := context.Background()
environments := getEnvironmentsToProcess(args, cfg.Environments)
for _, env := range environments {
fmt.Printf("Stopping environment: %s\n", env.Name)
if err := compose.Down(ctx, env, removeVolumes); err != nil {
return fmt.Errorf("failed to stop %s: %w", env.Name, err)
}
fmt.Printf("Environment '%s' stopped successfully!\n", env.Name)
}
return nil
},
}
var restartCmd = &cobra.Command{
Use: "restart [environment...]",
Short: "Restart environments",
Long: `Restart one or more environments. If no environment is specified, restarts all enabled environments.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
compose, err := docker.NewComposeManager()
if err != nil {
return err
}
ctx := context.Background()
environments := getEnvironmentsToProcess(args, cfg.Environments)
for _, env := range environments {
if !env.Enabled {
fmt.Printf("Skipping disabled environment: %s\n", env.Name)
continue
}
fmt.Printf("Restarting environment: %s\n", env.Name)
if err := compose.Restart(ctx, env); err != nil {
return fmt.Errorf("failed to restart %s: %w", env.Name, err)
}
fmt.Printf("Environment '%s' restarted successfully!\n", env.Name)
}
return nil
},
}
var pullCmd = &cobra.Command{
Use: "pull [environment...]",
Short: "Pull images for environments",
Long: `Pull latest images for one or more environments. If no environment is specified, pulls all enabled environments.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
compose, err := docker.NewComposeManager()
if err != nil {
return err
}
ctx := context.Background()
environments := getEnvironmentsToProcess(args, cfg.Environments)
for _, env := range environments {
if !env.Enabled {
fmt.Printf("Skipping disabled environment: %s\n", env.Name)
continue
}
fmt.Printf("Pulling images for environment: %s\n", env.Name)
if err := compose.Pull(ctx, env); err != nil {
fmt.Printf("Warning: failed to pull images for %s: %v\n", env.Name, err)
continue
}
fmt.Printf("Images for '%s' pulled successfully!\n", env.Name)
}
return nil
},
}
var updateCmd = &cobra.Command{
Use: "update [environment...]",
Short: "Update environments",
Long: `Update one or more environments by pulling latest images and recreating containers.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
compose, err := docker.NewComposeManager()
if err != nil {
return err
}
ctx := context.Background()
environments := getEnvironmentsToProcess(args, cfg.Environments)
for _, env := range environments {
if !env.Enabled {
fmt.Printf("Skipping disabled environment: %s\n", env.Name)
continue
}
fmt.Printf("Updating environment: %s\n", env.Name)
if err := compose.Update(ctx, env); err != nil {
return fmt.Errorf("failed to update %s: %w", env.Name, err)
}
fmt.Printf("Environment '%s' updated successfully!\n", env.Name)
}
return nil
},
}
func init() {
upCmd.Flags().BoolP("detach", "d", true, "Detached mode: Run containers in the background")
upCmd.Flags().BoolP("build", "b", false, "Build images before starting")
downCmd.Flags().BoolP("volumes", "v", false, "Remove volumes")
}

179
internal/cmd/env.go Archivo normal
Ver fichero

@@ -0,0 +1,179 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/yourusername/buque/internal/models"
)
var envCmd = &cobra.Command{
Use: "env",
Short: "Manage environments",
Long: `Add, remove, list, and manage Docker Compose environments.`,
}
var envListCmd = &cobra.Command{
Use: "list",
Short: "List all environments",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
if len(cfg.Environments) == 0 {
fmt.Println("No environments configured.")
fmt.Println("Add an environment with: buque env add <name> <path>")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "NAME\tPATH\tCOMPOSE FILE\tENABLED\tCREATED")
for _, env := range cfg.Environments {
enabled := "yes"
if !env.Enabled {
enabled = "no"
}
created := env.CreatedAt.Format("2006-01-02")
if env.CreatedAt.IsZero() {
created = "N/A"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
env.Name, env.Path, env.ComposeFile, enabled, created)
}
w.Flush()
return nil
},
}
var envAddCmd = &cobra.Command{
Use: "add <name> <path>",
Short: "Add a new environment",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
path := args[1]
// Convert to absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("invalid path: %w", err)
}
// Check if path exists
if _, err := os.Stat(absPath); os.IsNotExist(err) {
return fmt.Errorf("path does not exist: %s", absPath)
}
composeFile, _ := cmd.Flags().GetString("compose-file")
// Check if compose file exists
composePath := filepath.Join(absPath, composeFile)
if _, err := os.Stat(composePath); os.IsNotExist(err) {
return fmt.Errorf("compose file not found: %s", composePath)
}
env := models.Environment{
Name: name,
Path: absPath,
ComposeFile: composeFile,
Enabled: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Labels: make(map[string]string),
}
if err := configMgr.AddEnvironment(env); err != nil {
return fmt.Errorf("failed to add environment: %w", err)
}
fmt.Printf("Environment '%s' added successfully!\n", name)
fmt.Printf("Path: %s\n", absPath)
fmt.Printf("Compose file: %s\n", composeFile)
return nil
},
}
var envRemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove an environment",
Aliases: []string{"rm"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
if err := configMgr.RemoveEnvironment(name); err != nil {
return fmt.Errorf("failed to remove environment: %w", err)
}
fmt.Printf("Environment '%s' removed successfully!\n", name)
return nil
},
}
var envEnableCmd = &cobra.Command{
Use: "enable <name>",
Short: "Enable an environment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
cfg := configMgr.GetConfig()
for i, env := range cfg.Environments {
if env.Name == name {
cfg.Environments[i].Enabled = true
cfg.Environments[i].UpdatedAt = time.Now()
if err := configMgr.Save(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
fmt.Printf("Environment '%s' enabled!\n", name)
return nil
}
}
return fmt.Errorf("environment '%s' not found", name)
},
}
var envDisableCmd = &cobra.Command{
Use: "disable <name>",
Short: "Disable an environment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
cfg := configMgr.GetConfig()
for i, env := range cfg.Environments {
if env.Name == name {
cfg.Environments[i].Enabled = false
cfg.Environments[i].UpdatedAt = time.Now()
if err := configMgr.Save(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
fmt.Printf("Environment '%s' disabled!\n", name)
return nil
}
}
return fmt.Errorf("environment '%s' not found", name)
},
}
func init() {
envCmd.AddCommand(envListCmd)
envCmd.AddCommand(envAddCmd)
envCmd.AddCommand(envRemoveCmd)
envCmd.AddCommand(envEnableCmd)
envCmd.AddCommand(envDisableCmd)
envAddCmd.Flags().StringP("compose-file", "f", "docker-compose.yml", "Docker Compose file name")
}

28
internal/cmd/init.go Archivo normal
Ver fichero

@@ -0,0 +1,28 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize buque configuration",
Long: `Initialize buque with default configuration file.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := configMgr.Load()
if err != nil {
return fmt.Errorf("failed to initialize config: %w", err)
}
fmt.Printf("Buque initialized successfully!\n")
fmt.Printf("Configuration file: %s\n", cfg.ConfigPath)
fmt.Printf("\nNext steps:\n")
fmt.Printf(" 1. Add environments: buque env add <name> <path>\n")
fmt.Printf(" 2. Deploy nginx-proxy: buque proxy deploy\n")
fmt.Printf(" 3. Start environments: buque up <name>\n")
return nil
},
}

114
internal/cmd/logs.go Archivo normal
Ver fichero

@@ -0,0 +1,114 @@
package cmd
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/yourusername/buque/internal/docker"
"github.com/yourusername/buque/internal/models"
)
var logsCmd = &cobra.Command{
Use: "logs <environment>",
Short: "Show logs from an environment",
Long: `Display logs from containers in an environment.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
envName := args[0]
var env *models.Environment
for _, e := range cfg.Environments {
if e.Name == envName {
env = &e
break
}
}
if env == nil {
return fmt.Errorf("environment '%s' not found", envName)
}
compose, err := docker.NewComposeManager()
if err != nil {
return err
}
follow, _ := cmd.Flags().GetBool("follow")
tail, _ := cmd.Flags().GetString("tail")
ctx := context.Background()
return compose.Logs(ctx, *env, follow, tail)
},
}
var psCmd = &cobra.Command{
Use: "ps [environment...]",
Short: "List containers",
Long: `List containers for one or more environments. If no environment is specified, lists all.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
compose, err := docker.NewComposeManager()
if err != nil {
return err
}
ctx := context.Background()
environments := getEnvironmentsToProcess(args, cfg.Environments)
for _, env := range environments {
fmt.Printf("\n=== Environment: %s ===\n", env.Name)
output, err := compose.PS(ctx, env)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
if output == "" {
fmt.Println("No containers running")
} else {
fmt.Println(output)
}
}
return nil
},
}
var pruneCmd = &cobra.Command{
Use: "prune",
Short: "Remove unused Docker resources",
Long: `Remove unused containers, images, networks, and volumes.`,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
ctx := context.Background()
fmt.Println("Pruning unused images...")
if err := client.PruneImages(ctx); err != nil {
return fmt.Errorf("failed to prune images: %w", err)
}
fmt.Println("Unused resources pruned successfully!")
return nil
},
}
func init() {
logsCmd.Flags().BoolP("follow", "f", false, "Follow log output")
logsCmd.Flags().StringP("tail", "t", "100", "Number of lines to show from the end of the logs")
}

118
internal/cmd/proxy.go Archivo normal
Ver fichero

@@ -0,0 +1,118 @@
package cmd
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/yourusername/buque/internal/proxy"
)
var proxyCmd = &cobra.Command{
Use: "proxy",
Short: "Manage nginx-proxy",
Long: `Deploy, remove, and manage the nginx-proxy reverse proxy.`,
}
var proxyDeployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy nginx-proxy",
Long: `Deploy nginx-proxy with SSL support using Let's Encrypt.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
manager, err := proxy.NewNginxManager(cfg.NginxProxy)
if err != nil {
return err
}
defer manager.Close()
ctx := context.Background()
return manager.Deploy(ctx)
},
}
var proxyRemoveCmd = &cobra.Command{
Use: "remove",
Short: "Remove nginx-proxy",
Long: `Stop and remove the nginx-proxy deployment.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
manager, err := proxy.NewNginxManager(cfg.NginxProxy)
if err != nil {
return err
}
defer manager.Close()
ctx := context.Background()
return manager.Remove(ctx)
},
}
var proxyStatusCmd = &cobra.Command{
Use: "status",
Short: "Show nginx-proxy status",
Long: `Display the status of nginx-proxy containers.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
manager, err := proxy.NewNginxManager(cfg.NginxProxy)
if err != nil {
return err
}
defer manager.Close()
ctx := context.Background()
status, err := manager.Status(ctx)
if err != nil {
return err
}
fmt.Println(status)
return nil
},
}
var proxyExampleCmd = &cobra.Command{
Use: "example <service-name> <virtual-host>",
Short: "Generate example docker-compose.yml",
Long: `Generate an example docker-compose.yml for a service behind nginx-proxy.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cfg := configMgr.GetConfig()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
serviceName := args[0]
virtualHost := args[1]
manager, err := proxy.NewNginxManager(cfg.NginxProxy)
if err != nil {
return err
}
defer manager.Close()
example := manager.GetExampleServiceCompose(serviceName, virtualHost)
fmt.Println(example)
return nil
},
}
func init() {
proxyCmd.AddCommand(proxyDeployCmd)
proxyCmd.AddCommand(proxyRemoveCmd)
proxyCmd.AddCommand(proxyStatusCmd)
proxyCmd.AddCommand(proxyExampleCmd)
}

56
internal/cmd/root.go Archivo normal
Ver fichero

@@ -0,0 +1,56 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/yourusername/buque/internal/config"
)
var (
cfgFile string
configMgr *config.Manager
rootCmd *cobra.Command
)
func init() {
cobra.OnInitialize(initConfig)
rootCmd = &cobra.Command{
Use: "buque",
Short: "Buque - Docker Compose environment manager",
Long: `Buque is a command-line tool for managing multiple Docker Compose
environments on a single machine. It provides easy deployment, monitoring,
and maintenance of containerized applications with nginx-proxy integration.`,
Version: "1.0.0",
}
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.buque/config.yaml)")
// Add all subcommands
rootCmd.AddCommand(initCmd)
rootCmd.AddCommand(envCmd)
rootCmd.AddCommand(upCmd)
rootCmd.AddCommand(downCmd)
rootCmd.AddCommand(restartCmd)
rootCmd.AddCommand(updateCmd)
rootCmd.AddCommand(statsCmd)
rootCmd.AddCommand(logsCmd)
rootCmd.AddCommand(psCmd)
rootCmd.AddCommand(proxyCmd)
rootCmd.AddCommand(pullCmd)
rootCmd.AddCommand(pruneCmd)
}
func initConfig() {
configMgr = config.NewManager(cfgFile)
if _, err := configMgr.Load(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to load config: %v\n", err)
}
}
// Execute runs the root command
func Execute() error {
return rootCmd.Execute()
}

131
internal/cmd/stats.go Archivo normal
Ver fichero

@@ -0,0 +1,131 @@
package cmd
import (
"context"
"fmt"
"os"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/yourusername/buque/internal/models"
"github.com/yourusername/buque/internal/stats"
)
var statsCmd = &cobra.Command{
Use: "stats [environment]",
Short: "Show container statistics",
Long: `Display resource usage statistics for containers. If an environment is specified, shows only containers from that environment.`,
RunE: func(cmd *cobra.Command, args []string) error {
collector, err := stats.NewCollector()
if err != nil {
return err
}
defer collector.Close()
ctx := context.Background()
continuous, _ := cmd.Flags().GetBool("continuous")
interval, _ := cmd.Flags().GetInt("interval")
sortBy, _ := cmd.Flags().GetString("sort")
if continuous {
// Clear screen and show stats continuously
fmt.Print("\033[2J") // Clear screen
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
fmt.Print("\033[H") // Move cursor to home position
if err := displayStats(ctx, collector, args, sortBy); err != nil {
return err
}
fmt.Printf("\nRefreshing every %d seconds... (Press Ctrl+C to exit)\n", interval)
}
}
}
return displayStats(ctx, collector, args, sortBy)
},
}
func displayStats(ctx context.Context, collector *stats.Collector, args []string, sortBy string) error {
var containerStats []models.ContainerStats
var err error
if len(args) > 0 {
// Show stats for specific environment
containerStats, err = collector.CollectForEnvironment(ctx, args[0])
} else {
// Show stats for all containers
containerStats, err = collector.CollectAll(ctx)
}
if err != nil {
return err
}
if len(containerStats) == 0 {
fmt.Println("No running containers found.")
return nil
}
// Sort stats
containerStats = collector.SortStats(containerStats, sortBy, true)
// Display in table format
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "CONTAINER\tENVIRONMENT\tCPU %\tMEMORY USAGE\tMEMORY %\tNET I/O\tBLOCK I/O")
for _, stat := range containerStats {
netIO := fmt.Sprintf("%s / %s",
stats.FormatBytes(stat.NetworkRx),
stats.FormatBytes(stat.NetworkTx))
blockIO := fmt.Sprintf("%s / %s",
stats.FormatBytes(stat.BlockRead),
stats.FormatBytes(stat.BlockWrite))
memUsage := fmt.Sprintf("%s / %s",
stats.FormatBytes(stat.MemoryUsage),
stats.FormatBytes(stat.MemoryLimit))
fmt.Fprintf(w, "%s\t%s\t%.2f%%\t%s\t%.2f%%\t%s\t%s\n",
stat.Name,
stat.Environment,
stat.CPUPercentage,
memUsage,
stat.MemoryPercent,
netIO,
blockIO,
)
}
w.Flush()
// Show aggregated stats
aggStats, err := collector.GetAggregatedStats(ctx)
if err == nil {
fmt.Printf("\nTotal Containers: %d\n", aggStats.TotalContainers)
fmt.Printf("Total CPU: %.2f%%\n", aggStats.TotalCPUPercent)
fmt.Printf("Total Memory: %s / %s (%.2f%%)\n",
stats.FormatBytes(aggStats.TotalMemoryUsage),
stats.FormatBytes(aggStats.TotalMemoryLimit),
aggStats.TotalMemoryPercent)
fmt.Printf("Total Network: %s / %s\n",
stats.FormatBytes(aggStats.TotalNetworkRx),
stats.FormatBytes(aggStats.TotalNetworkTx))
}
return nil
}
func init() {
statsCmd.Flags().BoolP("continuous", "c", false, "Continuous monitoring mode")
statsCmd.Flags().IntP("interval", "i", 2, "Refresh interval in seconds (for continuous mode)")
statsCmd.Flags().StringP("sort", "s", "cpu", "Sort by: cpu, memory, network, name")
}

24
internal/cmd/utils.go Archivo normal
Ver fichero

@@ -0,0 +1,24 @@
package cmd
import (
"github.com/yourusername/buque/internal/models"
)
// getEnvironmentsToProcess returns environments to process based on args
func getEnvironmentsToProcess(args []string, allEnvs []models.Environment) []models.Environment {
if len(args) == 0 {
return allEnvs
}
var result []models.Environment
for _, arg := range args {
for _, env := range allEnvs {
if env.Name == arg {
result = append(result, env)
break
}
}
}
return result
}

173
internal/config/config.go Archivo normal
Ver fichero

@@ -0,0 +1,173 @@
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/yourusername/buque/internal/models"
"gopkg.in/yaml.v3"
)
const (
DefaultConfigDir = ".buque"
DefaultConfigFile = "config.yaml"
)
// Manager handles configuration operations
type Manager struct {
configPath string
config *models.Config
}
// NewManager creates a new configuration manager
func NewManager(configPath string) *Manager {
if configPath == "" {
homeDir, _ := os.UserHomeDir()
configPath = filepath.Join(homeDir, DefaultConfigDir, DefaultConfigFile)
}
return &Manager{
configPath: configPath,
}
}
// Load loads the configuration from file
func (m *Manager) Load() (*models.Config, error) {
// Create config directory if it doesn't exist
configDir := filepath.Dir(m.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
}
// Check if config file exists
if _, err := os.Stat(m.configPath); os.IsNotExist(err) {
// Create default configuration
m.config = m.defaultConfig()
if err := m.Save(); err != nil {
return nil, fmt.Errorf("failed to save default config: %w", err)
}
return m.config, nil
}
// Read existing configuration
data, err := os.ReadFile(m.configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config models.Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
config.ConfigPath = m.configPath
m.config = &config
return m.config, nil
}
// Save saves the current configuration to file
func (m *Manager) Save() error {
if m.config == nil {
return fmt.Errorf("no configuration to save")
}
data, err := yaml.Marshal(m.config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(m.configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// GetConfig returns the current configuration
func (m *Manager) GetConfig() *models.Config {
return m.config
}
// AddEnvironment adds a new environment to the configuration
func (m *Manager) AddEnvironment(env models.Environment) error {
if m.config == nil {
return fmt.Errorf("configuration not loaded")
}
// Check if environment already exists
for _, e := range m.config.Environments {
if e.Name == env.Name {
return fmt.Errorf("environment '%s' already exists", env.Name)
}
}
m.config.Environments = append(m.config.Environments, env)
return m.Save()
}
// RemoveEnvironment removes an environment from the configuration
func (m *Manager) RemoveEnvironment(name string) error {
if m.config == nil {
return fmt.Errorf("configuration not loaded")
}
for i, env := range m.config.Environments {
if env.Name == name {
m.config.Environments = append(m.config.Environments[:i], m.config.Environments[i+1:]...)
return m.Save()
}
}
return fmt.Errorf("environment '%s' not found", name)
}
// UpdateEnvironment updates an existing environment
func (m *Manager) UpdateEnvironment(env models.Environment) error {
if m.config == nil {
return fmt.Errorf("configuration not loaded")
}
for i, e := range m.config.Environments {
if e.Name == env.Name {
m.config.Environments[i] = env
return m.Save()
}
}
return fmt.Errorf("environment '%s' not found", env.Name)
}
// GetEnvironment retrieves an environment by name
func (m *Manager) GetEnvironment(name string) (*models.Environment, error) {
if m.config == nil {
return nil, fmt.Errorf("configuration not loaded")
}
for _, env := range m.config.Environments {
if env.Name == env.Name {
return &env, nil
}
}
return nil, fmt.Errorf("environment '%s' not found", name)
}
// defaultConfig returns a default configuration
func (m *Manager) defaultConfig() *models.Config {
return &models.Config{
ConfigPath: m.configPath,
Environments: []models.Environment{},
NginxProxy: models.NginxProxyConfig{
Enabled: false,
NetworkName: "nginx-proxy",
ContainerName: "nginx-proxy",
Path: filepath.Join(filepath.Dir(m.configPath), "nginx-proxy"),
HTTPPort: 80,
HTTPSPort: 443,
SSLEnabled: true,
},
Docker: models.DockerConfig{
ComposeVersion: "v2",
},
}
}

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()
}

92
internal/models/models.go Archivo normal
Ver fichero

@@ -0,0 +1,92 @@
package models
import "time"
// Environment represents a Docker Compose environment
type Environment struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
ComposeFile string `yaml:"compose_file"`
Enabled bool `yaml:"enabled"`
Labels map[string]string `yaml:"labels,omitempty"`
CreatedAt time.Time `yaml:"created_at"`
UpdatedAt time.Time `yaml:"updated_at"`
}
// Service represents a Docker service/container
type Service struct {
ID string
Name string
Image string
Status string
State string
Environment string
Ports []string
Networks []string
CreatedAt time.Time
RestartCount int
}
// ContainerStats represents statistics for a running container
type ContainerStats struct {
ID string
Name string
Environment string
CPUPercentage float64
MemoryUsage uint64
MemoryLimit uint64
MemoryPercent float64
NetworkRx uint64
NetworkTx uint64
BlockRead uint64
BlockWrite uint64
PIDs uint64
}
// Config represents the buque configuration
type Config struct {
ConfigPath string `yaml:"config_path"`
Environments []Environment `yaml:"environments"`
NginxProxy NginxProxyConfig `yaml:"nginx_proxy"`
Docker DockerConfig `yaml:"docker"`
UpdateSchedule string `yaml:"update_schedule,omitempty"`
}
// NginxProxyConfig represents nginx-proxy configuration
type NginxProxyConfig struct {
Enabled bool `yaml:"enabled"`
NetworkName string `yaml:"network_name"`
ContainerName string `yaml:"container_name"`
Path string `yaml:"path"`
HTTPPort int `yaml:"http_port"`
HTTPSPort int `yaml:"https_port"`
SSLEnabled bool `yaml:"ssl_enabled"`
Labels map[string]string `yaml:"labels,omitempty"`
}
// DockerConfig represents Docker-related configuration
type DockerConfig struct {
Host string `yaml:"host,omitempty"`
APIVersion string `yaml:"api_version,omitempty"`
ComposeVersion string `yaml:"compose_version,omitempty"`
}
// EnvironmentStatus represents the status of an environment
type EnvironmentStatus struct {
Environment Environment
Services []Service
Running int
Stopped int
Error error
}
// UpdateResult represents the result of an update operation
type UpdateResult struct {
Environment string
Service string
OldImage string
NewImage string
Success bool
Error error
UpdatedAt time.Time
}

252
internal/proxy/nginx.go Archivo normal
Ver fichero

@@ -0,0 +1,252 @@
package proxy
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/yourusername/buque/internal/docker"
"github.com/yourusername/buque/internal/models"
)
const (
nginxProxyImage = "nginxproxy/nginx-proxy:latest"
nginxProxyCompanionImage = "nginxproxy/acme-companion:latest"
)
// NginxManager manages nginx-proxy deployment and configuration
type NginxManager struct {
config models.NginxProxyConfig
dockerClient *docker.Client
composeManager *docker.ComposeManager
}
// NewNginxManager creates a new nginx-proxy manager
func NewNginxManager(config models.NginxProxyConfig) (*NginxManager, error) {
dockerClient, err := docker.NewClient()
if err != nil {
return nil, err
}
composeManager, err := docker.NewComposeManager()
if err != nil {
return nil, err
}
return &NginxManager{
config: config,
dockerClient: dockerClient,
composeManager: composeManager,
}, nil
}
// Close closes the nginx manager
func (nm *NginxManager) Close() error {
return nm.dockerClient.Close()
}
// Deploy deploys the nginx-proxy environment
func (nm *NginxManager) Deploy(ctx context.Context) error {
// Create nginx-proxy directory if it doesn't exist
if err := os.MkdirAll(nm.config.Path, 0755); err != nil {
return fmt.Errorf("failed to create nginx-proxy directory: %w", err)
}
// Create docker-compose.yml
composeContent := nm.generateComposeFile()
composePath := filepath.Join(nm.config.Path, "docker-compose.yml")
if err := os.WriteFile(composePath, []byte(composeContent), 0644); err != nil {
return fmt.Errorf("failed to write compose file: %w", err)
}
// Create network if it doesn't exist
exists, err := nm.dockerClient.NetworkExists(ctx, nm.config.NetworkName)
if err != nil {
return fmt.Errorf("failed to check network: %w", err)
}
if !exists {
if err := nm.dockerClient.CreateNetwork(ctx, nm.config.NetworkName); err != nil {
return fmt.Errorf("failed to create network: %w", err)
}
fmt.Printf("Created network: %s\n", nm.config.NetworkName)
}
// Deploy using docker-compose
env := models.Environment{
Name: "nginx-proxy",
Path: nm.config.Path,
ComposeFile: "docker-compose.yml",
Enabled: true,
}
fmt.Println("Deploying nginx-proxy...")
if err := nm.composeManager.Up(ctx, env, true); err != nil {
return fmt.Errorf("failed to deploy nginx-proxy: %w", err)
}
fmt.Println("Nginx-proxy deployed successfully!")
return nil
}
// Remove removes the nginx-proxy environment
func (nm *NginxManager) Remove(ctx context.Context) error {
env := models.Environment{
Name: "nginx-proxy",
Path: nm.config.Path,
ComposeFile: "docker-compose.yml",
Enabled: true,
}
fmt.Println("Removing nginx-proxy...")
if err := nm.composeManager.Down(ctx, env, true); err != nil {
return fmt.Errorf("failed to remove nginx-proxy: %w", err)
}
fmt.Println("Nginx-proxy removed successfully!")
return nil
}
// Status returns the status of nginx-proxy
func (nm *NginxManager) Status(ctx context.Context) (string, error) {
env := models.Environment{
Name: "nginx-proxy",
Path: nm.config.Path,
ComposeFile: "docker-compose.yml",
Enabled: true,
}
return nm.composeManager.PS(ctx, env)
}
// generateComposeFile generates the docker-compose.yml content for nginx-proxy
func (nm *NginxManager) generateComposeFile() string {
content := fmt.Sprintf(`version: '3.8'
services:
nginx-proxy:
image: %s
container_name: %s
restart: unless-stopped
ports:
- "%d:80"`, nginxProxyImage, nm.config.ContainerName, nm.config.HTTPPort)
if nm.config.SSLEnabled {
content += fmt.Sprintf(`
- "%d:443"`, nm.config.HTTPSPort)
}
content += `
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro`
if nm.config.SSLEnabled {
content += `
- certs:/etc/nginx/certs:ro
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html`
}
content += `
environment:
- DEFAULT_HOST=localhost
networks:
- ` + nm.config.NetworkName
content += `
labels:
- "buque.managed=true"
- "buque.service=nginx-proxy"`
if nm.config.SSLEnabled {
content += fmt.Sprintf(`
acme-companion:
image: %s
container_name: nginx-proxy-acme
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- certs:/etc/nginx/certs
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- acme:/etc/acme.sh
environment:
- DEFAULT_EMAIL=admin@localhost
- NGINX_PROXY_CONTAINER=%s
networks:
- %s
depends_on:
- nginx-proxy
labels:
- "buque.managed=true"
- "buque.service=nginx-proxy-acme"`, nginxProxyCompanionImage, nm.config.ContainerName, nm.config.NetworkName)
}
content += `
networks:
` + nm.config.NetworkName + `:
external: true`
if nm.config.SSLEnabled {
content += `
volumes:
certs:
vhost:
html:
acme:`
}
return content
}
// GenerateServiceLabels generates labels for a service to work with nginx-proxy
func (nm *NginxManager) GenerateServiceLabels(virtualHost string, virtualPort int, letsencryptHost string, letsencryptEmail string) map[string]string {
labels := map[string]string{
"VIRTUAL_HOST": virtualHost,
}
if virtualPort > 0 {
labels["VIRTUAL_PORT"] = fmt.Sprintf("%d", virtualPort)
}
if nm.config.SSLEnabled && letsencryptHost != "" {
labels["LETSENCRYPT_HOST"] = letsencryptHost
if letsencryptEmail != "" {
labels["LETSENCRYPT_EMAIL"] = letsencryptEmail
}
}
return labels
}
// GetExampleServiceCompose returns an example docker-compose.yml for a service behind nginx-proxy
func (nm *NginxManager) GetExampleServiceCompose(serviceName, virtualHost string) string {
return fmt.Sprintf(`version: '3.8'
services:
%s:
image: your-image:latest
container_name: %s
restart: unless-stopped
expose:
- "80"
environment:
- VIRTUAL_HOST=%s
- VIRTUAL_PORT=80
- LETSENCRYPT_HOST=%s
- LETSENCRYPT_EMAIL=admin@%s
networks:
- %s
labels:
- "buque.environment=%s"
- "buque.managed=true"
networks:
%s:
external: true
`, serviceName, serviceName, virtualHost, virtualHost, virtualHost, nm.config.NetworkName, serviceName, nm.config.NetworkName)
}

198
internal/stats/collector.go Archivo normal
Ver fichero

@@ -0,0 +1,198 @@
package stats
import (
"context"
"fmt"
"sort"
"time"
"github.com/yourusername/buque/internal/docker"
"github.com/yourusername/buque/internal/models"
)
// Collector collects and manages container statistics
type Collector struct {
dockerClient *docker.Client
}
// NewCollector creates a new statistics collector
func NewCollector() (*Collector, error) {
client, err := docker.NewClient()
if err != nil {
return nil, err
}
return &Collector{
dockerClient: client,
}, nil
}
// Close closes the statistics collector
func (sc *Collector) Close() error {
return sc.dockerClient.Close()
}
// CollectAll collects statistics for all running containers
func (sc *Collector) CollectAll(ctx context.Context) ([]models.ContainerStats, error) {
containers, err := sc.dockerClient.ListContainers(ctx, false)
if err != nil {
return nil, err
}
stats := make([]models.ContainerStats, 0, len(containers))
for _, container := range containers {
stat, err := sc.dockerClient.GetContainerStats(ctx, container.ID)
if err != nil {
// Log error but continue with other containers
fmt.Printf("Warning: failed to get stats for container %s: %v\n", container.Names[0], err)
continue
}
// Extract environment name from labels
if envName, ok := container.Labels["buque.environment"]; ok {
stat.Environment = envName
}
// Clean up container name (remove leading /)
if len(container.Names) > 0 && len(container.Names[0]) > 0 {
stat.Name = container.Names[0][1:]
}
stats = append(stats, *stat)
}
return stats, nil
}
// CollectForEnvironment collects statistics for containers in a specific environment
func (sc *Collector) CollectForEnvironment(ctx context.Context, envName string) ([]models.ContainerStats, error) {
containers, err := sc.dockerClient.GetContainersByLabel(ctx, "buque.environment", envName)
if err != nil {
return nil, err
}
stats := make([]models.ContainerStats, 0, len(containers))
for _, container := range containers {
stat, err := sc.dockerClient.GetContainerStats(ctx, container.ID)
if err != nil {
fmt.Printf("Warning: failed to get stats for container %s: %v\n", container.Names[0], err)
continue
}
stat.Environment = envName
if len(container.Names) > 0 && len(container.Names[0]) > 0 {
stat.Name = container.Names[0][1:]
}
stats = append(stats, *stat)
}
return stats, nil
}
// GetAggregatedStats returns aggregated statistics for all containers
func (sc *Collector) GetAggregatedStats(ctx context.Context) (*AggregatedStats, error) {
stats, err := sc.CollectAll(ctx)
if err != nil {
return nil, err
}
agg := &AggregatedStats{
TotalContainers: len(stats),
CollectedAt: time.Now(),
}
for _, stat := range stats {
agg.TotalCPUPercent += stat.CPUPercentage
agg.TotalMemoryUsage += stat.MemoryUsage
agg.TotalMemoryLimit += stat.MemoryLimit
agg.TotalNetworkRx += stat.NetworkRx
agg.TotalNetworkTx += stat.NetworkTx
agg.TotalBlockRead += stat.BlockRead
agg.TotalBlockWrite += stat.BlockWrite
}
if agg.TotalMemoryLimit > 0 {
agg.TotalMemoryPercent = (float64(agg.TotalMemoryUsage) / float64(agg.TotalMemoryLimit)) * 100.0
}
return agg, nil
}
// SortStats sorts container statistics by the specified field
func (sc *Collector) SortStats(stats []models.ContainerStats, sortBy string, descending bool) []models.ContainerStats {
sort.Slice(stats, func(i, j int) bool {
var less bool
switch sortBy {
case "cpu":
less = stats[i].CPUPercentage < stats[j].CPUPercentage
case "memory":
less = stats[i].MemoryUsage < stats[j].MemoryUsage
case "network":
less = (stats[i].NetworkRx + stats[i].NetworkTx) < (stats[j].NetworkRx + stats[j].NetworkTx)
case "name":
less = stats[i].Name < stats[j].Name
default:
less = stats[i].Name < stats[j].Name
}
if descending {
return !less
}
return less
})
return stats
}
// MonitorContinuously monitors container statistics continuously
func (sc *Collector) MonitorContinuously(ctx context.Context, interval time.Duration, callback func([]models.ContainerStats)) error {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
stats, err := sc.CollectAll(ctx)
if err != nil {
return err
}
callback(stats)
}
}
}
// AggregatedStats represents aggregated statistics for all containers
type AggregatedStats struct {
TotalContainers int
TotalCPUPercent float64
TotalMemoryUsage uint64
TotalMemoryLimit uint64
TotalMemoryPercent float64
TotalNetworkRx uint64
TotalNetworkTx uint64
TotalBlockRead uint64
TotalBlockWrite uint64
CollectedAt time.Time
}
// FormatBytes formats bytes to human-readable format
func FormatBytes(bytes uint64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := uint64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.2f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// FormatPercent formats a percentage value
func FormatPercent(percent float64) string {
return fmt.Sprintf("%.2f%%", percent)
}