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  }