This commit is contained in:
Colin 2024-08-29 11:22:58 -04:00
parent 7ce835c4de
commit b18dab5b49
8 changed files with 54 additions and 52 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

106
main.go
View File

@ -14,26 +14,21 @@ import (
)
func main() {
// Command-line flags
stackName := flag.String("s", "", "Specify the stack or project name to export.")
outputFile := flag.String("o", "docker-compose.yml", "Specify the output file for the YAML.")
flag.Parse()
// Fetch list of all containers with their stack or project labels
containers, err := listContainers()
if err != nil {
log.Fatalf("Error fetching container list: %v", err)
}
// Map to group containers by stack or project
stacks := make(map[string][]Container)
// Group containers by Docker Compose project or Docker Swarm stack
for _, container := range containers {
project := container.Labels["com.docker.compose.project"]
stackNamespace := container.Labels["com.docker.stack.namespace"]
// Use project name if available; otherwise, use stack namespace
stackKey := project
if stackNamespace != "" {
stackKey = stackNamespace
@ -45,7 +40,6 @@ func main() {
}
if *stackName == "" {
// No stack specified, list available stacks
if len(stacks) == 0 {
fmt.Println("No Docker Compose projects or Docker Swarm stacks found.")
} else {
@ -57,22 +51,18 @@ func main() {
return
}
// Stack specified, check if it exists
containers, exists := stacks[*stackName]
if !exists {
log.Fatalf("Stack or project '%s' not found.", *stackName)
}
// Determine if this is a Docker Swarm stack or a Docker Compose project
isSwarm := isDockerSwarmStack(*stackName, containers)
if isSwarm {
// Notify the user that exporting Swarm stacks isn't fully supported
fmt.Printf("Warning: The stack '%s' appears to be a Docker Swarm stack. Export functionality for Swarm stacks may not provide complete configurations.\n", *stackName)
fmt.Println("Skipping export for Docker Swarm stacks. Please use `docker stack` commands directly for managing Swarm stacks.")
return
} else {
// Handle Docker Compose project export
err = exportComposeProjectToComposeFile(*stackName, containers, *outputFile)
if err != nil {
log.Fatalf("Error exporting Docker Compose project %s: %v", *stackName, err)
@ -81,17 +71,18 @@ func main() {
}
}
// Container represents basic Docker container information.
type Container struct {
ID string
Name string
Image string
Labels map[string]string
Env []string
Networks []string
ID string
Name string
Image string
Labels map[string]string
Env []string
Networks []string
Volumes []string
Ports []string
RestartPolicy string
}
// listContainers runs 'docker ps' and 'docker inspect' to get a list of all containers.
func listContainers() ([]Container, error) {
cmd := exec.Command("docker", "ps", "--format", "{{.ID}} {{.Names}} {{.Image}} {{.Label \"com.docker.compose.project\"}} {{.Label \"com.docker.stack.namespace\"}}")
var out bytes.Buffer
@ -106,8 +97,6 @@ func listContainers() ([]Container, error) {
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
// Check that there are at least 3 fields (ID, Name, Image)
if len(fields) < 3 {
log.Printf("Warning: Skipping incomplete line: %s", scanner.Text())
continue
@ -117,7 +106,6 @@ func listContainers() ([]Container, error) {
name := fields[1]
image := fields[2]
// Handle cases where project or stackNamespace might be missing
project := ""
stackNamespace := ""
if len(fields) > 3 {
@ -132,49 +120,60 @@ func listContainers() ([]Container, error) {
"com.docker.stack.namespace": stackNamespace,
}
// Run 'docker inspect' to get detailed container information.
env, networks, err := inspectContainer(id)
env, networks, volumes, ports, restartPolicy, err := inspectContainer(id)
if err != nil {
log.Printf("Warning: Error inspecting container %s: %v", id, err)
continue
}
containers = append(containers, Container{
ID: id,
Name: name,
Image: image,
Labels: labels,
Env: env,
Networks: networks,
ID: id,
Name: name,
Image: image,
Labels: labels,
Env: env,
Networks: networks,
Volumes: volumes,
Ports: ports,
RestartPolicy: restartPolicy,
})
}
return containers, scanner.Err()
}
// inspectContainer runs 'docker inspect' and parses environment variables and networks.
func inspectContainer(containerID string) ([]string, []string, error) {
cmd := exec.Command("docker", "inspect", "--format", "{{range .Config.Env}}{{printf \"%s \" .}}{{end}} {{range $key, $value := .NetworkSettings.Networks}}{{$key}} {{end}}", containerID)
func inspectContainer(containerID string) ([]string, []string, []string, []string, string, error) {
cmd := exec.Command("docker", "inspect", "--format", `{{range .Config.Env}}{{println .}}{{end}}{{range .NetworkSettings.Networks}}{{println .}}{{end}}{{range .Mounts}}{{println .}}{{end}}{{range $p, $conf := .HostConfig.PortBindings}}{{println $p}}{{end}}{{.HostConfig.RestartPolicy.Name}}`, containerID)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, nil, err
return nil, nil, nil, nil, "", err
}
// Split output into environment variables and network names
parts := strings.Split(out.String(), " ")
if len(parts) < 2 {
// Handle cases where no networks are listed
parts = append(parts, "")
}
envVars := strings.Fields(parts[0])
networks := strings.Fields(parts[1])
lines := strings.Split(out.String(), "\n")
return envVars, networks, nil
var envVars, networks, volumes, ports []string
restartPolicy := ""
for _, line := range lines {
if strings.Contains(line, "=") {
envVars = append(envVars, line)
} else if strings.Contains(line, "Name:") && strings.Contains(line, "Driver:") {
networks = append(networks, strings.TrimSpace(line))
} else if strings.Contains(line, "Source:") && strings.Contains(line, "Destination:") {
volume := strings.Split(line, "Destination:")[1]
volumes = append(volumes, volume)
} else if strings.Contains(line, "/tcp") || strings.Contains(line, "/udp") {
ports = append(ports, line)
} else if line != "" {
restartPolicy = line
}
}
return envVars, networks, volumes, ports, restartPolicy, nil
}
// isDockerSwarmStack checks if the stack is a Docker Swarm stack based on the container names.
func isDockerSwarmStack(stackName string, containers []Container) bool {
for _, container := range containers {
if strings.Contains(container.Name, ".") {
@ -184,43 +183,46 @@ func isDockerSwarmStack(stackName string, containers []Container) bool {
return false
}
// exportComposeProjectToComposeFile exports a Docker Compose project to a docker-compose.yml file.
func exportComposeProjectToComposeFile(stack string, containers []Container, outputFile string) error {
// Map to hold docker-compose configuration
composeConfig := make(map[string]interface{})
composeConfig["services"] = make(map[string]interface{})
// Process each container in the stack
for _, container := range containers {
serviceName := container.Name
image := container.Image
// Initialize service configuration
serviceConfig := map[string]interface{}{
"image": image,
}
// Handle environment variables
if len(container.Env) > 0 {
serviceConfig["environment"] = container.Env
}
// Handle network settings
if len(container.Networks) > 0 {
serviceConfig["networks"] = container.Networks
}
// Add service configuration to compose config
if len(container.Volumes) > 0 {
serviceConfig["volumes"] = container.Volumes
}
if len(container.Ports) > 0 {
serviceConfig["ports"] = container.Ports
}
if container.RestartPolicy != "" {
serviceConfig["restart"] = container.RestartPolicy
}
composeConfig["services"].(map[string]interface{})[serviceName] = serviceConfig
}
// Convert to YAML
yamlData, err := yaml.Marshal(&composeConfig)
if err != nil {
return fmt.Errorf("error generating YAML for Docker Compose project %s: %v", stack, err)
}
// Write YAML to a file
err = os.WriteFile(outputFile, yamlData, 0644)
if err != nil {
return fmt.Errorf("error writing to file %s: %v", outputFile, err)