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 }