diff --git a/dist/go-compose-exporter_darwin_amd64 b/dist/go-compose-exporter_darwin_amd64 index 284768c..27a4fd7 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 1f1db1c..ab7219e 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 7f32650..469dd12 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 3793e7e..9b086e6 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 65af2b3..7ec77a3 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 09bf2e9..9f04728 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 7384d71..fa87d1c 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 877b365..a517f8c 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,8 @@ func main() { 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 @@ -61,12 +63,24 @@ func main() { log.Fatalf("Stack or project '%s' not found.", *stackName) } - // Export the specified stack/project to a docker-compose.yml file - err = exportStackToComposeFile(*stackName, containers, *outputFile) - if err != nil { - log.Fatalf("Error exporting stack %s: %v", *stackName, err) + // Determine if this is a Docker Swarm stack or a Docker Compose project + isSwarm := isDockerSwarmStack(*stackName, containers) + + if isSwarm { + // Handle Docker Swarm stack export + err = exportSwarmStackToComposeFile(*stackName, *outputFile) + if err != nil { + log.Fatalf("Error exporting Docker Swarm stack %s: %v", *stackName, err) + } + fmt.Printf("docker-compose.yml generated successfully for Docker Swarm stack '%s' in %s\n", *stackName, *outputFile) + } 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) + } + fmt.Printf("docker-compose.yml generated successfully for Docker Compose project '%s' in %s\n", *stackName, *outputFile) } - fmt.Printf("docker-compose.yml generated successfully for stack '%s' in %s\n", *stackName, *outputFile) } // Container represents basic Docker container information. @@ -95,8 +109,8 @@ func listContainers() ([]Container, error) { for scanner.Scan() { fields := strings.Fields(scanner.Text()) - // Check that there are at least 5 fields before accessing them - if len(fields) < 5 { + // 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 } @@ -104,9 +118,20 @@ func listContainers() ([]Container, error) { id := fields[0] name := fields[1] image := fields[2] + + // Handle cases where project or stackNamespace might be missing + project := "" + stackNamespace := "" + if len(fields) > 3 { + project = fields[3] + } + if len(fields) > 4 { + stackNamespace = fields[4] + } + labels := map[string]string{ - "com.docker.compose.project": fields[3], - "com.docker.stack.namespace": fields[4], + "com.docker.compose.project": project, + "com.docker.stack.namespace": stackNamespace, } // Run 'docker inspect' to get detailed container information. @@ -139,15 +164,89 @@ func inspectContainer(containerID string) ([]string, []string, error) { return 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]) return envVars, networks, nil } -// exportStackToComposeFile generates a docker-compose.yml for a given stack or project. -func exportStackToComposeFile(stack string, containers []Container, outputFile string) error { +// 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, ".") { + return true + } + } + return false +} + +// exportSwarmStackToComposeFile exports a Docker Swarm stack to a docker-compose.yml file. +func exportSwarmStackToComposeFile(stackName string, outputFile string) error { + // Fetch stack details using `docker stack services` and construct the docker-compose.yml + cmd := exec.Command("docker", "stack", "services", stackName, "--format", "{{.Name}} {{.Image}} {{.Ports}} {{.Replicas}}") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return fmt.Errorf("error fetching Docker Swarm stack services: %v", err) + } + + composeConfig := make(map[string]interface{}) + composeConfig["services"] = make(map[string]interface{}) + + scanner := bufio.NewScanner(&out) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) < 2 { + continue + } + + serviceName := fields[0] + image := fields[1] + + serviceConfig := map[string]interface{}{ + "image": image, + } + + // Handle additional fields like ports and replicas + if len(fields) > 2 { + // Parse ports and replicas if available + ports := fields[2] + replicas := fields[3] + serviceConfig["deploy"] = map[string]interface{}{ + "replicas": replicas, + } + if ports != "" { + serviceConfig["ports"] = []string{ports} + } + } + + 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 Swarm stack %s: %v", stackName, 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) + } + + return nil +} + +// 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{}) @@ -179,7 +278,7 @@ func exportStackToComposeFile(stack string, containers []Container, outputFile s // Convert to YAML yamlData, err := yaml.Marshal(&composeConfig) if err != nil { - return fmt.Errorf("error generating YAML for stack %s: %v", stack, err) + return fmt.Errorf("error generating YAML for Docker Compose project %s: %v", stack, err) } // Write YAML to a file