• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes / kompose / 15023842747

14 May 2025 02:46PM UTC coverage: 48.476% (+0.4%) from 48.055%
15023842747

Pull #2008

github

yuefanxiao
Add test cases for loading .env file
Pull Request #2008: fix: Load variables from .env file by default

0 of 2 new or added lines in 1 file covered. (0.0%)

414 existing lines in 3 files now uncovered.

2608 of 5380 relevant lines covered (48.48%)

7.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.21
/pkg/loader/compose/compose.go
1
/*
2
Copyright 2017 The Kubernetes Authors All rights reserved.
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

17
package compose
18

19
import (
20
        "context"
21
        "fmt"
22
        "os"
23
        "reflect"
24
        "strconv"
25
        "strings"
26
        "time"
27

28
        "github.com/compose-spec/compose-go/v2/cli"
29
        "github.com/compose-spec/compose-go/v2/types"
30
        "github.com/fatih/structs"
31
        "github.com/google/shlex"
32
        "github.com/kubernetes/kompose/pkg/kobject"
33
        "github.com/kubernetes/kompose/pkg/transformer"
34
        "github.com/pkg/errors"
35
        log "github.com/sirupsen/logrus"
36
        "github.com/spf13/cast"
37
        batchv1 "k8s.io/api/batch/v1"
38
        api "k8s.io/api/core/v1"
39
)
40

41
// StdinData is data bytes read from stdin
42
var StdinData []byte
43

44
// Compose is docker compose file loader, implements Loader interface
45
type Compose struct {
46
}
47

48
// checkUnsupportedKey checks if compose-go project contains
49
// keys that are not supported by this loader.
50
// list of all unsupported keys are stored in unsupportedKey variable
51
// returns list of unsupported YAML keys from docker-compose
52
func checkUnsupportedKey(composeProject *types.Project) []string {
2✔
53
        // list of all unsupported keys for this loader
2✔
54
        // this is map to make searching for keys easier
2✔
55
        // to make sure that unsupported key is not going to be reported twice
2✔
56
        // by keeping record if already saw this key in another service
2✔
57
        var unsupportedKey = map[string]bool{
2✔
58
                "CgroupParent":  false,
2✔
59
                "CPUSet":        false,
2✔
60
                "CPUShares":     false,
2✔
61
                "Devices":       false,
2✔
62
                "DependsOn":     false,
2✔
63
                "DNS":           false,
2✔
64
                "DNSSearch":     false,
2✔
65
                "EnvFile":       false,
2✔
66
                "ExternalLinks": false,
2✔
67
                "ExtraHosts":    false,
2✔
68
                "Ipc":           false,
2✔
69
                "Logging":       false,
2✔
70
                "MacAddress":    false,
2✔
71
                "MemSwapLimit":  false,
2✔
72
                "NetworkMode":   false,
2✔
73
                "SecurityOpt":   false,
2✔
74
                "ShmSize":       false,
2✔
75
                "StopSignal":    false,
2✔
76
                "VolumeDriver":  false,
2✔
77
                "Uts":           false,
2✔
78
                "ReadOnly":      false,
2✔
79
                "Ulimits":       false,
2✔
80
                "Net":           false,
2✔
81
                "Sysctls":       false,
2✔
82
                //"Networks":    false, // We shall be spporting network now. There are special checks for Network in checkUnsupportedKey function
2✔
83
                "Links": false,
2✔
84
        }
2✔
85

2✔
86
        var keysFound []string
2✔
87

2✔
88
        // Root level keys are not yet supported except Network
2✔
89
        // Check to see if the default network is available and length is only equal to one.
2✔
90
        if _, ok := composeProject.Networks["default"]; ok && len(composeProject.Networks) == 1 {
2✔
91
                log.Debug("Default network found")
×
92
        }
×
93

94
        // Root level volumes are not yet supported
95
        if len(composeProject.Volumes) > 0 {
3✔
96
                keysFound = append(keysFound, "root level volumes")
1✔
97
        }
1✔
98

99
        for _, serviceConfig := range composeProject.AllServices() {
5✔
100
                // this reflection is used in check for empty arrays
3✔
101
                val := reflect.ValueOf(serviceConfig)
3✔
102
                s := structs.New(serviceConfig)
3✔
103

3✔
104
                for _, f := range s.Fields() {
288✔
105
                        // Check if given key is among unsupported keys, and skip it if we already saw this key
285✔
106
                        if alreadySaw, ok := unsupportedKey[f.Name()]; ok && !alreadySaw {
357✔
107
                                if f.IsExported() && !f.IsZero() {
72✔
108
                                        // IsZero returns false for empty array/slice ([])
×
109
                                        // this check if field is Slice, and then it checks its size
×
110
                                        if field := val.FieldByName(f.Name()); field.Kind() == reflect.Slice {
×
111
                                                if field.Len() == 0 {
×
112
                                                        // array is empty it doesn't matter if it is in unsupportedKey or not
×
113
                                                        continue
×
114
                                                }
115
                                        }
116
                                        //get yaml tag name instead of variable name
117
                                        yamlTagName := strings.Split(f.Tag("yaml"), ",")[0]
×
118
                                        if f.Name() == "Networks" {
×
119
                                                // networks always contains one default element, even it isn't declared in compose v2.
×
120
                                                if len(serviceConfig.Networks) == 1 && serviceConfig.NetworksByPriority()[0] == "default" {
×
121
                                                        // this is empty Network definition, skip it
×
122
                                                        continue
×
123
                                                }
124
                                        }
125

126
                                        if linksArray := val.FieldByName(f.Name()); f.Name() == "Links" && linksArray.Kind() == reflect.Slice {
×
127
                                                //Links has "SERVICE:ALIAS" style, we don't support SERVICE != ALIAS
×
128
                                                findUnsupportedLinksFlag := false
×
129
                                                for i := 0; i < linksArray.Len(); i++ {
×
130
                                                        if tmpLink := linksArray.Index(i); tmpLink.Kind() == reflect.String {
×
131
                                                                tmpLinkStr := tmpLink.String()
×
132
                                                                tmpLinkStrSplit := strings.Split(tmpLinkStr, ":")
×
133
                                                                if len(tmpLinkStrSplit) == 2 && tmpLinkStrSplit[0] != tmpLinkStrSplit[1] {
×
134
                                                                        findUnsupportedLinksFlag = true
×
135
                                                                        break
×
136
                                                                }
137
                                                        }
138
                                                }
139
                                                if !findUnsupportedLinksFlag {
×
140
                                                        continue
×
141
                                                }
142
                                        }
143

144
                                        keysFound = append(keysFound, yamlTagName)
×
145
                                        unsupportedKey[f.Name()] = true
×
146
                                }
147
                        }
148
                }
149
        }
150
        return keysFound
2✔
151
}
152

153
// LoadFile loads a compose file into KomposeObject
154
func (c *Compose) LoadFile(files []string, profiles []string, noInterpolate bool) (kobject.KomposeObject, error) {
×
155
        // Gather the working directory
×
156
        workingDir, err := transformer.GetComposeFileDir(files)
×
157
        if err != nil {
×
158
                return kobject.KomposeObject{}, err
×
159
        }
×
160

161
        projectOptions, err := cli.NewProjectOptions(
×
162
                files, cli.WithOsEnv,
×
163
                cli.WithWorkingDirectory(workingDir),
×
164
                cli.WithInterpolation(!noInterpolate),
×
165
                cli.WithProfiles(profiles),
×
NEW
166
                cli.WithEnvFiles([]string{}...),
×
NEW
167
                cli.WithDotEnv,
×
168
        )
×
169
        if err != nil {
×
170
                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to create compose options")
×
171
        }
×
172

173
        project, err := cli.ProjectFromOptions(context.Background(), projectOptions)
×
174
        if err != nil {
×
175
                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to load files")
×
176
        }
×
177

178
        // Finding 0 services means two things:
179
        // 1. The compose project is empty
180
        // 2. The profile that is configured in the compose project is different than the one defined in Kompose convert options
181
        // In both cases we should provide the user with a warning indicating that we didn't find any service.
182
        if len(project.Services) == 0 {
×
183
                log.Warning("No service selected. The profile specified in services of your compose yaml may not exist.")
×
184
        }
×
185

186
        komposeObject, err := dockerComposeToKomposeMapping(project)
×
187
        if err != nil {
×
188
                return kobject.KomposeObject{}, err
×
189
        }
×
190
        return komposeObject, nil
×
191
}
192

193
func loadPlacement(placement types.Placement) kobject.Placement {
1✔
194
        komposePlacement := kobject.Placement{
1✔
195
                PositiveConstraints: make(map[string]string),
1✔
196
                NegativeConstraints: make(map[string]string),
1✔
197
                Preferences:         make([]string, 0, len(placement.Preferences)),
1✔
198
        }
1✔
199

1✔
200
        // Convert constraints
1✔
201
        equal, notEqual := " == ", " != "
1✔
202
        for _, j := range placement.Constraints {
3✔
203
                operator := equal
2✔
204
                if strings.Contains(j, notEqual) {
3✔
205
                        operator = notEqual
1✔
206
                }
1✔
207
                p := strings.Split(j, operator)
2✔
208
                if len(p) < 2 {
2✔
209
                        log.Warnf("Failed to parse placement constraints %s, the correct format is 'label == xxx'", j)
×
210
                        continue
×
211
                }
212

213
                key, err := convertDockerLabel(p[0])
2✔
214
                if err != nil {
2✔
215
                        log.Warn("Ignore placement constraints: ", err.Error())
×
216
                        continue
×
217
                }
218

219
                if operator == equal {
3✔
220
                        komposePlacement.PositiveConstraints[key] = p[1]
1✔
221
                } else if operator == notEqual {
3✔
222
                        komposePlacement.NegativeConstraints[key] = p[1]
1✔
223
                }
1✔
224
        }
225

226
        // Convert preferences
227
        for _, p := range placement.Preferences {
4✔
228
                // Spread is the only supported strategy currently
3✔
229
                label, err := convertDockerLabel(p.Spread)
3✔
230
                if err != nil {
4✔
231
                        log.Warn("Ignore placement preferences: ", err.Error())
1✔
232
                        continue
1✔
233
                }
234
                komposePlacement.Preferences = append(komposePlacement.Preferences, label)
2✔
235
        }
236
        return komposePlacement
1✔
237
}
238

239
// Convert docker label to k8s label
240
func convertDockerLabel(dockerLabel string) (string, error) {
5✔
241
        switch dockerLabel {
5✔
242
        case "node.hostname":
×
243
                return "kubernetes.io/hostname", nil
×
244
        case "engine.labels.operatingsystem":
×
245
                return "kubernetes.io/os", nil
×
246
        default:
5✔
247
                if strings.HasPrefix(dockerLabel, "node.labels.") {
9✔
248
                        return strings.TrimPrefix(dockerLabel, "node.labels."), nil
4✔
249
                }
4✔
250
        }
251
        errMsg := fmt.Sprint(dockerLabel, " is not supported, only 'node.hostname', 'engine.labels.operatingsystem' and 'node.labels.xxx' (ex: node.labels.something == anything) is supported")
1✔
252
        return "", errors.New(errMsg)
1✔
253
}
254

255
// Convert the Compose volumes to []string (the old way)
256
// TODO: Check to see if it's a "bind" or "volume". Ignore for now.
257
// TODO: Refactor it similar to loadPorts
258
// See: https://docs.docker.com/compose/compose-file/#long-syntax-3
259
func loadVolumes(volumes []types.ServiceVolumeConfig) []string {
1✔
260
        var volArray []string
1✔
261
        for _, vol := range volumes {
2✔
262
                // There will *always* be Source when parsing
1✔
263
                v := vol.Source
1✔
264

1✔
265
                if vol.Target != "" {
2✔
266
                        v = v + ":" + vol.Target
1✔
267
                }
1✔
268

269
                if vol.ReadOnly {
2✔
270
                        v = v + ":ro"
1✔
271
                }
1✔
272

273
                volArray = append(volArray, v)
1✔
274
        }
275
        return volArray
1✔
276
}
277

278
// Convert Compose ports to kobject.Ports
279
// expose ports will be treated as TCP ports
280
func loadPorts(ports []types.ServicePortConfig, expose []string) []kobject.Ports {
11✔
281
        komposePorts := []kobject.Ports{}
11✔
282
        exist := map[string]bool{}
11✔
283

11✔
284
        for _, port := range ports {
30✔
285
                // Convert to a kobject struct with ports
19✔
286
                komposePorts = append(komposePorts, kobject.Ports{
19✔
287
                        HostPort:      cast.ToInt32(port.Published),
19✔
288
                        ContainerPort: int32(port.Target),
19✔
289
                        HostIP:        port.HostIP,
19✔
290
                        Protocol:      strings.ToUpper(port.Protocol),
19✔
291
                })
19✔
292
                exist[cast.ToString(port.Target)+port.Protocol] = true
19✔
293
        }
19✔
294

295
        for _, port := range expose {
16✔
296
                portValue := port
5✔
297
                protocol := string(api.ProtocolTCP)
5✔
298
                if strings.Contains(portValue, "/") {
6✔
299
                        splits := strings.Split(port, "/")
1✔
300
                        portValue = splits[0]
1✔
301
                        protocol = splits[1]
1✔
302
                }
1✔
303

304
                if exist[portValue+protocol] {
6✔
305
                        continue
1✔
306
                }
307
                komposePorts = append(komposePorts, kobject.Ports{
4✔
308
                        ContainerPort: cast.ToInt32(portValue),
4✔
309
                        HostIP:        "",
4✔
310
                        Protocol:      strings.ToUpper(protocol),
4✔
311
                })
4✔
312
        }
313

314
        return komposePorts
11✔
315
}
316

317
/*
318
        Convert the HealthCheckConfig as designed by Docker to
319

320
a Kubernetes-compatible format.
321
*/
322
func parseHealthCheckReadiness(labels types.Labels) (kobject.HealthCheck, error) {
3✔
323
        var test []string
3✔
324
        var httpPath string
3✔
325
        var httpPort, tcpPort, timeout, interval, retries, startPeriod int32
3✔
326
        var disable bool
3✔
327

3✔
328
        for key, value := range labels {
19✔
329
                switch key {
16✔
330
                case HealthCheckReadinessDisable:
×
331
                        disable = cast.ToBool(value)
×
332
                case HealthCheckReadinessTest:
1✔
333
                        if len(value) > 0 {
2✔
334
                                test, _ = shlex.Split(value)
1✔
335
                        }
1✔
336
                case HealthCheckReadinessHTTPGetPath:
1✔
337
                        httpPath = value
1✔
338
                case HealthCheckReadinessHTTPGetPort:
1✔
339
                        httpPort = cast.ToInt32(value)
1✔
340
                case HealthCheckReadinessTCPPort:
1✔
341
                        tcpPort = cast.ToInt32(value)
1✔
342
                case HealthCheckReadinessInterval:
3✔
343
                        parse, err := time.ParseDuration(value)
3✔
344
                        if err != nil {
3✔
345
                                return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check interval variable")
×
346
                        }
×
347
                        interval = int32(parse.Seconds())
3✔
348
                case HealthCheckReadinessTimeout:
3✔
349
                        parse, err := time.ParseDuration(value)
3✔
350
                        if err != nil {
3✔
351
                                return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check timeout variable")
×
352
                        }
×
353
                        timeout = int32(parse.Seconds())
3✔
354
                case HealthCheckReadinessRetries:
3✔
355
                        retries = cast.ToInt32(value)
3✔
356
                case HealthCheckReadinessStartPeriod:
3✔
357
                        parse, err := time.ParseDuration(value)
3✔
358
                        if err != nil {
3✔
359
                                return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check startPeriod variable")
×
360
                        }
×
361
                        startPeriod = int32(parse.Seconds())
3✔
362
                }
363
        }
364

365
        if len(test) > 0 {
4✔
366
                if test[0] == "NONE" {
1✔
367
                        disable = true
×
368
                        test = test[1:]
×
369
                }
×
370
                // Due to docker/cli adding "CMD-SHELL" to the struct, we remove the first element of composeHealthCheck.Test
371
                if test[0] == "CMD" || test[0] == "CMD-SHELL" {
1✔
372
                        test = test[1:]
×
373
                }
×
374
        }
375

376
        return kobject.HealthCheck{
3✔
377
                Test:        test,
3✔
378
                HTTPPath:    httpPath,
3✔
379
                HTTPPort:    httpPort,
3✔
380
                TCPPort:     tcpPort,
3✔
381
                Timeout:     timeout,
3✔
382
                Interval:    interval,
3✔
383
                Retries:     retries,
3✔
384
                StartPeriod: startPeriod,
3✔
385
                Disable:     disable,
3✔
386
        }, nil
3✔
387
}
388

