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

kubernetes / kompose / 4138934819

09 Feb 2023 10:31PM UTC coverage: 53.502% (+1.3%) from 52.252%
4138934819

Pull #1544

github

AhmedGrati
feat: support external traffic policy
Pull Request #1544: Feat support external traffic policy

28 of 28 new or added lines in 4 files covered. (100.0%)

1963 of 3669 relevant lines covered (53.5%)

7.76 hits per line

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

34.3
/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
        "fmt"
21
        "os"
22
        "reflect"
23
        "strconv"
24
        "strings"
25
        "time"
26

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

39
// StdinData is data bytes read from stdin
40
var StdinData []byte
41

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

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

2✔
84
        var keysFound []string
2✔
85

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

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

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

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

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

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

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

159
        projectOptions, err := cli.NewProjectOptions(files, cli.WithOsEnv, cli.WithWorkingDirectory(workingDir), cli.WithInterpolation(false))
×
160
        if err != nil {
×
161
                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to create compose options")
×
162
        }
×
163

164
        project, err := cli.ProjectFromOptions(projectOptions)
×
165
        if err != nil {
×
166
                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to load files")
×
167
        }
×
168

169
        komposeObject, err := dockerComposeToKomposeMapping(project)
×
170
        if err != nil {
×
171
                return kobject.KomposeObject{}, err
×
172
        }
×
173
        return komposeObject, nil
×
174
}
175

176
func loadPlacement(placement types.Placement) kobject.Placement {
1✔
177
        komposePlacement := kobject.Placement{
1✔
178
                PositiveConstraints: make(map[string]string),
1✔
179
                NegativeConstraints: make(map[string]string),
1✔
180
                Preferences:         make([]string, 0, len(placement.Preferences)),
1✔
181
        }
1✔
182

1✔
183
        // Convert constraints
1✔
184
        equal, notEqual := " == ", " != "
1✔
185
        for _, j := range placement.Constraints {
3✔
186
                operator := equal
2✔
187
                if strings.Contains(j, notEqual) {
3✔
188
                        operator = notEqual
1✔
189
                }
1✔
190
                p := strings.Split(j, operator)
2✔
191
                if len(p) < 2 {
2✔
192
                        log.Warnf("Failed to parse placement constraints %s, the correct format is 'label == xxx'", j)
×
193
                        continue
×
194
                }
195

196
                key, err := convertDockerLabel(p[0])
2✔
197
                if err != nil {
2✔
198
                        log.Warn("Ignore placement constraints: ", err.Error())
×
199
                        continue
×
200
                }
201

202
                if operator == equal {
3✔
203
                        komposePlacement.PositiveConstraints[key] = p[1]
1✔
204
                } else if operator == notEqual {
3✔
205
                        komposePlacement.NegativeConstraints[key] = p[1]
1✔
206
                }
1✔
207
        }
208

209
        // Convert preferences
210
        for _, p := range placement.Preferences {
4✔
211
                // Spread is the only supported strategy currently
3✔
212
                label, err := convertDockerLabel(p.Spread)
3✔
213
                if err != nil {
4✔
214
                        log.Warn("Ignore placement preferences: ", err.Error())
1✔
215
                        continue
1✔
216
                }
217
                komposePlacement.Preferences = append(komposePlacement.Preferences, label)
2✔
218
        }
219
        return komposePlacement
1✔
220
}
221

222
// Convert docker label to k8s label
223
func convertDockerLabel(dockerLabel string) (string, error) {
5✔
224
        switch dockerLabel {
5✔
225
        case "node.hostname":
×
226
                return "kubernetes.io/hostname", nil
×
227
        case "engine.labels.operatingsystem":
×
228
                return "kubernetes.io/os", nil
×
229
        default:
5✔
230
                if strings.HasPrefix(dockerLabel, "node.labels.") {
9✔
231
                        return strings.TrimPrefix(dockerLabel, "node.labels."), nil
4✔
232
                }
4✔
233
        }
234
        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✔
235
        return "", errors.New(errMsg)
1✔
236
}
237

