github.com/docker/libcompose@v0.4.1-0.20210616120443-2a046c0bdbf2/config/merge.go (about)

     1  package config
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"reflect"
    11  
    12  	"github.com/docker/docker/pkg/urlutil"
    13  	"github.com/docker/libcompose/utils"
    14  	composeYaml "github.com/docker/libcompose/yaml"
    15  	"github.com/sirupsen/logrus"
    16  	"gopkg.in/yaml.v2"
    17  )
    18  
    19  var (
    20  	noMerge = []string{
    21  		"links",
    22  		"volumes_from",
    23  	}
    24  	defaultParseOptions = ParseOptions{
    25  		Interpolate: true,
    26  		Validate:    true,
    27  	}
    28  )
    29  
    30  func getComposeMajorVersion(version string) (int, error) {
    31  	if version == "" {
    32  		return 1, nil
    33  	}
    34  	parts := strings.Split(version, ".")
    35  	if len(parts) == 1 {
    36  		return strconv.Atoi(version)
    37  	} else if len(parts) == 2 {
    38  		return strconv.Atoi(parts[0])
    39  	} else {
    40  		return -1, fmt.Errorf("Invalid version string, expected single integer or dot delimited int.int. Got: %s", version)
    41  	}
    42  }
    43  
    44  // CreateConfig unmarshals bytes of a YAML manifest file and returns a new
    45  // Config. Initialize any defaults that can't be parsed (but are optional)
    46  // across various file formats. Most of these can remain unused.
    47  //
    48  // This function only handles parsing YAML in the general case. Any other file
    49  // format validation should be handled by the caller.
    50  func CreateConfig(bytes []byte) (*Config, error) {
    51  	var config Config
    52  	if err := yaml.Unmarshal(bytes, &config); err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	major, err := getComposeMajorVersion(config.Version)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  	if major < 2 {
    61  		var baseRawServices RawServiceMap
    62  		if err := yaml.Unmarshal(bytes, &baseRawServices); err != nil {
    63  			return nil, err
    64  		}
    65  		config.Services = baseRawServices
    66  	}
    67  
    68  	if config.Volumes == nil {
    69  		config.Volumes = make(map[string]interface{})
    70  	}
    71  	if config.Networks == nil {
    72  		config.Networks = make(map[string]interface{})
    73  	}
    74  
    75  	return &config, nil
    76  }
    77  
    78  // Merge merges a compose file into an existing set of service configs
    79  func Merge(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte, options *ParseOptions) (string, map[string]*ServiceConfig, map[string]*VolumeConfig, map[string]*NetworkConfig, error) {
    80  	if options == nil {
    81  		options = &defaultParseOptions
    82  	}
    83  
    84  	config, err := CreateConfig(bytes)
    85  	if err != nil {
    86  		return "", nil, nil, nil, err
    87  	}
    88  	baseRawServices := config.Services
    89  
    90  	for service, data := range baseRawServices {
    91  		for key, value := range data {
    92  			//check for "extends" key and check whether it is string or not
    93  			if key == "extends" && reflect.TypeOf(value).Kind() == reflect.String {
    94  				//converting string to map
    95  				extendMap := make(map[interface{}]interface{})
    96  				extendMap["service"] = value
    97  				baseRawServices[service][key] = extendMap
    98  			}
    99  		}
   100  	}
   101  
   102  	if options.Interpolate {
   103  		if err := InterpolateRawServiceMap(&baseRawServices, environmentLookup); err != nil {
   104  			return "", nil, nil, nil, err
   105  		}
   106  
   107  		for k, v := range config.Volumes {
   108  			if err := Interpolate(k, &v, environmentLookup); err != nil {
   109  				return "", nil, nil, nil, err
   110  			}
   111  			config.Volumes[k] = v
   112  		}
   113  
   114  		for k, v := range config.Networks {
   115  			if err := Interpolate(k, &v, environmentLookup); err != nil {
   116  				return "", nil, nil, nil, err
   117  			}
   118  			config.Networks[k] = v
   119  		}
   120  	}
   121  
   122  	if options.Preprocess != nil {
   123  		var err error
   124  		baseRawServices, err = options.Preprocess(baseRawServices)
   125  		if err != nil {
   126  			return "", nil, nil, nil, err
   127  		}
   128  	}
   129  
   130  	major, err := getComposeMajorVersion(config.Version)
   131  	if err != nil {
   132  		return "", nil, nil, nil, err
   133  	}
   134  
   135  	var serviceConfigs map[string]*ServiceConfig
   136  	switch major {
   137  	case 3:
   138  		logrus.Fatal("Note: Compose file version 3 is not yet implemented")
   139  	case 2:
   140  		var err error
   141  		serviceConfigs, err = MergeServicesV2(existingServices, environmentLookup, resourceLookup, file, baseRawServices, options)
   142  		if err != nil {
   143  			return "", nil, nil, nil, err
   144  		}
   145  	default:
   146  		serviceConfigsV1, err := MergeServicesV1(existingServices, environmentLookup, resourceLookup, file, baseRawServices, options)
   147  		if err != nil {
   148  			return "", nil, nil, nil, err
   149  		}
   150  		serviceConfigs, err = ConvertServices(serviceConfigsV1)
   151  		if err != nil {
   152  			return "", nil, nil, nil, err
   153  		}
   154  	}
   155  
   156  	adjustValues(serviceConfigs)
   157  
   158  	if options.Postprocess != nil {
   159  		var err error
   160  		serviceConfigs, err = options.Postprocess(serviceConfigs)
   161  		if err != nil {
   162  			return "", nil, nil, nil, err
   163  		}
   164  	}
   165  
   166  	var volumes map[string]*VolumeConfig
   167  	var networks map[string]*NetworkConfig
   168  	if err := utils.Convert(config.Volumes, &volumes); err != nil {
   169  		return "", nil, nil, nil, err
   170  	}
   171  	if err := utils.Convert(config.Networks, &networks); err != nil {
   172  		return "", nil, nil, nil, err
   173  	}
   174  
   175  	return config.Version, serviceConfigs, volumes, networks, nil
   176  }
   177  
   178  // InterpolateRawServiceMap replaces varialbse in raw service map struct based on environment lookup
   179  func InterpolateRawServiceMap(baseRawServices *RawServiceMap, environmentLookup EnvironmentLookup) error {
   180  	for k, v := range *baseRawServices {
   181  		for k2, v2 := range v {
   182  			if err := Interpolate(k2, &v2, environmentLookup); err != nil {
   183  				return err
   184  			}
   185  			(*baseRawServices)[k][k2] = v2
   186  		}
   187  	}
   188  	return nil
   189  }
   190  
   191  func adjustValues(configs map[string]*ServiceConfig) {
   192  	// yaml parser turns "no" into "false" but that is not valid for a restart policy
   193  	for _, v := range configs {
   194  		if v.Restart == "false" {
   195  			v.Restart = "no"
   196  		}
   197  	}
   198  }
   199  
   200  func readEnvFile(resourceLookup ResourceLookup, inFile string, serviceData RawService) (RawService, error) {
   201  	if _, ok := serviceData["env_file"]; !ok {
   202  		return serviceData, nil
   203  	}
   204  
   205  	var envFiles composeYaml.Stringorslice
   206  
   207  	if err := utils.Convert(serviceData["env_file"], &envFiles); err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	if len(envFiles) == 0 {
   212  		return serviceData, nil
   213  	}
   214  
   215  	if resourceLookup == nil {
   216  		return nil, fmt.Errorf("Can not use env_file in file %s no mechanism provided to load files", inFile)
   217  	}
   218  
   219  	var vars composeYaml.MaporEqualSlice
   220  
   221  	if _, ok := serviceData["environment"]; ok {
   222  		if err := utils.Convert(serviceData["environment"], &vars); err != nil {
   223  			return nil, err
   224  		}
   225  	}
   226  
   227  	for i := len(envFiles) - 1; i >= 0; i-- {
   228  		envFile := envFiles[i]
   229  		content, _, err := resourceLookup.Lookup(envFile, inFile)
   230  		if err != nil {
   231  			return nil, err
   232  		}
   233  
   234  		if err != nil {
   235  			return nil, err
   236  		}
   237  
   238  		scanner := bufio.NewScanner(bytes.NewBuffer(content))
   239  		for scanner.Scan() {
   240  			line := strings.TrimSpace(scanner.Text())
   241  
   242  			if len(line) > 0 && !strings.HasPrefix(line, "#") {
   243  				key := strings.SplitAfter(line, "=")[0]
   244  
   245  				found := false
   246  				for _, v := range vars {
   247  					if strings.HasPrefix(v, key) {
   248  						found = true
   249  						break
   250  					}
   251  				}
   252  
   253  				if !found {
   254  					vars = append(vars, line)
   255  				}
   256  			}
   257  		}
   258  
   259  		if scanner.Err() != nil {
   260  			return nil, scanner.Err()
   261  		}
   262  	}
   263  
   264  	serviceData["environment"] = vars
   265  
   266  	return serviceData, nil
   267  }
   268  
   269  func mergeConfig(baseService, serviceData RawService) RawService {
   270  	for k, v := range serviceData {
   271  		existing, ok := baseService[k]
   272  		if ok {
   273  			baseService[k] = merge(existing, v)
   274  		} else {
   275  			baseService[k] = v
   276  		}
   277  	}
   278  
   279  	return baseService
   280  }
   281  
   282  // IsValidRemote checks if the specified string is a valid remote (for builds)
   283  func IsValidRemote(remote string) bool {
   284  	return urlutil.IsGitURL(remote) || urlutil.IsURL(remote)
   285  }