389
/*
390
        Convert the HealthCheckConfig as designed by Docker to
391

392
a Kubernetes-compatible format.
393
*/
394
func parseHealthCheck(composeHealthCheck types.HealthCheckConfig, labels types.Labels) (kobject.HealthCheck, error) {
3✔
395
        var httpPort, tcpPort, timeout, interval, retries, startPeriod int32
3✔
396
        var test []string
3✔
397
        var httpPath string
3✔
398

3✔
399
        // Here we convert the timeout from 1h30s (example) to 36030 seconds.
3✔
400
        if composeHealthCheck.Timeout != nil {
6✔
401
                parse, err := time.ParseDuration(composeHealthCheck.Timeout.String())
3✔
402
                if err != nil {
3✔
403
                        return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check timeout variable")
×
404
                }
×
405
                timeout = int32(parse.Seconds())
3✔
406
        }
407

408
        if composeHealthCheck.Interval != nil {
6✔
409
                parse, err := time.ParseDuration(composeHealthCheck.Interval.String())
3✔
410
                if err != nil {
3✔
411
                        return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check interval variable")
×
412
                }
×
413
                interval = int32(parse.Seconds())
3✔
414
        }
415

416
        if composeHealthCheck.Retries != nil {
6✔
417
                retries = int32(*composeHealthCheck.Retries)
3✔
418
        }
3✔
419

420
        if composeHealthCheck.StartPeriod != nil {
6✔
421
                parse, err := time.ParseDuration(composeHealthCheck.StartPeriod.String())
3✔
422
                if err != nil {
3✔
423
                        return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check startPeriod variable")
×
424
                }
×
425
                startPeriod = int32(parse.Seconds())
3✔
426
        }
427

428
        if composeHealthCheck.Test != nil {
4✔
429
                test = composeHealthCheck.Test[1:]
1✔
430
        }
1✔
431

432
        for key, value := range labels {
6✔
433
                switch key {
3✔
434
                case HealthCheckLivenessHTTPGetPath:
1✔
435
                        httpPath = value
1✔
436
                case HealthCheckLivenessHTTPGetPort:
1✔
437
                        httpPort = cast.ToInt32(value)
1✔
438
                case HealthCheckLivenessTCPPort:
1✔
439
                        tcpPort = cast.ToInt32(value)
1✔
440
                }
441
        }
442

443
        // Due to docker/cli adding "CMD-SHELL" to the struct, we remove the first element of composeHealthCheck.Test
444
        return kobject.HealthCheck{
3✔
445
                Test:        test,
3✔
446
                TCPPort:     tcpPort,
3✔
447
                HTTPPath:    httpPath,
3✔
448
                HTTPPort:    httpPort,
3✔
449
                Timeout:     timeout,
3✔
450
                Interval:    interval,
3✔
451
                Retries:     retries,
3✔
452
                StartPeriod: startPeriod,
3✔
453
        }, nil
3✔
454
}
455