238
// Convert the Docker Compose volumes to []string (the old way)
239
// TODO: Check to see if it's a "bind" or "volume". Ignore for now.
240
// TODO: Refactor it similar to loadPorts
241
// See: https://docs.docker.com/compose/compose-file/#long-syntax-3
242
func loadVolumes(volumes []types.ServiceVolumeConfig) []string {
1✔
243
        var volArray []string
1✔
244
        for _, vol := range volumes {
2✔
245
                // There will *always* be Source when parsing
1✔
246
                v := vol.Source
1✔
247

1✔
248
                if vol.Target != "" {
2✔
249
                        v = v + ":" + vol.Target
1✔
250
                }
1✔
251

252
                if vol.ReadOnly {
2✔
253
                        v = v + ":ro"
1✔
254
                }
1✔
255

256
                volArray = append(volArray, v)
1✔
257
        }
258
        return volArray
1✔
259
}
260

261
// Convert Docker Compose ports to kobject.Ports
262
// expose ports will be treated as TCP ports
263
func loadPorts(ports []types.ServicePortConfig, expose []string) []kobject.Ports {
11✔
264
        komposePorts := []kobject.Ports{}
11✔
265
        exist := map[string]bool{}
11✔
266

11✔
267
        for _, port := range ports {
30✔
268
                // Convert to a kobject struct with ports
19✔
269
                komposePorts = append(komposePorts, kobject.Ports{
19✔
270
                        HostPort:      cast.ToInt32(port.Published),
19✔
271
                        ContainerPort: int32(port.Target),
19✔
272
                        HostIP:        port.HostIP,
19✔
273
                        Protocol:      strings.ToUpper(port.Protocol),
19✔
274
                })
19✔
275
                exist[cast.ToString(port.Target)+port.Protocol] = true
19✔
276
        }
19✔
277

278
        for _, port := range expose {
16✔
279
                portValue := port
5✔
280
                protocol := string(api.ProtocolTCP)
5✔
281
                if strings.Contains(portValue, "/") {
6✔
282
                        splits := strings.Split(port, "/")
1✔
283
                        portValue = splits[0]
1✔
284
                        protocol = splits[1]
1✔
285
                }
1✔
286

287
                if exist[portValue+protocol] {
6✔
288
                        continue
1✔
289
                }
290
                komposePorts = append(komposePorts, kobject.Ports{
4✔
291
                        HostPort:      cast.ToInt32(portValue),
4✔
292
                        ContainerPort: cast.ToInt32(portValue),
4✔
293
                        HostIP:        "",
4✔
294
                        Protocol:      strings.ToUpper(protocol),
4✔
295
                })
4✔
296
        }
297

298
        return komposePorts
11✔
299
}
300

301
/*
302
        Convert the HealthCheckConfig as designed by Docker to
303

304
a Kubernetes-compatible format.
305
*/
306
func parseHealthCheckReadiness(labels types.Labels) (kobject.HealthCheck, error) {
3✔
307
        var test []string
3✔
308
        var httpPath string
3✔
309
        var httpPort, tcpPort, timeout, interval, retries, startPeriod int32
3✔
310
        var disable bool
3✔
311

3✔
312
        for key, value := range labels {
19✔
313
                switch key {
16✔
314
                case HealthCheckReadinessDisable:
×
315
                        disable = cast.ToBool(value)
×
316
                case HealthCheckReadinessTest:
1✔
317
                        if len(value) > 0 {
2✔
318
                                test, _ = shlex.Split(value)
1✔
319
                        }
1✔
320
                case HealthCheckReadinessHTTPGetPath:
1✔
321
                        httpPath = value
1✔
322
                case HealthCheckReadinessHTTPGetPort:
1✔
323
                        httpPort = cast.ToInt32(value)
1✔
324
                case HealthCheckReadinessTCPPort:
1✔
325
                        tcpPort = cast.ToInt32(value)
1✔
326
                case HealthCheckReadinessInterval:
3✔
327
                        parse, err := time.ParseDuration(value)
3✔
328
                        if err != nil {
3✔
329
                                return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check interval variable")
×
330
                        }
×
331
                        interval = int32(parse.Seconds())
3✔
332
                case HealthCheckReadinessTimeout:
3✔
333
                        parse, err := time.ParseDuration(value)
3✔
334
                        if err != nil {
3✔
335
                                return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check timeout variable")
×
336
                        }
×
337
                        timeout = int32(parse.Seconds())
3✔
338
                case HealthCheckReadinessRetries:
3✔
339
                        retries = cast.ToInt32(value)
3✔
340
                case HealthCheckReadinessStartPeriod:
3✔
341
                        parse, err := time.ParseDuration(value)
3✔
342
                        if err != nil {
3✔
343
                                return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check startPeriod variable")
×
344
                        }
×
345
                        startPeriod = int32(parse.Seconds())
3✔
346
                }
347
        }
348

349
        if len(test) > 0 {
4✔
350
                if test[0] == "NONE" {
1✔
351
                        disable = true
×
352
                        test = test[1:]
×
353
                }
×
354
                // Due to docker/cli adding "CMD-SHELL" to the struct, we remove the first element of composeHealthCheck.Test
355
                if test[0] == "CMD" || test[0] == "CMD-SHELL" {
1✔
356
                        test = test[1:]
×
357
                }
×
358
        }
359

360
        return kobject.HealthCheck{
3✔
361
                Test:        test,
3✔
362
                HTTPPath:    httpPath,
3✔
363
                HTTPPort:    httpPort,
3✔
364
                TCPPort:     tcpPort,
3✔
365
                Timeout:     timeout,
3✔
366
                Interval:    interval,
3✔
367
                Retries:     retries,
3✔
368
                StartPeriod: startPeriod,
3✔
369
                Disable:     disable,
3✔
370
        }, nil
3✔
371
}
372

