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

kubernetes / kompose / 6696194161

30 Oct 2023 05:02PM UTC coverage: 54.96%. Remained the same
6696194161

push

github

web-flow
chore(deps)(deps): bump golang.org/x/tools from 0.13.0 to 0.14.0 (#1723)

Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.13.0 to 0.14.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

2205 of 4012 relevant lines covered (54.96%)

7.91 hits per line

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

33.69
/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() {
276✔
103
                        // Check if given key is among unsupported keys, and skip it if we already saw this key
273✔
104
                        if alreadySaw, ok := unsupportedKey[f.Name()]; ok && !alreadySaw {
348✔
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, profiles []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(
×
160
                files, cli.WithOsEnv,
×
161
                cli.WithWorkingDirectory(workingDir),
×
162
                cli.WithInterpolation(true),
×
163
                cli.WithProfiles(profiles),
×
164
        )
×
165
        if err != nil {
×
166
                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to create compose options")
×
167
        }
×
168

169
        project, err := cli.ProjectFromOptions(projectOptions)
×
170
        if err != nil {
×
171
                return kobject.KomposeObject{}, errors.Wrap(err, "Unable to load files")
×
172
        }
×
173

174
        komposeObject, err := dockerComposeToKomposeMapping(project)
×
175
        if err != nil {
×
176
                return kobject.KomposeObject{}, err
×
177
        }
×
178
        return komposeObject, nil
×
179
}
180

181
func loadPlacement(placement types.Placement) kobject.Placement {
1✔
182
        komposePlacement := kobject.Placement{
1✔
183
                PositiveConstraints: make(map[string]string),
1✔
184
                NegativeConstraints: make(map[string]string),
1✔
185
                Preferences:         make([]string, 0, len(placement.Preferences)),
1✔
186
        }
1✔
187

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

201
                key, err := convertDockerLabel(p[0])
2✔
202
                if err != nil {
2✔
203
                        log.Warn("Ignore placement constraints: ", err.Error())
×
204
                        continue
×
205
                }
206

207
                if operator == equal {
3✔
208
                        komposePlacement.PositiveConstraints[key] = p[1]
1✔
209
                } else if operator == notEqual {
3✔
210
                        komposePlacement.NegativeConstraints[key] = p[1]
1✔
211
                }
1✔
212
        }
213

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

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

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

1✔
253
                if vol.Target != "" {
2✔
254
                        v = v + ":" + vol.Target
1✔
255
                }
1✔
256

257
                if vol.ReadOnly {
2✔
258
                        v = v + ":ro"
1✔
259
                }
1✔
260

261
                volArray = append(volArray, v)
1✔
262
        }
263
        return volArray
1✔
264
}
265

266
// Convert Docker Compose ports to kobject.Ports
267
// expose ports will be treated as TCP ports
268
func loadPorts(ports []types.ServicePortConfig, expose []string) []kobject.Ports {
11✔
269
        komposePorts := []kobject.Ports{}
11✔
270
        exist := map[string]bool{}
11✔
271

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

283
        for _, port := range expose {
16✔
284
                portValue := port
5✔
285
                protocol := string(api.ProtocolTCP)
5✔
286
                if strings.Contains(portValue, "/") {
6✔
287
                        splits := strings.Split(port, "/")
1✔
288
                        portValue = splits[0]
1✔
289
                        protocol = splits[1]
1✔
290
                }
1✔
291

292
                if exist[portValue+protocol] {
6✔
293
                        continue
1✔
294
                }
295
                komposePorts = append(komposePorts, kobject.Ports{
4✔
296
                        ContainerPort: cast.ToInt32(portValue),
4✔
297
                        HostIP:        "",
4✔
298
                        Protocol:      strings.ToUpper(protocol),
4✔
299
                })
4✔
300
        }
301

302
        return komposePorts
11✔
303
}
304