456
func dockerComposeToKomposeMapping(composeObject *types.Project) (kobject.KomposeObject, error) {
×
457
        // Step 1. Initialize what's going to be returned
×
458
        komposeObject := kobject.KomposeObject{
×
459
                ServiceConfigs: make(map[string]kobject.ServiceConfig),
×
460
                LoadedFrom:     "compose",
×
461
                Secrets:        composeObject.Secrets,
×
462
        }
×
463

×
464
        // Step 2. Parse through the object and convert it to kobject.KomposeObject!
×
465
        // Here we "clean up" the service configuration so we return something that includes
×
466
        // all relevant information as well as avoid the unsupported keys as well.
×
467
        for _, composeServiceConfig := range composeObject.Services {
×
468
                // Standard import
×
469
                // No need to modify before importation
×
470
                name := parseResourceName(composeServiceConfig.Name, composeServiceConfig.Labels)
×
471
                serviceConfig := kobject.ServiceConfig{}
×
472
                serviceConfig.Name = name
×
473
                serviceConfig.Image = composeServiceConfig.Image
×
474
                serviceConfig.WorkingDir = composeServiceConfig.WorkingDir
×
475
                serviceConfig.Annotations = composeServiceConfig.Labels
×
476
                serviceConfig.CapAdd = composeServiceConfig.CapAdd
×
477
                serviceConfig.CapDrop = composeServiceConfig.CapDrop
×
478
                serviceConfig.Expose = composeServiceConfig.Expose
×
479
                serviceConfig.Privileged = composeServiceConfig.Privileged
×
480
                serviceConfig.User = composeServiceConfig.User
×
481
                serviceConfig.ReadOnly = composeServiceConfig.ReadOnly
×
482
                serviceConfig.Stdin = composeServiceConfig.StdinOpen
×
483
                serviceConfig.Tty = composeServiceConfig.Tty
×
484
                serviceConfig.TmpFs = composeServiceConfig.Tmpfs
×
485
                serviceConfig.ContainerName = normalizeContainerNames(composeServiceConfig.ContainerName)
×
486
                serviceConfig.Command = composeServiceConfig.Entrypoint
×
487
                serviceConfig.Args = composeServiceConfig.Command
×
488
                serviceConfig.Labels = composeServiceConfig.Labels
×
489
                serviceConfig.HostName = composeServiceConfig.Hostname
×
490
                serviceConfig.DomainName = composeServiceConfig.DomainName
×
491
                serviceConfig.Secrets = composeServiceConfig.Secrets
×
492
                serviceConfig.NetworkMode = composeServiceConfig.NetworkMode
×
493

×
494
                if composeServiceConfig.StopGracePeriod != nil {
×
495
                        serviceConfig.StopGracePeriod = composeServiceConfig.StopGracePeriod.String()
×
496
                }
×
497

498
                if err := parseNetwork(&composeServiceConfig, &serviceConfig, composeObject); err != nil {
×
499
                        return kobject.KomposeObject{}, err
×
500
                }
×
501

502
                if err := parseResources(&composeServiceConfig, &serviceConfig); err != nil {
×
503
                        return kobject.KomposeObject{}, err
×
504
                }
×
505

506
                serviceConfig.Restart = composeServiceConfig.Restart
×
507

×
508
                if composeServiceConfig.Deploy != nil {
×
509
                        // Deploy keys
×
510
                        // mode:
×
511
                        serviceConfig.DeployMode = composeServiceConfig.Deploy.Mode
×
512
                        // labels
×
513
                        serviceConfig.DeployLabels = composeServiceConfig.Deploy.Labels
×
514

×
515
                        // restart-policy: deploy.restart_policy.condition will rewrite restart option
×
516
                        // see: https://docs.docker.com/compose/compose-file/#restart_policy
×
517
                        if composeServiceConfig.Deploy.RestartPolicy != nil {
×
518
                                serviceConfig.Restart = composeServiceConfig.Deploy.RestartPolicy.Condition
×
519
                        }
×
520

521
                        // replicas:
522
                        if composeServiceConfig.Deploy.Replicas != nil {
×
523
                                serviceConfig.Replicas = int(*composeServiceConfig.Deploy.Replicas)
×
524
                        }
×
525

526
                        // placement:
527
                        serviceConfig.Placement = loadPlacement(composeServiceConfig.Deploy.Placement)
×
528

×
529
                        if composeServiceConfig.Deploy.UpdateConfig != nil {
×
530
                                serviceConfig.DeployUpdateConfig = *composeServiceConfig.Deploy.UpdateConfig
×
531
                        }
×
532

533
                        if composeServiceConfig.Deploy.EndpointMode == "vip" {
×
534
                                serviceConfig.ServiceType = string(api.ServiceTypeNodePort)
×
535
                        }
×
536
                }
537

538
                // HealthCheck Liveness
539
                if composeServiceConfig.HealthCheck != nil && !composeServiceConfig.HealthCheck.Disable {
×
540
                        var err error
×
541
                        serviceConfig.HealthChecks.Liveness, err = parseHealthCheck(*composeServiceConfig.HealthCheck, composeServiceConfig.Labels)
×
542
                        if err != nil {
×
543
                                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to parse health check")
×
544
                        }
×
545
                }
546

547
                // HealthCheck Readiness
548
                var readiness, errReadiness = parseHealthCheckReadiness(composeServiceConfig.Labels)
×
549
                if !readiness.Disable {
×
550
                        serviceConfig.HealthChecks.Readiness = readiness
×
551
                        if errReadiness != nil {
×
552
                                return kobject.KomposeObject{}, errors.Wrap(errReadiness, "Unable to parse health check")
×
553
                        }
×
554
                }
555

556
                if serviceConfig.Restart == "unless-stopped" {
×
557
                        log.Warnf("Restart policy 'unless-stopped' in service %s is not supported, convert it to 'always'", name)
×
558
                        serviceConfig.Restart = "always"
×
559
                }
×
560

561
                if composeServiceConfig.Build != nil {
×
562
                        serviceConfig.Build = composeServiceConfig.Build.Context
×
563
                        serviceConfig.Dockerfile = composeServiceConfig.Build.Dockerfile
×
564
                        serviceConfig.BuildArgs = composeServiceConfig.Build.Args
×
565
                        serviceConfig.BuildLabels = composeServiceConfig.Build.Labels
×
566
                        serviceConfig.BuildTarget = composeServiceConfig.Build.Target
×
567
                }
×
568

569
                // env
570
                parseEnvironment(&composeServiceConfig, &serviceConfig)
×
571

×
572
                // Get env_file
×
573
                parseEnvFiles(&composeServiceConfig, &serviceConfig)
×
574

×
575
                // Parse the ports
×
576
                // v3 uses a new format called "long syntax" starting in 3.2
×
577
                // https://docs.docker.com/compose/compose-file/#ports
×
578

×
579
                // here we will translate `expose` too, they basically means the same thing in kubernetes
×
580
                serviceConfig.Port = loadPorts(composeServiceConfig.Ports, serviceConfig.Expose)
×
581

×
582
                // Parse the volumes
×
583
                // Again, in v3, we use the "long syntax" for volumes in terms of parsing
×
584
                // https://docs.docker.com/compose/compose-file/#long-syntax-3
×
585
                serviceConfig.VolList = loadVolumes(composeServiceConfig.Volumes)
×
586
                if err := parseKomposeLabels(composeServiceConfig.Labels, &serviceConfig); err != nil {
×
587
                        return kobject.KomposeObject{}, err
×
588
                }
×
589

590
                // Log if the name will been changed
591
                if normalizeServiceNames(name) != name {
×
592
                        log.Infof("Service name in docker-compose has been changed from %q to %q", name, normalizeServiceNames(name))
×
593
                }
×
594

595
                serviceConfig.Configs = composeServiceConfig.Configs
×
596
                serviceConfig.ConfigsMetaData = composeObject.Configs
×
597

×
598
                // Get GroupAdd, group should be mentioned in gid format but not the group name
×
599
                groupAdd, err := getGroupAdd(composeServiceConfig.GroupAdd)
×
600
                if err != nil {
×
601
                        return kobject.KomposeObject{}, errors.Wrap(err, "GroupAdd should be mentioned in gid format, not a group name")
×
602
                }
×
603
                serviceConfig.GroupAdd = groupAdd
×
604

×
605
                // Final step, add to the array!
×
606
                komposeObject.ServiceConfigs[normalizeServiceNames(name)] = serviceConfig
×
607
        }
608

609
        handleVolume(&komposeObject, &composeObject.Volumes)
×
610
        return komposeObject, nil
×
611
}
612