373
/*
374
        Convert the HealthCheckConfig as designed by Docker to
375

376
a Kubernetes-compatible format.
377
*/
378
func parseHealthCheck(composeHealthCheck types.HealthCheckConfig, labels types.Labels) (kobject.HealthCheck, error) {
3✔
379
        var httpPort, tcpPort, timeout, interval, retries, startPeriod int32
3✔
380
        var test []string
3✔
381
        var httpPath string
3✔
382

3✔
383
        // Here we convert the timeout from 1h30s (example) to 36030 seconds.
3✔
384
        if composeHealthCheck.Timeout != nil {
6✔
385
                parse, err := time.ParseDuration(composeHealthCheck.Timeout.String())
3✔
386
                if err != nil {
3✔
387
                        return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check timeout variable")
×
388
                }
×
389
                timeout = int32(parse.Seconds())
3✔
390
        }
391

392
        if composeHealthCheck.Interval != nil {
6✔
393
                parse, err := time.ParseDuration(composeHealthCheck.Interval.String())
3✔
394
                if err != nil {
3✔
395
                        return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check interval variable")
×
396
                }
×
397
                interval = int32(parse.Seconds())
3✔
398
        }
399

400
        if composeHealthCheck.Retries != nil {
6✔
401
                retries = int32(*composeHealthCheck.Retries)
3✔
402
        }
3✔
403

404
        if composeHealthCheck.StartPeriod != nil {
6✔
405
                parse, err := time.ParseDuration(composeHealthCheck.StartPeriod.String())
3✔
406
                if err != nil {
3✔
407
                        return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check startPeriod variable")
×
408
                }
×
409
                startPeriod = int32(parse.Seconds())
3✔
410
        }
411

412
        if composeHealthCheck.Test != nil {
4✔
413
                test = composeHealthCheck.Test[1:]
1✔
414
        }
1✔
415

416
        for key, value := range labels {
6✔
417
                switch key {
3✔
418
                case HealthCheckLivenessHTTPGetPath:
1✔
419
                        httpPath = value
1✔
420
                case HealthCheckLivenessHTTPGetPort:
1✔
421
                        httpPort = cast.ToInt32(value)
1✔
422
                case HealthCheckLivenessTCPPort:
1✔
423
                        tcpPort = cast.ToInt32(value)
1✔
424
                }
425
        }
426

427
        // Due to docker/cli adding "CMD-SHELL" to the struct, we remove the first element of composeHealthCheck.Test
428
        return kobject.HealthCheck{
3✔
429
                Test:        test,
3✔
430
                TCPPort:     tcpPort,
3✔
431
                HTTPPath:    httpPath,
3✔
432
                HTTPPort:    httpPort,
3✔
433
                Timeout:     timeout,
3✔
434
                Interval:    interval,
3✔
435
                Retries:     retries,
3✔
436
                StartPeriod: startPeriod,
3✔
437
        }, nil
3✔
438
}
439

