diff --git a/dist/go-compose-exporter_darwin_amd64 b/dist/go-compose-exporter_darwin_amd64 index 69add98..fc174e3 100755 Binary files a/dist/go-compose-exporter_darwin_amd64 and b/dist/go-compose-exporter_darwin_amd64 differ diff --git a/dist/go-compose-exporter_darwin_arm64 b/dist/go-compose-exporter_darwin_arm64 index 5e93039..4b02c2c 100755 Binary files a/dist/go-compose-exporter_darwin_arm64 and b/dist/go-compose-exporter_darwin_arm64 differ diff --git a/dist/go-compose-exporter_linux_amd64 b/dist/go-compose-exporter_linux_amd64 index 6178532..1108bf3 100755 Binary files a/dist/go-compose-exporter_linux_amd64 and b/dist/go-compose-exporter_linux_amd64 differ diff --git a/dist/go-compose-exporter_linux_amd64_static b/dist/go-compose-exporter_linux_amd64_static index 4d0923e..4bd77b0 100755 Binary files a/dist/go-compose-exporter_linux_amd64_static and b/dist/go-compose-exporter_linux_amd64_static differ diff --git a/dist/go-compose-exporter_linux_arm64 b/dist/go-compose-exporter_linux_arm64 index 5f4e509..e92f7d7 100755 Binary files a/dist/go-compose-exporter_linux_arm64 and b/dist/go-compose-exporter_linux_arm64 differ diff --git a/dist/go-compose-exporter_linux_arm64_static b/dist/go-compose-exporter_linux_arm64_static index b1a285a..7d67cdb 100755 Binary files a/dist/go-compose-exporter_linux_arm64_static and b/dist/go-compose-exporter_linux_arm64_static differ diff --git a/dist/go-compose-exporter_windows_amd64 b/dist/go-compose-exporter_windows_amd64 index bf82ecb..1c6cc52 100755 Binary files a/dist/go-compose-exporter_windows_amd64 and b/dist/go-compose-exporter_windows_amd64 differ diff --git a/main.go b/main.go index e8d62e8..25354d3 100644 --- a/main.go +++ b/main.go @@ -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)