613
func parseNetwork(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig, composeObject *types.Project) error {
×
614
        if len(composeServiceConfig.Networks) == 0 {
×
615
                if defaultNetwork, ok := composeObject.Networks["default"]; ok {
×
616
                        normalizedNetworkName, err := normalizeNetworkNames(defaultNetwork.Name)
×
617
                        if err != nil {
×
618
                                return errors.Wrap(err, "Unable to normalize network name")
×
619
                        }
×
620
                        serviceConfig.Network = append(serviceConfig.Network, normalizedNetworkName)
×
621
                }
622
        } else {
×
623
                var alias = ""
×
624
                for key := range composeServiceConfig.Networks {
×
625
                        alias = key
×
626
                        netName := composeObject.Networks[alias].Name
×
627

×
628
                        // if Network Name Field is empty in the docker-compose definition
×
629
                        // we will use the alias name defined in service config file
×
630
                        if netName == "" {
×
631
                                netName = alias
×
632
                        }
×
633

634
                        normalizedNetworkName, err := normalizeNetworkNames(netName)
×
635
                        if err != nil {
×
636
                                return errors.Wrap(err, "Unable to normalize network name")
×
637
                        }
×
638

639
                        serviceConfig.Network = append(serviceConfig.Network, normalizedNetworkName)
×
640
                }
641
        }
642

643
        return nil
×
644
}
645