440
func dockerComposeToKomposeMapping(composeObject *types.Project) (kobject.KomposeObject, error) {
×
441
        // Step 1. Initialize what's going to be returned
×
442
        komposeObject := kobject.KomposeObject{
×
443
                ServiceConfigs: make(map[string]kobject.ServiceConfig),
×
444
                LoadedFrom:     "compose",
×
445
                Secrets:        composeObject.Secrets,
×
446
        }
×
447

×
448
        // Step 2. Parse through the object and convert it to kobject.KomposeObject!
×
449
        // Here we "clean up" the service configuration so we return something that includes
×
450
        // all relevant information as well as avoid the unsupported keys as well.
×
451
        for _, composeServiceConfig := range composeObject.Services {
×
452
                // Standard import
×
453
                // No need to modify before importation
×
454
                name := composeServiceConfig.Name
×
455
                serviceConfig := kobject.ServiceConfig{}
×
456
                serviceConfig.Name = name
×
457
                serviceConfig.Image = composeServiceConfig.Image
×
458
                serviceConfig.WorkingDir = composeServiceConfig.WorkingDir
×
459
                serviceConfig.Annotations = composeServiceConfig.Labels
×
460
                serviceConfig.CapAdd = composeServiceConfig.CapAdd
×
461
                serviceConfig.CapDrop = composeServiceConfig.CapDrop
×
462
                serviceConfig.Expose = composeServiceConfig.Expose
×
463
                serviceConfig.Privileged = composeServiceConfig.Privileged
×
464
                serviceConfig.User = composeServiceConfig.User
×
465
                serviceConfig.Stdin = composeServiceConfig.StdinOpen
×
466
                serviceConfig.Tty = composeServiceConfig.Tty
×
467
                serviceConfig.TmpFs = composeServiceConfig.Tmpfs
×
468
                serviceConfig.ContainerName = normalizeContainerNames(composeServiceConfig.ContainerName)
×
469
                serviceConfig.Command = composeServiceConfig.Entrypoint
×
470
                serviceConfig.Args = composeServiceConfig.Command
×
471
                serviceConfig.Labels = composeServiceConfig.Labels
×
472
                serviceConfig.HostName = composeServiceConfig.Hostname
×
473
                serviceConfig.DomainName = composeServiceConfig.DomainName
×
474
                serviceConfig.Secrets = composeServiceConfig.Secrets
×
475

×
476
                if composeServiceConfig.StopGracePeriod != nil {
×
477
                        serviceConfig.StopGracePeriod = composeServiceConfig.StopGracePeriod.String()
×
478
                }
×
479

480
                if err := parseNetwork(&composeServiceConfig, &serviceConfig, composeObject); err != nil {
×
481
                        return kobject.KomposeObject{}, err
×
482
                }
×
483

484
                if err := parseResources(&composeServiceConfig, &serviceConfig); err != nil {
×
485
                        return kobject.KomposeObject{}, err
×
486
                }
×
487

488
                serviceConfig.Restart = composeServiceConfig.Restart
×
489

×
490
                if composeServiceConfig.Deploy != nil {
×
491
                        // Deploy keys
×
492
                        // mode:
×
493
                        serviceConfig.DeployMode = composeServiceConfig.Deploy.Mode
×
494
                        // labels
×
495
                        serviceConfig.DeployLabels = composeServiceConfig.Deploy.Labels
×
496

×
497
                        // restart-policy: deploy.restart_policy.condition will rewrite restart option
×
498
                        // see: https://docs.docker.com/compose/compose-file/#restart_policy
×
499
                        if composeServiceConfig.Deploy.RestartPolicy != nil {
×
500
                                serviceConfig.Restart = composeServiceConfig.Deploy.RestartPolicy.Condition
×
501
                        }
×
502

503
                        // replicas:
504
                        if composeServiceConfig.Deploy.Replicas != nil {
×
505
                                serviceConfig.Replicas = int(*composeServiceConfig.Deploy.Replicas)
×
506
                        }
×
507

508
                        // placement:
509
                        serviceConfig.Placement = loadPlacement(composeServiceConfig.Deploy.Placement)
×
510

×
511
                        if composeServiceConfig.Deploy.UpdateConfig != nil {
×
512
                                serviceConfig.DeployUpdateConfig = *composeServiceConfig.Deploy.UpdateConfig
×
513
                        }
×
514

515
                        if composeServiceConfig.Deploy.EndpointMode == "vip" {
×
516
                                serviceConfig.ServiceType = string(api.ServiceTypeNodePort)
×
517
                        }
×
518
                }
519

520
                // HealthCheck Liveness
521
                if composeServiceConfig.HealthCheck != nil && !composeServiceConfig.HealthCheck.Disable {
×
522
                        var err error
×
523
                        serviceConfig.HealthChecks.Liveness, err = parseHealthCheck(*composeServiceConfig.HealthCheck, composeServiceConfig.Labels)
×
524
                        if err != nil {
×
525
                                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to parse health check")
×
526
                        }
×
527
                }
528

529
                // HealthCheck Readiness
530
                var readiness, errReadiness = parseHealthCheckReadiness(composeServiceConfig.Labels)
×
531
                if !readiness.Disable {
×
532
                        serviceConfig.HealthChecks.Readiness = readiness
×
533
                        if errReadiness != nil {
×
534
                                return kobject.KomposeObject{}, errors.Wrap(errReadiness, "Unable to parse health check")
×
535
                        }
×
536
                }
537

538
                if serviceConfig.Restart == "unless-stopped" {
×
539
                        log.Warnf("Restart policy 'unless-stopped' in service %s is not supported, convert it to 'always'", name)
×
540
                        serviceConfig.Restart = "always"
×
541
                }
×
542

543
                if composeServiceConfig.Build != nil {
×
544
                        serviceConfig.Build = composeServiceConfig.Build.Context
×
545
                        serviceConfig.Dockerfile = composeServiceConfig.Build.Dockerfile
×
546
                        serviceConfig.BuildArgs = composeServiceConfig.Build.Args
×
547
                        serviceConfig.BuildLabels = composeServiceConfig.Build.Labels
×
548
                }
×
549

550
                // env
551
                parseEnvironment(&composeServiceConfig, &serviceConfig)
×
552

×
553
                // Get env_file
×
554
                serviceConfig.EnvFile = composeServiceConfig.EnvFile
×
555

×
556
                // Parse the ports
×
557
                // v3 uses a new format called "long syntax" starting in 3.2
×
558
                // https://docs.docker.com/compose/compose-file/#ports
×
559

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

×
563
                // Parse the volumes
×
564
                // Again, in v3, we use the "long syntax" for volumes in terms of parsing
×
565
                // https://docs.docker.com/compose/compose-file/#long-syntax-3
×
566
                serviceConfig.VolList = loadVolumes(composeServiceConfig.Volumes)
×
567
                if err := parseKomposeLabels(composeServiceConfig.Labels, &serviceConfig); err != nil {
×
568
                        return kobject.KomposeObject{}, err
×
569
                }
×
570

571
                // Log if the name will been changed
572
                if normalizeServiceNames(name) != name {
×
573
                        log.Infof("Service name in docker-compose has been changed from %q to %q", name, normalizeServiceNames(name))
×
574
                }
×
575

576
                serviceConfig.Configs = composeServiceConfig.Configs
×
577
                serviceConfig.ConfigsMetaData = composeObject.Configs
×
578

×
579
                // Get GroupAdd, group should be mentioned in gid format but not the group name
×
580
                groupAdd, err := getGroupAdd(composeServiceConfig.GroupAdd)
×
581
                if err != nil {
×
582
                        return kobject.KomposeObject{}, errors.Wrap(err, "GroupAdd should be mentioned in gid format, not a group name")
×
583
                }
×
584
                serviceConfig.GroupAdd = groupAdd
×
585

×
586
                // Final step, add to the array!
×
587
                komposeObject.ServiceConfigs[normalizeServiceNames(name)] = serviceConfig
×
588
        }
589

590
        handleVolume(&komposeObject, &composeObject.Volumes)
×
591
        return komposeObject, nil
×
592
}
593

