mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-19 01:14:49 -05:00
feat: add basic cli and list command (#4)
* feat: add containermanager package and docker provider Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com> * feat: add list command Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com> * feat: add root command Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com> * feat: add list command Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com> * feat: wire-up root command in main Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com> * refactor(docker): use format json Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com> --------- Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com>
This commit is contained in:
committed by
Alessio Biancalana
parent
e18f7abec7
commit
1f5c3c3441
@@ -2,11 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/89luca89/distrobox/internal/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
(&cli.Command{}).Run(context.Background(), os.Args)
|
||||
cmd := cli.NewRootCommand()
|
||||
|
||||
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/89luca89/distrobox/pkg/commands"
|
||||
"github.com/89luca89/distrobox/pkg/containermanager"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
colorGreen = "\033[32m"
|
||||
colorYellow = "\033[33m"
|
||||
colorReset = "\033[0m"
|
||||
)
|
||||
|
||||
func newListCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List distroboxes",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "no-color",
|
||||
Usage: "Disable color output",
|
||||
},
|
||||
},
|
||||
Action: listAction,
|
||||
}
|
||||
}
|
||||
|
||||
func listAction(ctx context.Context, cmd *cli.Command) error {
|
||||
containerManager, ok := ctx.Value(containerManagerKey).(containermanager.ContainerManager)
|
||||
if !ok {
|
||||
return fmt.Errorf("container manager not found in context")
|
||||
}
|
||||
|
||||
listCmd := commands.NewListCommand(containerManager)
|
||||
result, err := listCmd.Execute(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute list command: %w", err)
|
||||
}
|
||||
|
||||
noColor := cmd.Bool("no-color") || !isTerminal()
|
||||
printResult(result, noColor)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printResult(result *commands.ListResult, noColor bool) {
|
||||
fmt.Printf("%-12s | %-20s | %-18s | %-30s\n",
|
||||
"ID", "NAME", "STATUS", "IMAGE")
|
||||
|
||||
for _, c := range result.Containers {
|
||||
if noColor {
|
||||
fmt.Printf("%-12s | %-20s | %-18s | %-30s\n",
|
||||
c.ID, c.Name, c.Status, c.Image)
|
||||
} else {
|
||||
color := colorYellow
|
||||
if c.IsRunning() {
|
||||
color = colorGreen
|
||||
}
|
||||
fmt.Printf("%s%-12s | %-20s | %-18s | %-30s%s\n",
|
||||
color, c.ID, c.Name, c.Status, c.Image, colorReset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTerminal() bool {
|
||||
stat, _ := os.Stdout.Stat()
|
||||
return (stat.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/89luca89/distrobox/pkg/containermanager"
|
||||
"github.com/89luca89/distrobox/pkg/containermanager/providers"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const containerManagerKey contextKey = "containerManager"
|
||||
|
||||
func NewRootCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "distrobox",
|
||||
Version: "1.0.0",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "container-manager",
|
||||
Usage: "",
|
||||
Sources: cli.EnvVars("DBX_CONTAINER_MANAGER", "container_manager"),
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sudo-command",
|
||||
Usage: "",
|
||||
Sources: cli.EnvVars("DBX_SUDO_COMMAND", "sudo_command"),
|
||||
Hidden: true,
|
||||
Value: "sudo",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"v"},
|
||||
Usage: "show more verbosity",
|
||||
Sources: cli.EnvVars("DBX_VERBOSE", "verbose"),
|
||||
},
|
||||
},
|
||||
Before: beforeAction,
|
||||
Commands: []*cli.Command{
|
||||
newListCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func beforeAction(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||
containerManagerType := cmd.String("container-manager")
|
||||
verbose := cmd.Bool("verbose")
|
||||
|
||||
var containerManager containermanager.ContainerManager
|
||||
switch containerManagerType {
|
||||
case "docker":
|
||||
containerManager = providers.NewDocker(verbose)
|
||||
default:
|
||||
containerManager = providers.NewDocker(verbose)
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, contextKey("containerManager"), containerManager), nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/89luca89/distrobox/pkg/containermanager"
|
||||
)
|
||||
|
||||
type ListResult struct {
|
||||
Containers []containermanager.Container
|
||||
}
|
||||
|
||||
type ListCommand struct {
|
||||
containerManager containermanager.ContainerManager
|
||||
}
|
||||
|
||||
func NewListCommand(cm containermanager.ContainerManager) *ListCommand {
|
||||
return &ListCommand{
|
||||
containerManager: cm,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ListCommand) Execute(ctx context.Context) (*ListResult, error) {
|
||||
containers, err := c.containerManager.ListContainers(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed while listing contaiers: %w", err)
|
||||
}
|
||||
|
||||
var distroboxes []containermanager.Container
|
||||
for _, container := range containers {
|
||||
if container.IsDistrobox() {
|
||||
distroboxes = append(distroboxes, container)
|
||||
}
|
||||
}
|
||||
|
||||
return &ListResult{Containers: distroboxes}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package containermanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
ID string
|
||||
Image string
|
||||
Name string
|
||||
Status string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
func (c Container) IsDistrobox() bool {
|
||||
return c.Labels["manager"] == "distrobox"
|
||||
}
|
||||
|
||||
func (c Container) IsRunning() bool {
|
||||
s := strings.ToLower(c.Status)
|
||||
return strings.Contains(s, "up") || strings.Contains(s, "running")
|
||||
}
|
||||
|
||||
type ContanerManagerType string
|
||||
|
||||
type ContainerManager interface {
|
||||
Name() string
|
||||
ListContainers(ctx context.Context) ([]Container, error)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/89luca89/distrobox/pkg/containermanager"
|
||||
)
|
||||
|
||||
type Docker struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
var _ containermanager.ContainerManager = &Docker{}
|
||||
|
||||
func NewDocker(verbose bool) *Docker {
|
||||
return &Docker{
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Docker) Name() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
// dockerContainer represents the JSON output from `docker ps --format json`.
|
||||
type dockerContainer struct {
|
||||
ID string `json:"ID"`
|
||||
Image string `json:"Image"`
|
||||
Names string `json:"Names"`
|
||||
Status string `json:"Status"`
|
||||
Labels string `json:"Labels"`
|
||||
}
|
||||
|
||||
func (d *Docker) ListContainers(ctx context.Context) ([]containermanager.Container, error) {
|
||||
args := []string{"ps", "-a", "--no-trunc", "--format", "json"}
|
||||
out, err := d.run(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseContainerList(out)
|
||||
}
|
||||
|
||||
func (d *Docker) run(ctx context.Context, args []string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
captured := strings.TrimSpace(stderr.String())
|
||||
if captured != "" {
|
||||
return "", fmt.Errorf("command execution failed: %s", captured)
|
||||
}
|
||||
return "", fmt.Errorf("command execution failed: %w", err)
|
||||
}
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
func parseContainerList(output string) ([]containermanager.Container, error) {
|
||||
var containers []containermanager.Container
|
||||
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var dc dockerContainer
|
||||
if err := json.Unmarshal([]byte(line), &dc); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse container JSON: %w", err)
|
||||
}
|
||||
|
||||
id := dc.ID
|
||||
if len(id) > 12 {
|
||||
id = id[:12]
|
||||
}
|
||||
|
||||
containers = append(containers, containermanager.Container{
|
||||
ID: id,
|
||||
Image: dc.Image,
|
||||
Name: dc.Names,
|
||||
Status: dc.Status,
|
||||
Labels: parseLabels(dc.Labels),
|
||||
})
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func parseLabels(labels string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
if labels == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
for label := range strings.SplitSeq(labels, ",") {
|
||||
key, value, found := strings.Cut(label, "=")
|
||||
if found {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user