646
func parseResources(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig) error {
×
647
        serviceConfig.MemLimit = composeServiceConfig.MemLimit
×
648

×
649
        if composeServiceConfig.Deploy != nil {
×
650
                // memory:
×
651
                // See: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
×
652
                // "The expression 0.1 is equivalent to the expression 100m, which can be read as “one hundred millicpu”."
×
653

×
654
                // Since Deploy.Resources.Limits does not initialize, we must check type Resources before continuing
×
655
                if composeServiceConfig.Deploy.Resources.Limits != nil {
×
656
                        serviceConfig.MemLimit = composeServiceConfig.Deploy.Resources.Limits.MemoryBytes
×
657

×
658
                        if composeServiceConfig.Deploy.Resources.Limits.NanoCPUs > 0 {
×
659
                                serviceConfig.CPULimit = int64(composeServiceConfig.Deploy.Resources.Limits.NanoCPUs * 1000)
×
660
                        }
×
661
                }
662
                if composeServiceConfig.Deploy.Resources.Reservations != nil {
×
663
                        serviceConfig.MemReservation = composeServiceConfig.Deploy.Resources.Reservations.MemoryBytes
×
664

×
665
                        if composeServiceConfig.Deploy.Resources.Reservations.NanoCPUs > 0 {
×
666
                                serviceConfig.CPUReservation = int64(composeServiceConfig.Deploy.Resources.Reservations.NanoCPUs * 1000)
×
667
                        }
×
668
                }
669
        }
670
        return nil
×
671
}
672