594
func parseNetwork(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig, composeObject *types.Project) error {
×
595
        if len(composeServiceConfig.Networks) == 0 {
×
596
                if defaultNetwork, ok := composeObject.Networks["default"]; ok {
×
597
                        normalizedNetworkName, err := normalizeNetworkNames(defaultNetwork.Name)
×
598
                        if err != nil {
×
599
                                return errors.Wrap(err, "Unable to normalize network name")
×
600
                        }
×
601
                        serviceConfig.Network = append(serviceConfig.Network, normalizedNetworkName)
×
602
                }
603
        } else {
×
604
                var alias = ""
×
605
                for key := range composeServiceConfig.Networks {
×
606
                        alias = key
×
607
                        netName := composeObject.Networks[alias].Name
×
608

×
609
                        // if Network Name Field is empty in the docker-compose definition
×
610
                        // we will use the alias name defined in service config file
×
611
                        if netName == "" {
×
612
                                netName = alias
×
613
                        }
×
614

615
                        normalizedNetworkName, err := normalizeNetworkNames(netName)
×
616
                        if err != nil {
×
617
                                return errors.Wrap(err, "Unable to normalize network name")
×
618
                        }
×
619

620
                        serviceConfig.Network = append(serviceConfig.Network, normalizedNetworkName)
×
621
                }
622
        }
623

624
        return nil
×
625
}
626

