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