673
func parseEnvironment(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig) {
×
674
        // Gather the environment values
×
675
        // DockerCompose uses map[string]*string while we use []string
×
676
        // So let's convert that using this hack
×
677
        // Note: unset env pick up the env value on host if exist
×
678
        for name, value := range composeServiceConfig.Environment {
×
679
                var env kobject.EnvVar
×
680
                if value != nil {
×
681
                        env = kobject.EnvVar{Name: name, Value: *value}
×
682
                } else {
×
683
                        result, ok := os.LookupEnv(name)
×
684
                        if ok {
×
685
                                env = kobject.EnvVar{Name: name, Value: result}
×
686
                        } else {
×
687
                                continue
×
688
                        }
689
                }
690
                serviceConfig.Environment = append(serviceConfig.Environment, env)
×
691
        }
692
}
693

694
func parseEnvFiles(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig) {
2✔
695
        for _, value := range composeServiceConfig.EnvFiles {
5✔
696
                serviceConfig.EnvFile = append(serviceConfig.EnvFile, value.Path)
3✔
697
                // value.Required is ignored
3✔
698
        }
3✔
699
}
700

701
func handleCronJobConcurrencyPolicy(policy string) (batchv1.ConcurrencyPolicy, error) {
×
702
        switch policy {
×
703
        case "Allow":
×
704
                return batchv1.AllowConcurrent, nil
×
705
        case "Forbid":
×
706
                return batchv1.ForbidConcurrent, nil
×
707
        case "Replace":
×
708
                return batchv1.ReplaceConcurrent, nil
×
709
        case "":
×
710
                return "", nil
×
711
        default:
×
712
                return "", fmt.Errorf("invalid cronjob concurrency policy: %s", policy)
×
713
        }
714
}
715

716
func handleCronJobBackoffLimit(backoffLimit string) (*int32, error) {
×
717
        if backoffLimit == "" {
×
718
                return nil, nil
×
719
        }
×
720

721
        limit, err := cast.ToInt32E(backoffLimit)
×
722
        if err != nil {
×
723
                return nil, fmt.Errorf("invalid cronjob backoff limit: %s", backoffLimit)
×
724
        }
×
725
        return &limit, nil
×
726
}
727

728
func handleCronJobSchedule(schedule string) (string, error) {
×
729
        if schedule == "" {
×
730
                return "", fmt.Errorf("cronjob schedule cannot be empty")
×
731
        }
×
732

733
        return schedule, nil
×
734

735
}
736