627
func parseResources(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig) error {
×
628
        serviceConfig.MemLimit = composeServiceConfig.MemLimit
×
629

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

×
635
                // Since Deploy.Resources.Limits does not initialize, we must check type Resources before continuing
×
636
                if composeServiceConfig.Deploy.Resources.Limits != nil {
×
637
                        serviceConfig.MemLimit = composeServiceConfig.Deploy.Resources.Limits.MemoryBytes
×
638

×
639
                        if composeServiceConfig.Deploy.Resources.Limits.NanoCPUs != "" {
×
640
                                cpuLimit, err := strconv.ParseFloat(composeServiceConfig.Deploy.Resources.Limits.NanoCPUs, 64)
×
641
                                if err != nil {
×
642
                                        return errors.Wrap(err, "Unable to convert cpu limits resources value")
×
643
                                }
×
644
                                serviceConfig.CPULimit = int64(cpuLimit * 1000)
×
645
                        }
646
                }
647
                if composeServiceConfig.Deploy.Resources.Reservations != nil {
×
648
                        serviceConfig.MemReservation = composeServiceConfig.Deploy.Resources.Reservations.MemoryBytes
×
649

×
650
                        if composeServiceConfig.Deploy.Resources.Reservations.NanoCPUs != "" {
×
651
                                cpuReservation, err := strconv.ParseFloat(composeServiceConfig.Deploy.Resources.Reservations.NanoCPUs, 64)
×
652
                                if err != nil {
×
653
                                        return errors.Wrap(err, "Unable to convert cpu limits reservation value")
×
654
                                }
×
655
                                serviceConfig.CPUReservation = int64(cpuReservation * 1000)
×
656
                        }
657
                }
658
        }
659
        return nil
×
660
}
661

662
func parseEnvironment(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig) {
×
663
        // Gather the environment values
×
664
        // DockerCompose uses map[string]*string while we use []string
×
665
        // So let's convert that using this hack
×
666
        // Note: unset env pick up the env value on host if exist
×
667
        for name, value := range composeServiceConfig.Environment {
×
668
                var env kobject.EnvVar
×
669
                if value != nil {
×
670
                        env = kobject.EnvVar{Name: name, Value: *value}
×
671
                } else {
×
672
                        result, ok := os.LookupEnv(name)
×
673
                        if ok {
×
674
                                env = kobject.EnvVar{Name: name, Value: result}
×
675
                        } else {
×
676
                                continue
×
677
                        }
678
                }
679
                serviceConfig.Environment = append(serviceConfig.Environment, env)
×
680
        }
681
}
682

