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.

82
main.go
View File

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