305
/*
306
        Convert the HealthCheckConfig as designed by Docker to
307

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

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

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

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

377
/*
378
        Convert the HealthCheckConfig as designed by Docker to
379

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

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

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

404
        if composeHealthCheck.Retries != nil {
6✔
405
                retries = int32(*composeHealthCheck.Retries)
3✔
406
        }
3✔
407

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

416
        if composeHealthCheck.Test != nil {
4✔
417
                test = composeHealthCheck.Test[1:]
1✔
418
        }
1✔
419

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

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

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

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

×
481
                if composeServiceConfig.StopGracePeriod != nil {
×
482
                        serviceConfig.StopGracePeriod = composeServiceConfig.StopGracePeriod.String()
×
483
                }
×
484

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

489
                if err := parseResources(&composeServiceConfig, &serviceConfig); err != nil {
×
490
                        return kobject.KomposeObject{}, err
×
491
                }
×
492

493
                serviceConfig.Restart = composeServiceConfig.Restart
×
494

×
495
                if composeServiceConfig.Deploy != nil {
×
496
                        // Deploy keys
×
497
                        // mode:
×
498
                        serviceConfig.DeployMode = composeServiceConfig.Deploy.Mode
×
499
                        // labels
×
500
                        serviceConfig.DeployLabels = composeServiceConfig.Deploy.Labels
×
501

×
502
                        // restart-policy: deploy.restart_policy.condition will rewrite restart option
×
503
                        // see: https://docs.docker.com/compose/compose-file/#restart_policy
×
504
                        if composeServiceConfig.Deploy.RestartPolicy != nil {
×
505
                                serviceConfig.Restart = composeServiceConfig.Deploy.RestartPolicy.Condition
×
506
                        }
×
507

508
                        // replicas:
509
                        if composeServiceConfig.Deploy.Replicas != nil {
×
510
                                serviceConfig.Replicas = int(*composeServiceConfig.Deploy.Replicas)
×
511
                        }
×
512

513
                        // placement:
514
                        serviceConfig.Placement = loadPlacement(composeServiceConfig.Deploy.Placement)
×
515

×
516
                        if composeServiceConfig.Deploy.UpdateConfig != nil {
×
517
                                serviceConfig.DeployUpdateConfig = *composeServiceConfig.Deploy.UpdateConfig
×
518
                        }
×
519

520
                        if composeServiceConfig.Deploy.EndpointMode == "vip" {
×
521
                                serviceConfig.ServiceType = string(api.ServiceTypeNodePort)
×
522
                        }
×
523
                }
524

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

534
                // HealthCheck Readiness
535
                var readiness, errReadiness = parseHealthCheckReadiness(composeServiceConfig.Labels)
×
536
                if !readiness.Disable {
×
537
                        serviceConfig.HealthChecks.Readiness = readiness
×
538
                        if errReadiness != nil {
×
539
                                return kobject.KomposeObject{}, errors.Wrap(errReadiness, "Unable to parse health check")
×
540
                        }
×
541
                }
542

543
                if serviceConfig.Restart == "unless-stopped" {
×
544
                        log.Warnf("Restart policy 'unless-stopped' in service %s is not supported, convert it to 'always'", name)
×
545
                        serviceConfig.Restart = "always"
×
546
                }
×
547

548
                if composeServiceConfig.Build != nil {
×
549
                        serviceConfig.Build = composeServiceConfig.Build.Context
×
550
                        serviceConfig.Dockerfile = composeServiceConfig.Build.Dockerfile
×
551
                        serviceConfig.BuildArgs = composeServiceConfig.Build.Args
×
552
                        serviceConfig.BuildLabels = composeServiceConfig.Build.Labels
×
553
                }
×
554

555
                // env
556
                parseEnvironment(&composeServiceConfig, &serviceConfig)
×
557

×
558
                // Get env_file
×
559
                serviceConfig.EnvFile = composeServiceConfig.EnvFile
×
560

×
561
                // Parse the ports
×
562
                // v3 uses a new format called "long syntax" starting in 3.2
×
563
                // https://docs.docker.com/compose/compose-file/#ports
×
564

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

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

576
                // Log if the name will been changed
577
                if normalizeServiceNames(name) != name {
×
578
                        log.Infof("Service name in docker-compose has been changed from %q to %q", name, normalizeServiceNames(name))
×
579
                }
×
580

581
                serviceConfig.Configs = composeServiceConfig.Configs
×
582
                serviceConfig.ConfigsMetaData = composeObject.Configs
×
583

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

×
591
                // Final step, add to the array!
×
592
                komposeObject.ServiceConfigs[normalizeServiceNames(name)] = serviceConfig
×
593
        }
594

595
        handleVolume(&komposeObject, &composeObject.Volumes)
×
596
        return komposeObject, nil
×
597
}
598

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

×
614
                        // if Network Name Field is empty in the docker-compose definition
×
615
                        // we will use the alias name defined in service config file
×
616
                        if netName == "" {
×
617
                                netName = alias
×
618
                        }
×
619

620
                        normalizedNetworkName, err := normalizeNetworkNames(netName)
×
621
                        if err != nil {
×
622
                                return errors.Wrap(err, "Unable to normalize network name")
×
623
                        }
×
624

625
                        serviceConfig.Network = append(serviceConfig.Network, normalizedNetworkName)
×
626
                }
627
        }
628

629
        return nil
×
630
}
631

632
func parseResources(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig) error {
×
633
        serviceConfig.MemLimit = composeServiceConfig.MemLimit
×
634

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

×
640
                // Since Deploy.Resources.Limits does not initialize, we must check type Resources before continuing
×
641
                if composeServiceConfig.Deploy.Resources.Limits != nil {
×
642
                        serviceConfig.MemLimit = composeServiceConfig.Deploy.Resources.Limits.MemoryBytes
×
643

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

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

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

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

697
        for key, value := range labels {
×
698
                switch key {
×
699
                case LabelServiceType:
×
700
                        serviceType, err := handleServiceType(value)
×
701
                        if err != nil {
×
702
                                return errors.Wrap(err, "handleServiceType failed")
×
703
                        }
×
704

705
                        serviceConfig.ServiceType = serviceType
×
706
                case LabelServiceExternalTrafficPolicy:
×
707
                        serviceExternalTypeTrafficPolicy, err := handleServiceExternalTrafficPolicy(value)
×
708
                        if err != nil {
×
709
                                return errors.Wrap(err, "handleServiceExternalTrafficPolicy failed")
×
710
                        }
×
711

712
                        serviceConfig.ServiceExternalTrafficPolicy = serviceExternalTypeTrafficPolicy
×
713
                case LabelSecurityContextFsGroup:
×
714
                        serviceConfig.FsGroup = cast.ToInt64(value)
×
715
                case LabelServiceExpose:
×
716
                        serviceConfig.ExposeService = strings.Trim(value, " ,")
×
717
                case LabelNodePortPort:
×
718
                        serviceConfig.NodePortPort = cast.ToInt32(value)
×
719
                case LabelServiceExposeTLSSecret:
×
720
                        serviceConfig.ExposeServiceTLS = value
×
721
                case LabelServiceExposeIngressClassName:
×
722
                        serviceConfig.ExposeServiceIngressClassName = value
×
723
                case LabelImagePullSecret:
×
724
                        serviceConfig.ImagePullSecret = value
×
725
                case LabelImagePullPolicy:
×
726
                        serviceConfig.ImagePullPolicy = value
×
727
                case LabelContainerVolumeSubpath:
×
728
                        serviceConfig.VolumeMountSubPath = value
×
729
                default:
×
730
                        serviceConfig.Labels[key] = value
×
731
                }
732
        }
733

734
        if serviceConfig.ExposeService == "" && serviceConfig.ExposeServiceTLS != "" {
×
735
                return errors.New("kompose.service.expose.tls-secret was specified without kompose.service.expose")
×
736
        }
×
737

738
        if serviceConfig.ExposeService == "" && serviceConfig.ExposeServiceIngressClassName != "" {
×
739
                return errors.New("kompose.service.expose.ingress-class-name was specified without kompose.service.expose")
×
740
        }
×
741

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

746
        if len(serviceConfig.Port) > 1 && serviceConfig.NodePortPort != 0 {
×
747
                return errors.New("cannot set kompose.service.nodeport.port when service has multiple ports")
×
748
        }
×
749

750
        return nil
×
751
}
752

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

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

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

830
// checkVolDependent returns false if dependent volume is present
831
func checkVolDependent(dv kobject.Volumes, volume []kobject.Volumes) bool {
×
832
        for _, vol := range volume {
×
833
                if vol.PVCName == dv.PVCName {
×
834
                        return false
×
835
                }
×
836
        }
837
        return true
×
838
}
839

840
// ParseVols parse volumes
841
func ParseVols(volNames []string, svcName string) ([]kobject.Volumes, error) {
×
842
        var volumes []kobject.Volumes
×
843
        var err error
×
844

×
845
        for i, vn := range volNames {
×
846
                var v kobject.Volumes
×
847
                v.VolumeName, v.Host, v.Container, v.Mode, err = transformer.ParseVolume(vn)
×
848
                if err != nil {
×
849
                        return nil, errors.Wrapf(err, "could not parse volume %q: %v", vn, err)
×
850
                }
×
851
                v.VolumeName = normalizeVolumes(v.VolumeName)
×
852
                v.SvcName = svcName
×
853
                v.MountPath = fmt.Sprintf("%s:%s", v.Host, v.Container)
×
854
                v.PVCName = fmt.Sprintf("%s-claim%d", v.SvcName, i)
×
855
                volumes = append(volumes, v)
×
856
        }
857

858
        return volumes, nil
×
859
}
860

861
// for dependent volumes, returns true and the respective volume if mountpath are same
862
func getVol(toFind kobject.Volumes, Vols []kobject.Volumes) (bool, kobject.Volumes) {
×
863
        for _, dv := range Vols {
×
864
                if toFind.MountPath == dv.MountPath {
×
865
                        return true, dv
×
866
                }
×
867
        }
868
        return false, kobject.Volumes{}
×
869
}
870

871
func getVolumeLabels(name string, volumes *types.Volumes) (string, string) {
×
872
        size, selector := "", ""
×
873

×
874
        if volume, ok := (*volumes)[name]; ok {
×
875
                for key, value := range volume.Labels {
×
876
                        if key == "kompose.volume.size" {
×
877
                                size = value
×
878
                        } else if key == "kompose.volume.selector" {
×
879
                                selector = value
×
880
                        }
×
881
                }
882
        }
883

884
        return size, selector
×
885
}
886

887
// getGroupAdd will return group in int64 format
888
func getGroupAdd(group []string) ([]int64, error) {
×
889
        var groupAdd []int64
×
890
        for _, i := range group {
×
891
                j, err := strconv.Atoi(i)
×
892
                if err != nil {
×
893
                        return nil, errors.Wrap(err, "unable to get group_add key")
×
894
                }
×
895
                groupAdd = append(groupAdd, int64(j))
×
896
        }
897
        return groupAdd, nil
×
898
}
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

© 2025 Coveralls, Inc