737
// parseKomposeLabels parse kompose labels, also do some validation
738
func parseKomposeLabels(labels map[string]string, serviceConfig *kobject.ServiceConfig) error {
5✔
739
        // Label handler
5✔
740
        // Labels used to influence conversion of kompose will be handled
5✔
741
        // from here for docker-compose. Each loader will have such handler.
5✔
742
        if serviceConfig.Labels == nil {
6✔
743
                serviceConfig.Labels = make(map[string]string)
1✔
744
        }
1✔
745

746
        for key, value := range labels {
10✔
747
                switch key {
5✔
748
                case LabelServiceType:
×
749
                        serviceType, err := handleServiceType(value)
×
750
                        if err != nil {
×
751
                                return errors.Wrap(err, "handleServiceType failed")
×
752
                        }
×
753

754
                        serviceConfig.ServiceType = serviceType
×
755
                case LabelServiceExternalTrafficPolicy:
×
756
                        serviceExternalTypeTrafficPolicy, err := handleServiceExternalTrafficPolicy(value)
×
757
                        if err != nil {
×
758
                                return errors.Wrap(err, "handleServiceExternalTrafficPolicy failed")
×
759
                        }
×
760

761
                        serviceConfig.ServiceExternalTrafficPolicy = serviceExternalTypeTrafficPolicy
×
762
                case LabelSecurityContextFsGroup:
×
763
                        serviceConfig.FsGroup = cast.ToInt64(value)
×
764
                case LabelExposeContainerToHost:
×
765
                        serviceConfig.ExposeContainerToHost = cast.ToBool(value)
×
766
                case LabelServiceExpose:
×
767
                        serviceConfig.ExposeService = strings.Trim(value, " ,")
×
768
                case LabelNodePortPort:
×
769
                        serviceConfig.NodePortPort = cast.ToInt32(value)
×
770
                case LabelServiceExposeTLSSecret:
×
771
                        serviceConfig.ExposeServiceTLS = value
×
772
                case LabelServiceExposeIngressClassName:
×
773
                        serviceConfig.ExposeServiceIngressClassName = value
×
774
                case LabelImagePullSecret:
×
775
                        serviceConfig.ImagePullSecret = value
×
776
                case LabelImagePullPolicy:
×
777
                        serviceConfig.ImagePullPolicy = value
×
778
                case LabelContainerVolumeSubpath:
×
779
                        serviceConfig.VolumeMountSubPath = value
×
780
                case LabelCronJobSchedule:
×
781
                        cronJobSchedule, err := handleCronJobSchedule(value)
×
782
                        if err != nil {
×
783
                                return errors.Wrap(err, "handleCronJobSchedule failed")
×
784
                        }
×
785

786
                        serviceConfig.CronJobSchedule = cronJobSchedule
×
787
                case LabelCronJobConcurrencyPolicy:
×
788
                        cronJobConcurrencyPolicy, err := handleCronJobConcurrencyPolicy(value)
×
789
                        if err != nil {
×
790
                                return errors.Wrap(err, "handleCronJobConcurrencyPolicy failed")
×
791
                        }
×
792

793
                        serviceConfig.CronJobConcurrencyPolicy = cronJobConcurrencyPolicy
×
794
                case LabelCronJobBackoffLimit:
×
795
                        cronJobBackoffLimit, err := handleCronJobBackoffLimit(value)
×
796
                        if err != nil {
×
797
                                return errors.Wrap(err, "handleCronJobBackoffLimit failed")
×
798
                        }
×
799

800
                        serviceConfig.CronJobBackoffLimit = cronJobBackoffLimit
×
801
                case LabelNameOverride:
5✔
802
                        // generate a valid k8s resource name
5✔
803
                        normalizedName := normalizeServiceNames(value)
5✔
804
                        serviceConfig.Name = normalizedName
5✔
805
                default:
×
806
                        serviceConfig.Labels[key] = value
×
807
                }
808
        }
809

810
        if serviceConfig.ExposeService == "" && serviceConfig.ExposeServiceTLS != "" {
5✔
811
                return errors.New("kompose.service.expose.tls-secret was specified without kompose.service.expose")
×
812
        }
×
813

814
        if serviceConfig.ExposeService == "" && serviceConfig.ExposeServiceIngressClassName != "" {
5✔
815
                return errors.New("kompose.service.expose.ingress-class-name was specified without kompose.service.expose")
×
816
        }
×
817

818
        if serviceConfig.ServiceType != string(api.ServiceTypeNodePort) && serviceConfig.NodePortPort != 0 {
5✔
819
                return errors.New("kompose.service.type must be nodeport when assign node port value")
×
820
        }
×
821

822
        if len(serviceConfig.Port) > 1 && serviceConfig.NodePortPort != 0 {
5✔
823
                return errors.New("cannot set kompose.service.nodeport.port when service has multiple ports")
×
824
        }
×
825

826
        if serviceConfig.Restart == "always" && serviceConfig.CronJobConcurrencyPolicy != "" {
5✔
827
                log.Infof("cronjob restart policy will be converted from '%s' to 'on-failure'", serviceConfig.Restart)
×
828
                serviceConfig.Restart = "on-failure"
×
829
        }
×
830

831
        return nil
5✔
832
}
833

834
func handleVolume(komposeObject *kobject.KomposeObject, volumes *types.Volumes) {
×
835
        for name := range komposeObject.ServiceConfigs {
×
836
                // retrieve volumes of service
×
837
                vols, err := retrieveVolume(name, *komposeObject)
×
838
                if err != nil {
×
839
                        errors.Wrap(err, "could not retrieve vvolume")
×
840
                }
×
841
                for volName, vol := range vols {
×
842
                        size, selector := getVolumeLabels(vol.VolumeName, volumes)
×
843
                        if len(size) > 0 || len(selector) > 0 {
×
844
                                // We can't assign value to struct field in map while iterating over it, so temporary variable `temp` is used here
×
845
                                var temp = vols[volName]
×
846
                                temp.PVCSize = size
×
847
                                temp.SelectorValue = selector
×
848
                                vols[volName] = temp
×
849
                        }
×
850
                }
851
                // We can't assign value to struct field in map while iterating over it, so temporary variable `temp` is used here
852
                var temp = komposeObject.ServiceConfigs[name]
×
853
                temp.Volumes = vols
×
854
                komposeObject.ServiceConfigs[name] = temp
×
855
        }
856
}
857

