github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/compose_yaml.go (about)

     1  package ddevapp
     2  
     3  import (
     4  	"github.com/ddev/ddev/pkg/dockerutil"
     5  	"github.com/ddev/ddev/pkg/util"
     6  	"gopkg.in/yaml.v3"
     7  	"os"
     8  	"strings"
     9  	//compose_cli "github.com/compose-spec/compose-go/cli"
    10  	//compose_types "github.com/compose-spec/compose-go/types"
    11  )
    12  
    13  // WriteDockerComposeYAML writes a .ddev-docker-compose-base.yaml and related to the .ddev directory.
    14  // It then uses `docker-compose convert` to get a canonical version of the full compose file.
    15  // It then makes a couple of fixups to the canonical version (networks and approot bind points) by
    16  // marshaling the canonical version to YAML and then unmarshaling it back into a canonical version.
    17  func (app *DdevApp) WriteDockerComposeYAML() error {
    18  	var err error
    19  
    20  	f, err := os.Create(app.DockerComposeYAMLPath())
    21  	if err != nil {
    22  		return err
    23  	}
    24  	defer util.CheckClose(f)
    25  
    26  	rendered, err := app.RenderComposeYAML()
    27  	if err != nil {
    28  		return err
    29  	}
    30  	_, err = f.WriteString(rendered)
    31  	if err != nil {
    32  		return err
    33  	}
    34  
    35  	files, err := app.ComposeFiles()
    36  	if err != nil {
    37  		return err
    38  	}
    39  	fullContents, _, err := dockerutil.ComposeCmd(&dockerutil.ComposeCmdOpts{
    40  		ComposeFiles: files,
    41  		Action:       []string{"config"},
    42  	})
    43  	if err != nil {
    44  		return err
    45  	}
    46  
    47  	app.ComposeYaml, err = fixupComposeYaml(fullContents, app)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	fullHandle, err := os.Create(app.DockerComposeFullRenderedYAMLPath())
    52  	if err != nil {
    53  		return err
    54  	}
    55  	defer func() {
    56  		err = fullHandle.Close()
    57  		if err != nil {
    58  			util.Warning("Error closing %s: %v", fullHandle.Name(), err)
    59  		}
    60  	}()
    61  	fullContentsBytes, err := yaml.Marshal(app.ComposeYaml)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	_, err = fullHandle.Write(fullContentsBytes)
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	return nil
    72  }
    73  
    74  // fixupComposeYaml makes minor changes to the `docker-compose config` output
    75  // to make sure extra services are always compatible with ddev.
    76  func fixupComposeYaml(yamlStr string, app *DdevApp) (map[string]interface{}, error) {
    77  	tempMap := make(map[string]interface{})
    78  	err := yaml.Unmarshal([]byte(yamlStr), &tempMap)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	// Find any services that have bind-mount to AppRoot and make them relative
    84  	// for https://youtrack.jetbrains.com/issue/WI-61976 - PhpStorm
    85  	// This is an ugly an shortsighted approach, but otherwise we'd have to parse the yaml.
    86  	// Note that this issue with docker-compose config was fixed in docker-compose 2.0.0RC4
    87  	// so it's in Docker Desktop 4.1.0.
    88  	// https://github.com/docker/compose/issues/8503#issuecomment-930969241
    89  
    90  	for _, service := range tempMap["services"].(map[string]interface{}) {
    91  		if service == nil {
    92  			continue
    93  		}
    94  		serviceMap := service.(map[string]interface{})
    95  
    96  		// Find any services that have bind-mount to app.AppRoot and make them relative
    97  		if serviceMap["volumes"] != nil {
    98  			volumes := serviceMap["volumes"].([]interface{})
    99  			for k, volume := range volumes {
   100  				// With docker-compose v1, the volume might not be a map, it might be
   101  				// old-style "/Users/rfay/workspace/d9/.ddev:/mnt/ddev_config:ro"
   102  				if volumeMap, ok := volume.(map[string]interface{}); ok {
   103  					if volumeMap["source"] != nil {
   104  						if volumeMap["source"].(string) == app.AppRoot {
   105  							volumeMap["source"] = "../"
   106  						}
   107  					}
   108  				} else if volumeMap, ok := volume.(string); ok {
   109  					parts := strings.SplitN(volumeMap, ":", 2)
   110  					if parts[0] == app.AppRoot && len(parts) >= 2 {
   111  						volumes[k] = "../" + parts[1]
   112  					}
   113  				}
   114  			}
   115  		}
   116  		// Make sure all services have our networks stanza
   117  		serviceMap["networks"] = map[string]interface{}{
   118  			"ddev_default": nil,
   119  			"default":      nil,
   120  		}
   121  	}
   122  
   123  	return tempMap, nil
   124  }