683
// parseKomposeLabels parse kompose labels, also do some validation
684
func parseKomposeLabels(labels map[string]string, serviceConfig *kobject.ServiceConfig) error {
×
685
        // Label handler
×
686
        // Labels used to influence conversion of kompose will be handled
×
687
        // from here for docker-compose. Each loader will have such handler.
×
688
        if serviceConfig.Labels == nil {
×
689
                serviceConfig.Labels = make(map[string]string)
×
690
        }
×
691

692
        for key, value := range labels {
×
693
                switch key {
×
694
                case LabelServiceType:
×
695
                        serviceType, err := handleServiceType(value)
×
696
                        if err != nil {
×
697
                                return errors.Wrap(err, "handleServiceType failed")
×
698
                        }
×
699

700
                        serviceConfig.ServiceType = serviceType
×
701
                case LabelServiceExternalTrafficPolicy:
×
702
                        serviceExternalTypeTrafficPolicy, err := handleServiceExternalTrafficPolicy(value)
×
703
                        if err != nil {
×
704
                                return errors.Wrap(err, "handleServiceExternalTrafficPolicy failed")
×
705
                        }
×
706

707
                        serviceConfig.ServiceExternalTrafficPolicy = serviceExternalTypeTrafficPolicy
×
708
                case LabelServiceExpose:
×
709
                        serviceConfig.ExposeService = strings.Trim(strings.ToLower(value), " ,")
×
710
                case LabelNodePortPort:
×
711
                        serviceConfig.NodePortPort = cast.ToInt32(value)
×
712
                case LabelServiceExposeTLSSecret:
×
713
                        serviceConfig.ExposeServiceTLS = value
×
714
                case LabelServiceExposeIngressClassName:
×
715
                        serviceConfig.ExposeServiceIngressClassName = value
×
716
                case LabelImagePullSecret:
×
717
                        serviceConfig.ImagePullSecret = value
×
718
                case LabelImagePullPolicy:
×
719
                        serviceConfig.ImagePullPolicy = value
×
720
                default:
×
721
                        serviceConfig.Labels[key] = value
×
722
                }
723
        }
724

725
        if serviceConfig.ExposeService == "" && serviceConfig.ExposeServiceTLS != "" {
×
726
                return errors.New("kompose.service.expose.tls-secret was specified without kompose.service.expose")
×
727
        }
×
728

729
        if serviceConfig.ExposeService == "" && serviceConfig.ExposeServiceIngressClassName != "" {
×
730
                return errors.New("kompose.service.expose.ingress-class-name was specified without kompose.service.expose")
×
731
        }
×
732

733
        if serviceConfig.ServiceType != string(api.ServiceTypeNodePort) && serviceConfig.NodePortPort != 0 {
×
734
                return errors.New("kompose.service.type must be nodeport when assign node port value")
×
735
        }
×
736

737
        if len(serviceConfig.Port) > 1 && serviceConfig.NodePortPort != 0 {
×
738
                return errors.New("cannot set kompose.service.nodeport.port when service has multiple ports")
×
739
        }
×
740

741
        return nil
×
742
}
743

744
func handleVolume(komposeObject *kobject.KomposeObject, volumes *types.Volumes) {
×
745
        for name := range komposeObject.ServiceConfigs {
×
746
                // retrieve volumes of service
×
747
                vols, err := retrieveVolume(name, *komposeObject)
×
748
                if err != nil {
×
749
                        errors.Wrap(err, "could not retrieve vvolume")
×
750
                }
×
751
                for volName, vol := range vols {
×
752
                        size, selector := getVolumeLabels(vol.VolumeName, volumes)
×
753
                        if len(size) > 0 || len(selector) > 0 {
×
754
                                // We can't assign value to struct field in map while iterating over it, so temporary variable `temp` is used here
×
755
                                var temp = vols[volName]
×
756
                                temp.PVCSize = size
×
757
                                temp.SelectorValue = selector
×
758
                                vols[volName] = temp
×
759
                        }
×
760
                }
761
                // We can't assign value to struct field in map while iterating over it, so temporary variable `temp` is used here
762
                var temp = komposeObject.ServiceConfigs[name]
×
763
                temp.Volumes = vols
×
764
                komposeObject.ServiceConfigs[name] = temp
×
765
        }
766
}
767

