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