github.com/containerd/nerdctl@v1.7.7/pkg/composer/serviceparser/serviceparser.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package serviceparser 18 19 import ( 20 "bytes" 21 "encoding/csv" 22 "errors" 23 "fmt" 24 "os" 25 "path/filepath" 26 "regexp" 27 "strconv" 28 "strings" 29 "time" 30 31 "github.com/compose-spec/compose-go/types" 32 "github.com/containerd/containerd/contrib/nvidia" 33 "github.com/containerd/containerd/identifiers" 34 "github.com/containerd/log" 35 "github.com/containerd/nerdctl/pkg/reflectutil" 36 ) 37 38 // ComposeExtensionKey defines fields used to implement extension features. 39 const ( 40 ComposeVerify = "x-nerdctl-verify" 41 ComposeCosignPublicKey = "x-nerdctl-cosign-public-key" 42 ComposeSign = "x-nerdctl-sign" 43 ComposeCosignPrivateKey = "x-nerdctl-cosign-private-key" 44 ComposeCosignCertificateIdentity = "x-nerdctl-cosign-certificate-identity" 45 ComposeCosignCertificateIdentityRegexp = "x-nerdctl-cosign-certificate-identity-regexp" 46 ComposeCosignCertificateOidcIssuer = "x-nerdctl-cosign-certificate-oidc-issuer" 47 ComposeCosignCertificateOidcIssuerRegexp = "x-nerdctl-cosign-certificate-oidc-issuer-regexp" 48 ) 49 50 // Separator is used for naming components (e.g., service image or container) 51 // https://github.com/docker/compose/blob/8c39b5b7fd4210a69d07885835f7ff826aaa1cd8/pkg/api/api.go#L483 52 const Separator = "-" 53 54 func warnUnknownFields(svc types.ServiceConfig) { 55 if unknown := reflectutil.UnknownNonEmptyFields(&svc, 56 "Name", 57 "Build", 58 "BlkioConfig", 59 "CapAdd", 60 "CapDrop", 61 "CPUS", 62 "CPUSet", 63 "CPUShares", 64 "Command", 65 "Configs", 66 "ContainerName", 67 "DependsOn", 68 "Deploy", 69 "Devices", 70 "Dockerfile", // handled by the loader (normalizer) 71 "DNS", 72 "DNSSearch", 73 "DNSOpts", 74 "Entrypoint", 75 "Environment", 76 "Extends", // handled by the loader 77 "Extensions", 78 "ExtraHosts", 79 "Hostname", 80 "Image", 81 "Init", 82 "Labels", 83 "Logging", 84 "MemLimit", 85 "Networks", 86 "NetworkMode", 87 "Pid", 88 "PidsLimit", 89 "Platform", 90 "Ports", 91 "Privileged", 92 "PullPolicy", 93 "ReadOnly", 94 "Restart", 95 "Runtime", 96 "Secrets", 97 "Scale", 98 "SecurityOpt", 99 "ShmSize", 100 "StopGracePeriod", 101 "StopSignal", 102 "Sysctls", 103 "StdinOpen", 104 "Tmpfs", 105 "Tty", 106 "User", 107 "WorkingDir", 108 "Volumes", 109 "Ulimits", 110 ); len(unknown) > 0 { 111 log.L.Warnf("Ignoring: service %s: %+v", svc.Name, unknown) 112 } 113 114 if svc.BlkioConfig != nil { 115 if unknown := reflectutil.UnknownNonEmptyFields(svc.BlkioConfig, 116 "Weight", 117 ); len(unknown) > 0 { 118 log.L.Warnf("Ignoring: service %s: blkio_config: %+v", svc.Name, unknown) 119 } 120 } 121 122 for depName, dep := range svc.DependsOn { 123 if unknown := reflectutil.UnknownNonEmptyFields(&dep, 124 "Condition", 125 ); len(unknown) > 0 { 126 log.L.Warnf("Ignoring: service %s: depends_on: %s: %+v", svc.Name, depName, unknown) 127 } 128 switch dep.Condition { 129 case "", types.ServiceConditionStarted: 130 // NOP 131 default: 132 log.L.Warnf("Ignoring: service %s: depends_on: %s: condition %s", svc.Name, depName, dep.Condition) 133 } 134 } 135 136 if svc.Deploy != nil { 137 if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy, 138 "Replicas", 139 "RestartPolicy", 140 "Resources", 141 ); len(unknown) > 0 { 142 log.L.Warnf("Ignoring: service %s: deploy: %+v", svc.Name, unknown) 143 } 144 if svc.Deploy.RestartPolicy != nil { 145 if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.RestartPolicy, 146 "Condition", 147 ); len(unknown) > 0 { 148 log.L.Warnf("Ignoring: service %s: deploy.restart_policy: %+v", svc.Name, unknown) 149 } 150 } 151 if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.Resources, 152 "Limits", 153 "Reservations", 154 ); len(unknown) > 0 { 155 log.L.Warnf("Ignoring: service %s: deploy.resources: %+v", svc.Name, unknown) 156 } 157 if svc.Deploy.Resources.Limits != nil { 158 if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.Resources.Limits, 159 "NanoCPUs", 160 "MemoryBytes", 161 ); len(unknown) > 0 { 162 log.L.Warnf("Ignoring: service %s: deploy.resources.resources: %+v", svc.Name, unknown) 163 } 164 } 165 if svc.Deploy.Resources.Reservations != nil { 166 if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.Resources.Reservations, 167 "Devices", 168 ); len(unknown) > 0 { 169 log.L.Warnf("Ignoring: service %s: deploy.resources.resources.reservations: %+v", svc.Name, unknown) 170 } 171 for i, dev := range svc.Deploy.Resources.Reservations.Devices { 172 if unknown := reflectutil.UnknownNonEmptyFields(dev, 173 "Capabilities", 174 "Driver", 175 "Count", 176 "IDs", 177 ); len(unknown) > 0 { 178 log.L.Warnf("Ignoring: service %s: deploy.resources.resources.reservations.devices[%d]: %+v", 179 svc.Name, i, unknown) 180 } 181 } 182 } 183 } 184 185 // unknown fields of Build is checked in parseBuild(). 186 } 187 188 type Container struct { 189 Name string // e.g., "compose-wordpress_wordpress_1" 190 RunArgs []string // {"--pull=never", ...} 191 Mkdir []string // For Bind.CreateHostPath 192 } 193 194 type Build struct { 195 Force bool // force build even if already present 196 BuildArgs []string // {"-t", "example.com/foo", "--target", "foo", "/path/to/ctx"} 197 // TODO: call BuildKit API directly without executing `nerdctl build` 198 } 199 200 type Service struct { 201 Image string 202 PullMode string 203 Containers []Container // length = replicas 204 Build *Build 205 Unparsed *types.ServiceConfig 206 } 207 208 func getReplicas(svc types.ServiceConfig) (int, error) { 209 replicas := 1 210 211 // No need to check svc.Scale, as it is automatically transformed to svc.Deploy.Replicas by compose-go 212 // https://github.com/compose-spec/compose-go/commit/958cb4f953330a3d1303961796d826b7f79132d7 213 214 if svc.Deploy != nil && svc.Deploy.Replicas != nil { 215 replicas = int(*svc.Deploy.Replicas) 216 } 217 218 if replicas < 0 { 219 return 0, fmt.Errorf("invalid replicas: %d", replicas) 220 } 221 return replicas, nil 222 } 223 224 func getCPULimit(svc types.ServiceConfig) (string, error) { 225 var limit string 226 if svc.CPUS > 0 { 227 log.L.Warn("cpus is deprecated, use deploy.resources.limits.cpus") 228 limit = fmt.Sprintf("%f", svc.CPUS) 229 } 230 if svc.Deploy != nil && svc.Deploy.Resources.Limits != nil { 231 if nanoCPUs := svc.Deploy.Resources.Limits.NanoCPUs; nanoCPUs != "" { 232 if svc.CPUS > 0 { 233 log.L.Warnf("deploy.resources.limits.cpus and cpus (deprecated) must not be set together, ignoring cpus=%f", svc.CPUS) 234 } 235 limit = nanoCPUs 236 } 237 } 238 return limit, nil 239 } 240 241 func getMemLimit(svc types.ServiceConfig) (types.UnitBytes, error) { 242 var limit types.UnitBytes 243 if svc.MemLimit > 0 { 244 log.L.Warn("mem_limit is deprecated, use deploy.resources.limits.memory") 245 limit = svc.MemLimit 246 } 247 if svc.Deploy != nil && svc.Deploy.Resources.Limits != nil { 248 if memoryBytes := svc.Deploy.Resources.Limits.MemoryBytes; memoryBytes > 0 { 249 if svc.MemLimit > 0 && memoryBytes != svc.MemLimit { 250 log.L.Warnf("deploy.resources.limits.memory and mem_limit (deprecated) must not be set together, ignoring mem_limit=%d", svc.MemLimit) 251 } 252 limit = memoryBytes 253 } 254 } 255 return limit, nil 256 } 257 258 func getGPUs(svc types.ServiceConfig) (reqs []string, _ error) { 259 // "gpu" and "nvidia" are also allowed capabilities (but not used as nvidia driver capabilities) 260 // https://github.com/moby/moby/blob/v20.10.7/daemon/nvidia_linux.go#L37 261 capset := map[string]struct{}{"gpu": {}, "nvidia": {}} 262 for _, c := range nvidia.AllCaps() { 263 capset[string(c)] = struct{}{} 264 } 265 if svc.Deploy != nil && svc.Deploy.Resources.Reservations != nil { 266 for _, dev := range svc.Deploy.Resources.Reservations.Devices { 267 if len(dev.Capabilities) == 0 { 268 // "capabilities" is required. 269 // https://github.com/compose-spec/compose-spec/blob/74b933db994109616580eab8f47bf2ba226e0faa/deploy.md#devices 270 return nil, fmt.Errorf("service %s: specifying \"capabilities\" is required for resource reservations", svc.Name) 271 } 272 273 var requiresGPU bool 274 for _, c := range dev.Capabilities { 275 if _, ok := capset[c]; ok { 276 requiresGPU = true 277 } 278 } 279 if !requiresGPU { 280 continue 281 } 282 283 var e []string 284 if len(dev.Capabilities) > 0 { 285 e = append(e, fmt.Sprintf("capabilities=%s", strings.Join(dev.Capabilities, ","))) 286 } 287 if dev.Driver != "" { 288 e = append(e, fmt.Sprintf("driver=%s", dev.Driver)) 289 } 290 if len(dev.IDs) > 0 { 291 e = append(e, fmt.Sprintf("device=%s", strings.Join(dev.IDs, ","))) 292 } 293 if dev.Count != 0 { 294 e = append(e, fmt.Sprintf("count=%d", dev.Count)) 295 } 296 297 buf := new(bytes.Buffer) 298 w := csv.NewWriter(buf) 299 if err := w.Write(e); err != nil { 300 return nil, err 301 } 302 w.Flush() 303 o := buf.Bytes() 304 if len(o) > 0 { 305 reqs = append(reqs, string(o[:len(o)-1])) // remove carriage return 306 } 307 } 308 } 309 return reqs, nil 310 } 311 312 var restartFailurePat = regexp.MustCompile(`^on-failure:\d+$`) 313 314 // getRestart returns `nerdctl run --restart` flag string 315 // 316 // restart: {"no" (default), "always", "on-failure", "unless-stopped"} (https://github.com/compose-spec/compose-spec/blob/167f207d0a8967df87c5ed757dbb1a2bb6025a1e/spec.md#restart) 317 // deploy.restart_policy.condition: {"none", "on-failure", "any" (default)} (https://github.com/compose-spec/compose-spec/blob/167f207d0a8967df87c5ed757dbb1a2bb6025a1e/deploy.md#restart_policy) 318 func getRestart(svc types.ServiceConfig) (string, error) { 319 var restartFlag string 320 switch svc.Restart { 321 case "": 322 restartFlag = "no" 323 case "no", "always", "on-failure", "unless-stopped": 324 restartFlag = svc.Restart 325 default: 326 if restartFailurePat.MatchString(svc.Restart) { 327 restartFlag = svc.Restart 328 } else { 329 log.L.Warnf("Ignoring: service %s: restart=%q (unknown)", svc.Name, svc.Restart) 330 } 331 } 332 333 if svc.Deploy != nil && svc.Deploy.RestartPolicy != nil { 334 if svc.Restart != "" { 335 log.L.Warnf("deploy.restart_policy and restart must not be set together, ignoring restart=%s", svc.Restart) 336 } 337 switch cond := svc.Deploy.RestartPolicy.Condition; cond { 338 case "", "any": 339 restartFlag = "always" 340 case "always": 341 return "", fmt.Errorf("deploy.restart_policy.condition: \"always\" is invalid, did you mean \"any\"?") 342 case "none": 343 restartFlag = "no" 344 case "no": 345 return "", fmt.Errorf("deploy.restart_policy.condition: \"no\" is invalid, did you mean \"none\"?") 346 case "on-failure": 347 log.L.Warnf("Ignoring: service %s: deploy.restart_policy.condition=%q (unimplemented)", svc.Name, cond) 348 default: 349 log.L.Warnf("Ignoring: service %s: deploy.restart_policy.condition=%q (unknown)", svc.Name, cond) 350 } 351 } 352 353 return restartFlag, nil 354 } 355 356 type networkNamePair struct { 357 shortNetworkName string 358 fullName string 359 } 360 361 // getNetworks returns full network names, e.g., {"compose-wordpress_default"}, or {"host"} 362 func getNetworks(project *types.Project, svc types.ServiceConfig) ([]networkNamePair, error) { 363 var fullNames []networkNamePair // nolint: prealloc 364 365 if svc.Net != "" { 366 log.L.Warn("net is deprecated, use network_mode or networks") 367 if len(svc.Networks) > 0 { 368 return nil, errors.New("networks and net must not be set together") 369 } 370 371 fullNames = append(fullNames, networkNamePair{ 372 fullName: svc.Net, 373 shortNetworkName: "", 374 }) 375 } 376 377 if svc.NetworkMode != "" { 378 if len(svc.Networks) > 0 { 379 return nil, errors.New("networks and network_mode must not be set together") 380 } 381 if svc.Net != "" && svc.NetworkMode != svc.Net { 382 return nil, errors.New("net and network_mode must not be set together") 383 } 384 if strings.Contains(svc.NetworkMode, ":") { 385 if !strings.HasPrefix(svc.NetworkMode, "container:") { 386 return nil, fmt.Errorf("unsupported network_mode: %q", svc.NetworkMode) 387 } 388 } 389 fullNames = append(fullNames, networkNamePair{ 390 fullName: svc.NetworkMode, 391 shortNetworkName: "", 392 }) 393 } 394 395 for shortName := range svc.Networks { 396 net, ok := project.Networks[shortName] 397 if !ok { 398 return nil, fmt.Errorf("invalid network %q", shortName) 399 } 400 fullNames = append(fullNames, networkNamePair{ 401 fullName: net.Name, 402 shortNetworkName: shortName, 403 }) 404 } 405 406 return fullNames, nil 407 } 408 409 func Parse(project *types.Project, svc types.ServiceConfig) (*Service, error) { 410 warnUnknownFields(svc) 411 412 replicas, err := getReplicas(svc) 413 if err != nil { 414 return nil, err 415 } 416 417 parsed := &Service{ 418 Image: svc.Image, 419 PullMode: "missing", 420 Containers: make([]Container, replicas), 421 Unparsed: &svc, 422 } 423 424 if svc.Build == nil { 425 if parsed.Image == "" { 426 return nil, fmt.Errorf("service %s: missing image", svc.Name) 427 } 428 } else { 429 if parsed.Image == "" { 430 parsed.Image = DefaultImageName(project.Name, svc.Name) 431 } 432 parsed.Build, err = parseBuildConfig(svc.Build, project, parsed.Image) 433 if err != nil { 434 return nil, fmt.Errorf("service %s: failed to parse build: %w", svc.Name, err) 435 } 436 } 437 438 switch svc.PullPolicy { 439 case "", types.PullPolicyMissing, types.PullPolicyIfNotPresent: 440 // NOP 441 case types.PullPolicyAlways, types.PullPolicyNever: 442 parsed.PullMode = svc.PullPolicy 443 case types.PullPolicyBuild: 444 if parsed.Build == nil { 445 return nil, fmt.Errorf("service %s: pull_policy \"build\" requires build config", svc.Name) 446 } 447 parsed.Build.Force = true 448 parsed.PullMode = "never" 449 default: 450 log.L.Warnf("Ignoring: service %s: pull_policy: %q", svc.Name, svc.PullPolicy) 451 } 452 453 for i := 0; i < replicas; i++ { 454 container, err := newContainer(project, parsed, i) 455 if err != nil { 456 return nil, err 457 } 458 parsed.Containers[i] = *container 459 } 460 461 return parsed, nil 462 } 463 464 func newContainer(project *types.Project, parsed *Service, i int) (*Container, error) { 465 svc := *parsed.Unparsed 466 var c Container 467 c.Name = DefaultContainerName(project.Name, svc.Name, strconv.Itoa(i+1)) 468 if svc.ContainerName != "" { 469 if i != 0 { 470 return nil, errors.New("container_name must not be specified when replicas != 1") 471 } 472 c.Name = svc.ContainerName 473 } 474 475 c.RunArgs = []string{ 476 "--name=" + c.Name, 477 "--pull=never", // because image will be ensured before running replicas with `nerdctl run`. 478 } 479 480 if svc.BlkioConfig != nil && svc.BlkioConfig.Weight != 0 { 481 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--blkio-weight=%d", svc.BlkioConfig.Weight)) 482 } 483 484 for _, v := range svc.CapAdd { 485 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cap-add=%s", v)) 486 } 487 488 for _, v := range svc.CapDrop { 489 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cap-drop=%s", v)) 490 } 491 492 if cpuLimit, err := getCPULimit(svc); err != nil { 493 return nil, err 494 } else if cpuLimit != "" { 495 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cpus=%s", cpuLimit)) 496 } 497 498 if svc.CPUSet != "" { 499 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cpuset-cpus=%s", svc.CPUSet)) 500 } 501 502 if svc.CPUShares != 0 { 503 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cpu-shares=%d", svc.CPUShares)) 504 } 505 506 for _, v := range svc.Devices { 507 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--device=%s", v)) 508 } 509 510 for _, v := range svc.DNS { 511 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--dns=%s", v)) 512 } 513 for _, v := range svc.DNSSearch { 514 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--dns-search=%s", v)) 515 } 516 for _, v := range svc.DNSOpts { 517 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--dns-option=%s", v)) 518 } 519 520 for _, v := range svc.Entrypoint { 521 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--entrypoint=%s", v)) 522 } 523 524 for k, v := range svc.Environment { 525 if v == nil { 526 c.RunArgs = append(c.RunArgs, fmt.Sprintf("-e=%s", k)) 527 } else { 528 c.RunArgs = append(c.RunArgs, fmt.Sprintf("-e=%s=%s", k, *v)) 529 } 530 } 531 for k, v := range svc.ExtraHosts { 532 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--add-host=%s:%s", k, v)) 533 } 534 535 if svc.Init != nil && *svc.Init { 536 c.RunArgs = append(c.RunArgs, "--init") 537 } 538 539 if memLimit, err := getMemLimit(svc); err != nil { 540 return nil, err 541 } else if memLimit > 0 { 542 c.RunArgs = append(c.RunArgs, fmt.Sprintf("-m=%d", memLimit)) 543 } 544 545 if gpuReqs, err := getGPUs(svc); err != nil { 546 return nil, err 547 } else if len(gpuReqs) > 0 { 548 for _, gpus := range gpuReqs { 549 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--gpus=%s", gpus)) 550 } 551 } 552 553 for k, v := range svc.Labels { 554 if v == "" { 555 c.RunArgs = append(c.RunArgs, fmt.Sprintf("-l=%s", k)) 556 } else { 557 c.RunArgs = append(c.RunArgs, fmt.Sprintf("-l=%s=%s", k, v)) 558 } 559 } 560 561 if svc.Logging != nil { 562 if svc.Logging.Driver != "" { 563 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--log-driver=%s", svc.Logging.Driver)) 564 } 565 if svc.Logging.Options != nil { 566 for k, v := range svc.Logging.Options { 567 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--log-opt=%s=%s", k, v)) 568 } 569 } 570 } 571 572 networks, err := getNetworks(project, svc) 573 if err != nil { 574 return nil, err 575 } 576 netTypeContainer := false 577 for _, net := range networks { 578 if strings.HasPrefix(net.fullName, "container:") { 579 netTypeContainer = true 580 } 581 c.RunArgs = append(c.RunArgs, "--net="+net.fullName) 582 if value, ok := svc.Networks[net.shortNetworkName]; ok { 583 if value != nil && value.Ipv4Address != "" { 584 c.RunArgs = append(c.RunArgs, "--ip="+value.Ipv4Address) 585 } 586 } 587 } 588 589 if netTypeContainer && svc.Hostname != "" { 590 return nil, fmt.Errorf("conflicting options: hostname and container network mode") 591 } 592 if !netTypeContainer { 593 hostname := svc.Hostname 594 if hostname == "" { 595 hostname = svc.Name 596 } 597 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--hostname=%s", hostname)) 598 } 599 600 if svc.Pid != "" { 601 c.RunArgs = append(c.RunArgs, "--pid="+svc.Pid) 602 } 603 604 if svc.PidsLimit > 0 { 605 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--pids-limit=%d", svc.PidsLimit)) 606 } 607 608 if svc.Ulimits != nil { 609 for utype, ulimit := range svc.Ulimits { 610 if ulimit.Single != 0 { 611 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--ulimit=%s=%d", utype, ulimit.Single)) 612 } else { 613 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--ulimit=%s=%d:%d", utype, ulimit.Soft, ulimit.Hard)) 614 } 615 } 616 } 617 618 if svc.Platform != "" { 619 c.RunArgs = append(c.RunArgs, "--platform="+svc.Platform) 620 } 621 622 for _, p := range svc.Ports { 623 pStr, err := servicePortConfigToFlagP(p) 624 if err != nil { 625 return nil, err 626 } 627 c.RunArgs = append(c.RunArgs, "-p="+pStr) 628 } 629 630 if svc.Privileged { 631 c.RunArgs = append(c.RunArgs, "--privileged") 632 } 633 634 if svc.ReadOnly { 635 c.RunArgs = append(c.RunArgs, "--read-only") 636 } 637 638 if svc.StopGracePeriod != nil { 639 timeout := time.Duration(*svc.StopGracePeriod) 640 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--stop-timeout=%d", int(timeout.Seconds()))) 641 } 642 if svc.StopSignal != "" { 643 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--stop-signal=%s", svc.StopSignal)) 644 } 645 646 if restart, err := getRestart(svc); err != nil { 647 return nil, err 648 } else if restart != "" { 649 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--restart=%s", restart)) 650 } 651 652 if svc.Runtime != "" { 653 c.RunArgs = append(c.RunArgs, "--runtime="+svc.Runtime) 654 } 655 656 if svc.ShmSize > 0 { 657 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--shm-size=%d", svc.ShmSize)) 658 } 659 660 for _, v := range svc.SecurityOpt { 661 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--security-opt=%s", v)) 662 } 663 664 for k, v := range svc.Sysctls { 665 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--sysctl=%s=%s", k, v)) 666 } 667 668 if svc.StdinOpen { 669 c.RunArgs = append(c.RunArgs, "--interactive") 670 } 671 672 if svc.User != "" { 673 c.RunArgs = append(c.RunArgs, "--user="+svc.User) 674 } 675 676 for _, v := range svc.GroupAdd { 677 c.RunArgs = append(c.RunArgs, fmt.Sprintf("--group-add=%s", v)) 678 } 679 680 for _, v := range svc.Volumes { 681 vStr, mkdir, err := serviceVolumeConfigToFlagV(v, project) 682 if err != nil { 683 return nil, err 684 } 685 c.RunArgs = append(c.RunArgs, "-v="+vStr) 686 c.Mkdir = mkdir 687 } 688 689 for _, config := range svc.Configs { 690 fileRef := types.FileReferenceConfig(config) 691 vStr, err := fileReferenceConfigToFlagV(fileRef, project, false) 692 if err != nil { 693 return nil, err 694 } 695 c.RunArgs = append(c.RunArgs, "-v="+vStr) 696 } 697 698 for _, secret := range svc.Secrets { 699 fileRef := types.FileReferenceConfig(secret) 700 vStr, err := fileReferenceConfigToFlagV(fileRef, project, true) 701 if err != nil { 702 return nil, err 703 } 704 c.RunArgs = append(c.RunArgs, "-v="+vStr) 705 } 706 707 for _, tmpfs := range svc.Tmpfs { 708 c.RunArgs = append(c.RunArgs, "--tmpfs="+tmpfs) 709 } 710 711 if svc.Tty { 712 c.RunArgs = append(c.RunArgs, "--tty") 713 } 714 715 if svc.WorkingDir != "" { 716 c.RunArgs = append(c.RunArgs, "-w="+svc.WorkingDir) 717 } 718 719 c.RunArgs = append(c.RunArgs, parsed.Image) // NOT svc.Image 720 c.RunArgs = append(c.RunArgs, svc.Command...) 721 return &c, nil 722 } 723 724 func servicePortConfigToFlagP(c types.ServicePortConfig) (string, error) { 725 if unknown := reflectutil.UnknownNonEmptyFields(&c, 726 "Mode", 727 "HostIP", 728 "Target", 729 "Published", 730 "Protocol", 731 ); len(unknown) > 0 { 732 log.L.Warnf("Ignoring: port: %+v", unknown) 733 } 734 switch c.Mode { 735 case "", "ingress": 736 default: 737 return "", fmt.Errorf("unsupported port mode: %s", c.Mode) 738 } 739 if c.Target <= 0 { 740 return "", fmt.Errorf("unsupported port number: %d", c.Target) 741 } 742 s := fmt.Sprintf("%s:%d", c.Published, c.Target) 743 if c.HostIP != "" { 744 if strings.Contains(c.HostIP, ":") { 745 s = fmt.Sprintf("[%s]:%s", c.HostIP, s) 746 } else { 747 s = fmt.Sprintf("%s:%s", c.HostIP, s) 748 } 749 } 750 if c.Protocol != "" { 751 s = fmt.Sprintf("%s/%s", s, c.Protocol) 752 } 753 return s, nil 754 } 755 756 func serviceVolumeConfigToFlagV(c types.ServiceVolumeConfig, project *types.Project) (flagV string, mkdir []string, err error) { 757 if unknown := reflectutil.UnknownNonEmptyFields(&c, 758 "Type", 759 "Source", 760 "Target", 761 "ReadOnly", 762 "Bind", 763 "Volume", 764 ); len(unknown) > 0 { 765 log.L.Warnf("Ignoring: volume: %+v", unknown) 766 } 767 if c.Bind != nil { 768 // c.Bind is expected to be a non-nil reference to an empty Bind struct 769 if unknown := reflectutil.UnknownNonEmptyFields(c.Bind, "CreateHostPath"); len(unknown) > 0 { 770 log.L.Warnf("Ignoring: volume: Bind: %+v", unknown) 771 } 772 } 773 if c.Volume != nil { 774 // c.Volume is expected to be a non-nil reference to an empty Volume struct 775 if unknown := reflectutil.UnknownNonEmptyFields(c.Volume); len(unknown) > 0 { 776 log.L.Warnf("Ignoring: volume: Volume: %+v", unknown) 777 } 778 } 779 780 if c.Target == "" { 781 return "", nil, errors.New("volume target is missing") 782 } 783 if !filepath.IsAbs(c.Target) { 784 return "", nil, fmt.Errorf("volume target must be an absolute path, got %q", c.Target) 785 } 786 787 if c.Source == "" { 788 // anonymous volume 789 s := c.Target 790 if c.ReadOnly { 791 s += ":ro" 792 } 793 return s, mkdir, nil 794 } 795 796 var src string 797 switch c.Type { 798 case "volume": 799 vol, ok := project.Volumes[c.Source] 800 if !ok { 801 return "", nil, fmt.Errorf("invalid volume %q", c.Source) 802 } 803 // c.Source is like "db_data", vol.Name is like "compose-wordpress_db_data" 804 src = vol.Name 805 case "bind": 806 src = project.RelativePath(c.Source) 807 var err error 808 src, err = filepath.Abs(src) 809 if err != nil { 810 return "", nil, fmt.Errorf("invalid relative path %q: %w", c.Source, err) 811 } 812 if c.Bind != nil && c.Bind.CreateHostPath { 813 if _, stErr := os.Stat(src); errors.Is(stErr, os.ErrNotExist) { 814 mkdir = append(mkdir, src) 815 } 816 } 817 default: 818 return "", nil, fmt.Errorf("unsupported volume type: %q", c.Type) 819 } 820 s := fmt.Sprintf("%s:%s", src, c.Target) 821 if c.ReadOnly { 822 s += ":ro" 823 } 824 return s, mkdir, nil 825 } 826 827 func fileReferenceConfigToFlagV(c types.FileReferenceConfig, project *types.Project, secret bool) (string, error) { 828 objType := "config" 829 if secret { 830 objType = "secret" 831 } 832 if unknown := reflectutil.UnknownNonEmptyFields(&c, 833 "Source", "Target", "UID", "GID", "Mode", 834 ); len(unknown) > 0 { 835 log.L.Warnf("Ignoring: %s: %+v", objType, unknown) 836 } 837 838 if err := identifiers.Validate(c.Source); err != nil { 839 return "", fmt.Errorf("%s source %q is invalid: %w", objType, c.Source, err) 840 } 841 842 var obj types.FileObjectConfig 843 if secret { 844 secret, ok := project.Secrets[c.Source] 845 if !ok { 846 return "", fmt.Errorf("secret %s is undefined", c.Source) 847 } 848 obj = types.FileObjectConfig(secret) 849 } else { 850 config, ok := project.Configs[c.Source] 851 if !ok { 852 return "", fmt.Errorf("config %s is undefined", c.Source) 853 } 854 obj = types.FileObjectConfig(config) 855 } 856 src := project.RelativePath(obj.File) 857 var err error 858 src, err = filepath.Abs(src) 859 if err != nil { 860 return "", fmt.Errorf("%s %s: invalid relative path %q: %w", objType, c.Source, src, err) 861 } 862 863 target := c.Target 864 if target == "" { 865 if secret { 866 target = filepath.Join("/run/secrets", c.Source) 867 } else { 868 target = filepath.Join("/", c.Source) 869 } 870 } else { 871 target = filepath.Clean(target) 872 if !filepath.IsAbs(target) { 873 if secret { 874 target = filepath.Join("/run/secrets", target) 875 } else { 876 return "", fmt.Errorf("config %s: target %q must be an absolute path", c.Source, c.Target) 877 } 878 } 879 } 880 881 if c.UID != "" { 882 // Raise an error rather than ignoring the value, for avoiding any security issue 883 return "", fmt.Errorf("%s %s: unsupported field: UID", objType, c.Source) 884 } 885 if c.GID != "" { 886 return "", fmt.Errorf("%s %s: unsupported field: GID", objType, c.Source) 887 } 888 if c.Mode != nil { 889 return "", fmt.Errorf("%s %s: unsupported field: Mode", objType, c.Source) 890 } 891 892 s := fmt.Sprintf("%s:%s:ro", src, target) 893 return s, nil 894 } 895 896 // DefaultImageName returns the image name following compose naming logic. 897 func DefaultImageName(projectName string, serviceName string) string { 898 return projectName + Separator + serviceName 899 } 900 901 // DefaultContainerName returns the service container name following compose naming logic. 902 func DefaultContainerName(projectName, serviceName, suffix string) string { 903 return DefaultImageName(projectName, serviceName) + Separator + suffix 904 }