858
// returns all volumes associated with service, if `volumes_from` key is used, we have to retrieve volumes from the services which are mentioned there. Hence, recursive function is used here.
859
func retrieveVolume(svcName string, komposeObject kobject.KomposeObject) (volume []kobject.Volumes, err error) {
×
860
        // if volumes-from key is present
×
861
        if komposeObject.ServiceConfigs[svcName].VolumesFrom != nil {
×
862
                // iterating over services from `volumes-from`
×
863
                for _, depSvc := range komposeObject.ServiceConfigs[svcName].VolumesFrom {
×
864
                        // recursive call for retrieving volumes of services from `volumes-from`
×
865
                        dVols, err := retrieveVolume(depSvc, komposeObject)
×
866
                        if err != nil {
×
867
                                return nil, errors.Wrapf(err, "could not retrieve the volume")
×
868
                        }
×
869
                        var cVols []kobject.Volumes
×
870
                        cVols, err = ParseVols(komposeObject.ServiceConfigs[svcName].VolList, svcName)
×
871
                        if err != nil {
×
872
                                return nil, errors.Wrapf(err, "error generating current volumes")
×
873
                        }
×
874

875
                        for _, cv := range cVols {
×
876
                                // check whether volumes of current service is same or not as that of dependent volumes coming from `volumes-from`
×
877
                                ok, dv := getVol(cv, dVols)
×
878
                                if ok {
×
879
                                        // change current volumes service name to dependent service name
×
880
                                        if dv.VFrom == "" {
×
881
                                                cv.VFrom = dv.SvcName
×
882
                                                cv.SvcName = dv.SvcName
×
883
                                        } else {
×
884
                                                cv.VFrom = dv.VFrom
×
885
                                                cv.SvcName = dv.SvcName
×
886
                                        }
×
887
                                        cv.PVCName = dv.PVCName
×
888
                                }
889
                                volume = append(volume, cv)
×
890
                        }
891
                        // iterating over dependent volumes
892
                        for _, dv := range dVols {
×
893
                                // check whether dependent volume is already present or not
×
894
                                if checkVolDependent(dv, volume) {
×
895
                                        // if found, add service name to `VFrom`
×
896
                                        dv.VFrom = dv.SvcName
×
897
                                        volume = append(volume, dv)
×
898
                                }
×
899
                        }
900
                }
901
        } else {
×
902
                // if `volumes-from` is not present
×
903
                volume, err = ParseVols(komposeObject.ServiceConfigs[svcName].VolList, svcName)
×
904
                if err != nil {
×
905
                        return nil, errors.Wrapf(err, "error generating current volumes")
×
906
                }
×
907
        }
908
        return
×
909
}
910

911
// checkVolDependent returns false if dependent volume is present
912
func checkVolDependent(dv kobject.Volumes, volume []kobject.Volumes) bool {
×
913
        for _, vol := range volume {
×
914
                if vol.PVCName == dv.PVCName {
×
915
                        return false
×
916
                }
×
917
        }
918
        return true
×
919
}
920

921
// ParseVols parse volumes
922
func ParseVols(volNames []string, svcName string) ([]kobject.Volumes, error) {
×
923
        var volumes []kobject.Volumes
×
924
        var err error
×
925

×
926
        for i, vn := range volNames {
×
927
                var v kobject.Volumes
×
928
                v.VolumeName, v.Host, v.Container, v.Mode, err = transformer.ParseVolume(vn)
×
929
                if err != nil {
×
930
                        return nil, errors.Wrapf(err, "could not parse volume %q: %v", vn, err)
×
931
                }
×
932
                v.VolumeName = normalizeVolumes(v.VolumeName)
×
933
                v.SvcName = svcName
×
934
                v.MountPath = fmt.Sprintf("%s:%s", v.Host, v.Container)
×
935
                v.PVCName = fmt.Sprintf("%s-claim%d", v.SvcName, i)
×
936
                volumes = append(volumes, v)
×
937
        }
938

939
        return volumes, nil
×
940
}
941

942
// for dependent volumes, returns true and the respective volume if mountpath are same
943
func getVol(toFind kobject.Volumes, Vols []kobject.Volumes) (bool, kobject.Volumes) {
×
944
        for _, dv := range Vols {
×
945
                if toFind.MountPath == dv.MountPath {
×
946
                        return true, dv
×
947
                }
×
948
        }
949
        return false, kobject.Volumes{}
×
950
}
951

952
func getVolumeLabels(name string, volumes *types.Volumes) (string, string) {
×
953
        size, selector := "", ""
×
954

×
955
        if volume, ok := (*volumes)[name]; ok {
×
956
                for key, value := range volume.Labels {
×
957
                        if key == "kompose.volume.size" {
×
958
                                size = value
×
959
                        } else if key == "kompose.volume.selector" {
×
960
                                selector = value
×
961
                        }
×
962
                }
963
        }
964

965
        return size, selector
×
966
}
967

968
// getGroupAdd will return group in int64 format
969
func getGroupAdd(group []string) ([]int64, error) {
×
970
        var groupAdd []int64
×
971
        for _, i := range group {
×
972
                j, err := strconv.Atoi(i)
×
973
                if err != nil {
×
974
                        return nil, errors.Wrap(err, "unable to get group_add key")
×
975
                }
×
976
                groupAdd = append(groupAdd, int64(j))
×
977
        }
978
        return groupAdd, nil
×
979
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc