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  }