207
internal/cmd/compose.go
Archivo normal
207
internal/cmd/compose.go
Archivo normal
@@ -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
179
internal/cmd/env.go
Archivo normal
@@ -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
28
internal/cmd/init.go
Archivo normal
@@ -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
114
internal/cmd/logs.go
Archivo normal
@@ -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
118
internal/cmd/proxy.go
Archivo normal
@@ -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
56
internal/cmd/root.go
Archivo normal
@@ -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
131
internal/cmd/stats.go
Archivo normal
@@ -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
24
internal/cmd/utils.go
Archivo normal
@@ -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
173
internal/config/config.go
Archivo normal
@@ -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
264
internal/docker/client.go
Archivo normal
@@ -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
258
internal/docker/compose.go
Archivo normal
@@ -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
92
internal/models/models.go
Archivo normal
@@ -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
252
internal/proxy/nginx.go
Archivo normal
@@ -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
198
internal/stats/collector.go
Archivo normal
@@ -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)
|
||||
}
|
||||
Referencia en una nueva incidencia
Block a user