1/* 2 Copyright 2020 The Compose Specification Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15*/ 16 17package loader 18 19import ( 20 "fmt" 21 "os" 22 "path/filepath" 23 24 "github.com/compose-spec/compose-go/errdefs" 25 "github.com/compose-spec/compose-go/types" 26 "github.com/pkg/errors" 27 "github.com/sirupsen/logrus" 28) 29 30// normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults 31func normalize(project *types.Project, resolvePaths bool) error { 32 absWorkingDir, err := filepath.Abs(project.WorkingDir) 33 if err != nil { 34 return err 35 } 36 project.WorkingDir = absWorkingDir 37 38 absComposeFiles, err := absComposeFiles(project.ComposeFiles) 39 if err != nil { 40 return err 41 } 42 project.ComposeFiles = absComposeFiles 43 44 if project.Networks == nil { 45 project.Networks = make(map[string]types.NetworkConfig) 46 } 47 48 // If not declared explicitly, Compose model involves an implicit "default" network 49 if _, ok := project.Networks["default"]; !ok { 50 project.Networks["default"] = types.NetworkConfig{} 51 } 52 53 err = relocateExternalName(project) 54 if err != nil { 55 return err 56 } 57 58 for i, s := range project.Services { 59 if len(s.Networks) == 0 && s.NetworkMode == "" { 60 // Service without explicit network attachment are implicitly exposed on default network 61 s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} 62 } 63 64 if s.PullPolicy == types.PullPolicyIfNotPresent { 65 s.PullPolicy = types.PullPolicyMissing 66 } 67 68 fn := func(s string) (string, bool) { 69 v, ok := project.Environment[s] 70 return v, ok 71 } 72 73 if s.Build != nil { 74 if s.Build.Dockerfile == "" { 75 s.Build.Dockerfile = "Dockerfile" 76 } 77 localContext := absPath(project.WorkingDir, s.Build.Context) 78 if _, err := os.Stat(localContext); err == nil { 79 if resolvePaths { 80 s.Build.Context = localContext 81 } 82 } else { 83 // might be a remote http/git context. Unfortunately supported "remote" syntax is highly ambiguous 84 // in moby/moby and not defined by compose-spec, so let's assume runtime will check 85 } 86 s.Build.Args = s.Build.Args.Resolve(fn) 87 } 88 s.Environment = s.Environment.Resolve(fn) 89 90 err := relocateLogDriver(&s) 91 if err != nil { 92 return err 93 } 94 95 err = relocateLogOpt(&s) 96 if err != nil { 97 return err 98 } 99 100 err = relocateDockerfile(&s) 101 if err != nil { 102 return err 103 } 104 105 err = relocateScale(&s) 106 if err != nil { 107 return err 108 } 109 110 project.Services[i] = s 111 } 112 113 setNameFromKey(project) 114 115 return nil 116} 117 118func relocateScale(s *types.ServiceConfig) error { 119 scale := uint64(s.Scale) 120 if scale != 1 { 121 logrus.Warn("`scale` is deprecated. Use the `deploy.replicas` element") 122 if s.Deploy == nil { 123 s.Deploy = &types.DeployConfig{} 124 } 125 if s.Deploy.Replicas != nil && *s.Deploy.Replicas != scale { 126 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'scale' (deprecated) and 'deploy.replicas'") 127 } 128 s.Deploy.Replicas = &scale 129 } 130 return nil 131} 132 133func absComposeFiles(composeFiles []string) ([]string, error) { 134 absComposeFiles := make([]string, len(composeFiles)) 135 for i, composeFile := range composeFiles { 136 absComposefile, err := filepath.Abs(composeFile) 137 if err != nil { 138 return nil, err 139 } 140 absComposeFiles[i] = absComposefile 141 } 142 return absComposeFiles, nil 143} 144 145// Resources with no explicit name are actually named by their key in map 146func setNameFromKey(project *types.Project) { 147 for i, n := range project.Networks { 148 if n.Name == "" { 149 n.Name = fmt.Sprintf("%s_%s", project.Name, i) 150 project.Networks[i] = n 151 } 152 } 153 154 for i, v := range project.Volumes { 155 if v.Name == "" { 156 v.Name = fmt.Sprintf("%s_%s", project.Name, i) 157 project.Volumes[i] = v 158 } 159 } 160 161 for i, c := range project.Configs { 162 if c.Name == "" { 163 c.Name = fmt.Sprintf("%s_%s", project.Name, i) 164 project.Configs[i] = c 165 } 166 } 167 168 for i, s := range project.Secrets { 169 if s.Name == "" { 170 s.Name = fmt.Sprintf("%s_%s", project.Name, i) 171 project.Secrets[i] = s 172 } 173 } 174} 175 176func relocateExternalName(project *types.Project) error { 177 for i, n := range project.Networks { 178 if n.External.Name != "" { 179 if n.Name != "" { 180 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'networks.external.name' (deprecated) and 'networks.name'") 181 } 182 n.Name = n.External.Name 183 } 184 project.Networks[i] = n 185 } 186 187 for i, v := range project.Volumes { 188 if v.External.Name != "" { 189 if v.Name != "" { 190 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'volumes.external.name' (deprecated) and 'volumes.name'") 191 } 192 v.Name = v.External.Name 193 } 194 project.Volumes[i] = v 195 } 196 197 for i, s := range project.Secrets { 198 if s.External.Name != "" { 199 if s.Name != "" { 200 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'secrets.external.name' (deprecated) and 'secrets.name'") 201 } 202 s.Name = s.External.Name 203 } 204 project.Secrets[i] = s 205 } 206 207 for i, c := range project.Configs { 208 if c.External.Name != "" { 209 if c.Name != "" { 210 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'configs.external.name' (deprecated) and 'configs.name'") 211 } 212 c.Name = c.External.Name 213 } 214 project.Configs[i] = c 215 } 216 return nil 217} 218 219func relocateLogOpt(s *types.ServiceConfig) error { 220 if len(s.LogOpt) != 0 { 221 logrus.Warn("`log_opts` is deprecated. Use the `logging` element") 222 if s.Logging == nil { 223 s.Logging = &types.LoggingConfig{} 224 } 225 for k, v := range s.LogOpt { 226 if _, ok := s.Logging.Options[k]; !ok { 227 s.Logging.Options[k] = v 228 } else { 229 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'log_opt' (deprecated) and 'logging.options'") 230 } 231 } 232 } 233 return nil 234} 235 236func relocateLogDriver(s *types.ServiceConfig) error { 237 if s.LogDriver != "" { 238 logrus.Warn("`log_driver` is deprecated. Use the `logging` element") 239 if s.Logging == nil { 240 s.Logging = &types.LoggingConfig{} 241 } 242 if s.Logging.Driver == "" { 243 s.Logging.Driver = s.LogDriver 244 } else { 245 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'log_driver' (deprecated) and 'logging.driver'") 246 } 247 } 248 return nil 249} 250 251func relocateDockerfile(s *types.ServiceConfig) error { 252 if s.Dockerfile != "" { 253 logrus.Warn("`dockerfile` is deprecated. Use the `build` element") 254 if s.Build == nil { 255 s.Build = &types.BuildConfig{} 256 } 257 if s.Dockerfile == "" { 258 s.Build.Dockerfile = s.Dockerfile 259 } else { 260 return errors.Wrap(errdefs.ErrInvalid, "can't use both 'dockerfile' (deprecated) and 'build.dockerfile'") 261 } 262 } 263 return nil 264} 265