768
// 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.
769
func retrieveVolume(svcName string, komposeObject kobject.KomposeObject) (volume []kobject.Volumes, err error) {
×
770
        // if volumes-from key is present
×
771
        if komposeObject.ServiceConfigs[svcName].VolumesFrom != nil {
×
772
                // iterating over services from `volumes-from`
×
773
                for _, depSvc := range komposeObject.ServiceConfigs[svcName].VolumesFrom {
×
774
                        // recursive call for retrieving volumes of services from `volumes-from`
×
775
                        dVols, err := retrieveVolume(depSvc, komposeObject)
×
776
                        if err != nil {
×
777
                                return nil, errors.Wrapf(err, "could not retrieve the volume")
×
778
                        }
×
779
                        var cVols []kobject.Volumes
×
780
                        cVols, err = ParseVols(komposeObject.ServiceConfigs[svcName].VolList, svcName)
×
781
                        if err != nil {
×
782
                                return nil, errors.Wrapf(err, "error generating current volumes")
×
783
                        }
×
784

785
                        for _, cv := range cVols {
×
786
                                // check whether volumes of current service is same or not as that of dependent volumes coming from `volumes-from`
×
787
                                ok, dv := getVol(cv, dVols)
×
788
                                if ok {
×
789
                                        // change current volumes service name to dependent service name
×
790
                                        if dv.VFrom == "" {
×
791
                                                cv.VFrom = dv.SvcName
×
792
                                                cv.SvcName = dv.SvcName
×
793
                                        } else {
×
794
                                                cv.VFrom = dv.VFrom
×
795
                                                cv.SvcName = dv.SvcName
×
796
                                        }
×
797
                                        cv.PVCName = dv.PVCName
×
798
                                }
799
                                volume = append(volume, cv)
×
800
                        }
801
                        // iterating over dependent volumes
802
                        for _, dv := range dVols {
×
803
                                // check whether dependent volume is already present or not
×
804
                                if checkVolDependent(dv, volume) {
×
805
                                        // if found, add service name to `VFrom`
×
806
                                        dv.VFrom = dv.SvcName
×
807
                                        volume = append(volume, dv)
×
808
                                }
×
809
                        }
810
                }
811
        } else {
×
812
                // if `volumes-from` is not present
×
813
                volume, err = ParseVols(komposeObject.ServiceConfigs[svcName].VolList, svcName)
×
814
                if err != nil {
×
815
                        return nil, errors.Wrapf(err, "error generating current volumes")
×
816
                }
×
817
        }
818
        return
×
819
}
820

821
// checkVolDependent returns false if dependent volume is present
822
func checkVolDependent(dv kobject.Volumes, volume []kobject.Volumes) bool {
×
823
        for _, vol := range volume {
×
824
                if vol.PVCName == dv.PVCName {
×
825
                        return false
×
826
                }
×
827
        }
828
        return true
×
829
}
830

831
// ParseVols parse volumes
832
func ParseVols(volNames []string, svcName string) ([]kobject.Volumes, error) {
×
833
        var volumes []kobject.Volumes
×
834
        var err error
×
835

×
836
        for i, vn := range volNames {
×
837
                var v kobject.Volumes
×
838
                v.VolumeName, v.Host, v.Container, v.Mode, err = transformer.ParseVolume(vn)
×
839
                if err != nil {
×
840
                        return nil, errors.Wrapf(err, "could not parse volume %q: %v", vn, err)
×
841
                }
×
842
                v.VolumeName = normalizeVolumes(v.VolumeName)
×
843
                v.SvcName = svcName
×
844
                v.MountPath = fmt.Sprintf("%s:%s", v.Host, v.Container)
×
845
                v.PVCName = fmt.Sprintf("%s-claim%d", v.SvcName, i)
×
846
                volumes = append(volumes, v)
×
847
        }
848

849
        return volumes, nil
×
850
}
851

852
// for dependent volumes, returns true and the respective volume if mountpath are same
853
func getVol(toFind kobject.Volumes, Vols []kobject.Volumes) (bool, kobject.Volumes) {
×
854
        for _, dv := range Vols {
×
855
                if toFind.MountPath == dv.MountPath {
×
856
                        return true, dv
×
857
                }
×
858
        }
859
        return false, kobject.Volumes{}
×
860
}
861

862
func getVolumeLabels(name string, volumes *types.Volumes) (string, string) {
×
863
        size, selector := "", ""
×
864

×
865
        if volume, ok := (*volumes)[name]; ok {
×
866
                for key, value := range volume.Labels {
×
867
                        if key == "kompose.volume.size" {
×
868
                                size = value
×
869
                        } else if key == "kompose.volume.selector" {
×
870
                                selector = value
×
871
                        }
×
872
                }
873
        }
874

875
        return size, selector
×
876
}
877

878
// getGroupAdd will return group in int64 format
879
func getGroupAdd(group []string) ([]int64, error) {
×
880
        var groupAdd []int64
×
881
        for _, i := range group {
×
882
                j, err := strconv.Atoi(i)
×
883
                if err != nil {
×
884
                        return nil, errors.Wrap(err, "unable to get group_add key")
×
885
                }
×
886
                groupAdd = append(groupAdd, int64(j))
×
887
        }
888
        return groupAdd, nil
×
889
}
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