github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/composer/serviceparser/serviceparser_test.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  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"strconv"
    24  	"testing"
    25  
    26  	"github.com/compose-spec/compose-go/types"
    27  	"github.com/containerd/nerdctl/v2/pkg/composer/projectloader"
    28  	"github.com/containerd/nerdctl/v2/pkg/strutil"
    29  	"github.com/containerd/nerdctl/v2/pkg/testutil"
    30  	"gotest.tools/v3/assert"
    31  )
    32  
    33  func TestServicePortConfigToFlagP(t *testing.T) {
    34  	t.Parallel()
    35  	type testCase struct {
    36  		types.ServicePortConfig
    37  		expected string
    38  	}
    39  	testCases := []testCase{
    40  		{
    41  			ServicePortConfig: types.ServicePortConfig{
    42  				Mode:      "ingress",
    43  				Target:    80,
    44  				Published: "8080",
    45  				Protocol:  "tcp",
    46  			},
    47  			expected: "8080:80/tcp",
    48  		},
    49  		{
    50  			ServicePortConfig: types.ServicePortConfig{
    51  				HostIP:    "127.0.0.1",
    52  				Target:    80,
    53  				Published: "8080",
    54  			},
    55  			expected: "127.0.0.1:8080:80",
    56  		},
    57  		{
    58  			ServicePortConfig: types.ServicePortConfig{
    59  				HostIP: "127.0.0.1",
    60  				Target: 80,
    61  			},
    62  			expected: "127.0.0.1::80",
    63  		},
    64  	}
    65  	for i, tc := range testCases {
    66  		got, err := servicePortConfigToFlagP(tc.ServicePortConfig)
    67  		if tc.expected == "" {
    68  			if err == nil {
    69  				t.Errorf("#%d: error is expected", i)
    70  			}
    71  			continue
    72  		}
    73  		assert.NilError(t, err)
    74  		assert.Equal(t, tc.expected, got)
    75  	}
    76  }
    77  
    78  var in = strutil.InStringSlice
    79  
    80  func TestParse(t *testing.T) {
    81  	t.Parallel()
    82  	const dockerComposeYAML = `
    83  version: '3.1'
    84  
    85  services:
    86  
    87    wordpress:
    88      ulimits:
    89        nproc: 500
    90        nofile:
    91          soft: 20000
    92          hard: 20000
    93      image: wordpress:5.7
    94      restart: always
    95      ports:
    96        - 8080:80
    97      extra_hosts:
    98        test.com: 172.19.1.1
    99        test2.com: 172.19.1.2
   100      environment:
   101        WORDPRESS_DB_HOST: db
   102        WORDPRESS_DB_USER: exampleuser
   103        WORDPRESS_DB_PASSWORD: examplepass
   104        WORDPRESS_DB_NAME: exampledb
   105      volumes:
   106        - wordpress:/var/www/html
   107      pids_limit: 100
   108      shm_size: 1G
   109      dns:
   110        - 8.8.8.8
   111        - 8.8.4.4
   112      dns_search: example.com
   113      dns_opt:
   114        - no-tld-query
   115      logging:
   116        driver: json-file
   117        options:
   118          max-size: "5K"
   119          max-file: "2"
   120      user: 1001:1001
   121      group_add:
   122        - "1001"
   123  
   124    db:
   125      image: mariadb:10.5
   126      restart: always
   127      environment:
   128        MYSQL_DATABASE: exampledb
   129        MYSQL_USER: exampleuser
   130        MYSQL_PASSWORD: examplepass
   131        MYSQL_RANDOM_ROOT_PASSWORD: '1'
   132      volumes:
   133        - db:/var/lib/mysql
   134      stop_grace_period: 1m30s
   135      stop_signal: SIGUSR1
   136  
   137  volumes:
   138    wordpress:
   139    db:
   140  `
   141  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   142  	defer comp.CleanUp()
   143  
   144  	project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
   145  	assert.NilError(t, err)
   146  
   147  	wpSvc, err := project.GetService("wordpress")
   148  	assert.NilError(t, err)
   149  
   150  	wp, err := Parse(project, wpSvc)
   151  	assert.NilError(t, err)
   152  
   153  	t.Logf("wordpress: %+v", wp)
   154  	assert.Assert(t, wp.PullMode == "missing")
   155  	assert.Assert(t, wp.Image == "wordpress:5.7")
   156  	assert.Assert(t, len(wp.Containers) == 1)
   157  	wp1 := wp.Containers[0]
   158  	assert.Assert(t, wp1.Name == DefaultContainerName(project.Name, "wordpress", "1"))
   159  	assert.Assert(t, in(wp1.RunArgs, "--name="+wp1.Name))
   160  	assert.Assert(t, in(wp1.RunArgs, "--hostname=wordpress"))
   161  	assert.Assert(t, in(wp1.RunArgs, fmt.Sprintf("--net=%s_default", project.Name)))
   162  	assert.Assert(t, in(wp1.RunArgs, "--restart=always"))
   163  	assert.Assert(t, in(wp1.RunArgs, "-e=WORDPRESS_DB_HOST=db"))
   164  	assert.Assert(t, in(wp1.RunArgs, "-e=WORDPRESS_DB_USER=exampleuser"))
   165  	assert.Assert(t, in(wp1.RunArgs, "-p=8080:80/tcp"))
   166  	assert.Assert(t, in(wp1.RunArgs, fmt.Sprintf("-v=%s_wordpress:/var/www/html", project.Name)))
   167  	assert.Assert(t, in(wp1.RunArgs, "--pids-limit=100"))
   168  	assert.Assert(t, in(wp1.RunArgs, "--ulimit=nproc=500"))
   169  	assert.Assert(t, in(wp1.RunArgs, "--ulimit=nofile=20000:20000"))
   170  	assert.Assert(t, in(wp1.RunArgs, "--dns=8.8.8.8"))
   171  	assert.Assert(t, in(wp1.RunArgs, "--dns=8.8.4.4"))
   172  	assert.Assert(t, in(wp1.RunArgs, "--dns-search=example.com"))
   173  	assert.Assert(t, in(wp1.RunArgs, "--dns-option=no-tld-query"))
   174  	assert.Assert(t, in(wp1.RunArgs, "--log-driver=json-file"))
   175  	assert.Assert(t, in(wp1.RunArgs, "--log-opt=max-size=5K"))
   176  	assert.Assert(t, in(wp1.RunArgs, "--log-opt=max-file=2"))
   177  	assert.Assert(t, in(wp1.RunArgs, "--add-host=test.com:172.19.1.1"))
   178  	assert.Assert(t, in(wp1.RunArgs, "--add-host=test2.com:172.19.1.2"))
   179  	assert.Assert(t, in(wp1.RunArgs, "--shm-size=1073741824"))
   180  	assert.Assert(t, in(wp1.RunArgs, "--user=1001:1001"))
   181  	assert.Assert(t, in(wp1.RunArgs, "--group-add=1001"))
   182  
   183  	dbSvc, err := project.GetService("db")
   184  	assert.NilError(t, err)
   185  
   186  	db, err := Parse(project, dbSvc)
   187  	assert.NilError(t, err)
   188  
   189  	t.Logf("db: %+v", db)
   190  	assert.Assert(t, len(db.Containers) == 1)
   191  	db1 := db.Containers[0]
   192  	assert.Assert(t, db1.Name == DefaultContainerName(project.Name, "db", "1"))
   193  	assert.Assert(t, in(db1.RunArgs, "--hostname=db"))
   194  	assert.Assert(t, in(db1.RunArgs, fmt.Sprintf("-v=%s_db:/var/lib/mysql", project.Name)))
   195  	assert.Assert(t, in(db1.RunArgs, "--stop-signal=SIGUSR1"))
   196  	assert.Assert(t, in(db1.RunArgs, "--stop-timeout=90"))
   197  }
   198  
   199  func TestParseDeprecated(t *testing.T) {
   200  	t.Parallel()
   201  	const dockerComposeYAML = `
   202  services:
   203    foo:
   204      image: nginx:alpine
   205      # scale is deprecated in favor of deploy.replicas, but still valid
   206      scale: 2
   207      # cpus is deprecated in favor of deploy.resources.limits.cpu, but still valid
   208      cpus: 0.42
   209      # mem_limit is deprecated in favor of deploy.resources.limits.memory, but still valid
   210      mem_limit: 42m
   211  `
   212  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   213  	defer comp.CleanUp()
   214  
   215  	project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
   216  	assert.NilError(t, err)
   217  
   218  	fooSvc, err := project.GetService("foo")
   219  	assert.NilError(t, err)
   220  
   221  	foo, err := Parse(project, fooSvc)
   222  	assert.NilError(t, err)
   223  
   224  	t.Logf("foo: %+v", foo)
   225  	assert.Assert(t, len(foo.Containers) == 2)
   226  	for i, c := range foo.Containers {
   227  		assert.Assert(t, c.Name == DefaultContainerName(project.Name, "foo", strconv.Itoa(i+1)))
   228  		assert.Assert(t, in(c.RunArgs, "--name="+c.Name))
   229  		assert.Assert(t, in(c.RunArgs, fmt.Sprintf("--cpus=%f", 0.42)))
   230  		assert.Assert(t, in(c.RunArgs, "-m=44040192"))
   231  	}
   232  }
   233  
   234  func TestParseDeploy(t *testing.T) {
   235  	t.Parallel()
   236  	const dockerComposeYAML = `
   237  services:
   238    foo: # restart=no
   239      image: nginx:alpine
   240      deploy:
   241        replicas: 3
   242        resources:
   243          limits:
   244            cpus: "0.42"
   245            memory: "42m"
   246    bar: # restart=always
   247      image: nginx:alpine
   248      deploy:
   249        restart_policy: {}
   250        resources:
   251          reservations:
   252            devices:
   253            - capabilities: ["gpu", "utility", "compute"]
   254              driver: nvidia
   255              count: 2
   256            - capabilities: ["nvidia"]
   257              device_ids: ["dummy", "dummy2"]
   258    baz: # restart=no
   259      image: nginx:alpine
   260      deploy:
   261        restart_policy:
   262          condition: none
   263        resources:
   264          reservations:
   265            devices:
   266            - capabilities: ["utility"]
   267              count: all
   268    qux: # replicas=0
   269      image: nginx:alpine
   270      deploy:
   271        replicas: 0
   272  `
   273  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   274  	defer comp.CleanUp()
   275  
   276  	project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
   277  	assert.NilError(t, err)
   278  
   279  	fooSvc, err := project.GetService("foo")
   280  	assert.NilError(t, err)
   281  
   282  	foo, err := Parse(project, fooSvc)
   283  	assert.NilError(t, err)
   284  
   285  	t.Logf("foo: %+v", foo)
   286  	assert.Assert(t, len(foo.Containers) == 3)
   287  	for i, c := range foo.Containers {
   288  		assert.Assert(t, c.Name == DefaultContainerName(project.Name, "foo", strconv.Itoa(i+1)))
   289  		assert.Assert(t, in(c.RunArgs, "--name="+c.Name))
   290  
   291  		assert.Assert(t, in(c.RunArgs, "--restart=no"))
   292  		assert.Assert(t, in(c.RunArgs, "--cpus=0.42"))
   293  		assert.Assert(t, in(c.RunArgs, "-m=44040192"))
   294  	}
   295  
   296  	barSvc, err := project.GetService("bar")
   297  	assert.NilError(t, err)
   298  
   299  	bar, err := Parse(project, barSvc)
   300  	assert.NilError(t, err)
   301  
   302  	t.Logf("bar: %+v", bar)
   303  	assert.Assert(t, len(bar.Containers) == 1)
   304  	for _, c := range bar.Containers {
   305  		assert.Assert(t, in(c.RunArgs, "--restart=always"))
   306  		assert.Assert(t, in(c.RunArgs, `--gpus="capabilities=gpu,utility,compute",driver=nvidia,count=2`))
   307  		assert.Assert(t, in(c.RunArgs, `--gpus=capabilities=nvidia,"device=dummy,dummy2"`))
   308  	}
   309  
   310  	bazSvc, err := project.GetService("baz")
   311  	assert.NilError(t, err)
   312  
   313  	baz, err := Parse(project, bazSvc)
   314  	assert.NilError(t, err)
   315  
   316  	t.Logf("baz: %+v", baz)
   317  	assert.Assert(t, len(baz.Containers) == 1)
   318  	for _, c := range baz.Containers {
   319  		assert.Assert(t, in(c.RunArgs, "--restart=no"))
   320  		assert.Assert(t, in(c.RunArgs, `--gpus=capabilities=utility,count=-1`))
   321  	}
   322  
   323  	quxSvc, err := project.GetService("qux")
   324  	assert.NilError(t, err)
   325  
   326  	qux, err := Parse(project, quxSvc)
   327  	assert.NilError(t, err)
   328  
   329  	t.Logf("qux: %+v", qux)
   330  	assert.Assert(t, len(qux.Containers) == 0)
   331  
   332  }
   333  
   334  func TestParseRelative(t *testing.T) {
   335  	t.Parallel()
   336  	const dockerComposeYAML = `
   337  services:
   338    foo:
   339      image: nginx:alpine
   340      volumes:
   341      - "/file1:/file1"
   342      - "./file2:/file2"
   343      # break out the project dir, but this is fine
   344      - "../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../file3:/file3"
   345  `
   346  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   347  	defer comp.CleanUp()
   348  
   349  	project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
   350  	assert.NilError(t, err)
   351  
   352  	fooSvc, err := project.GetService("foo")
   353  	assert.NilError(t, err)
   354  
   355  	foo, err := Parse(project, fooSvc)
   356  	assert.NilError(t, err)
   357  
   358  	t.Logf("foo: %+v", foo)
   359  	for _, c := range foo.Containers {
   360  		assert.Assert(t, in(c.RunArgs, "-v=/file1:/file1"))
   361  		assert.Assert(t, in(c.RunArgs, fmt.Sprintf("-v=%s:/file2", filepath.Join(project.WorkingDir, "file2"))))
   362  		assert.Assert(t, in(c.RunArgs, "-v=/file3:/file3"))
   363  	}
   364  }
   365  
   366  func TestParseNetworkMode(t *testing.T) {
   367  	t.Parallel()
   368  	const dockerComposeYAML = `
   369  services:
   370    foo:
   371      image: nginx:alpine
   372      network_mode: host
   373      container_name: nginx
   374    bar:
   375      image: alpine:3.14
   376      network_mode: container:nginx
   377  `
   378  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   379  	defer comp.CleanUp()
   380  
   381  	project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
   382  	assert.NilError(t, err)
   383  
   384  	fooSvc, err := project.GetService("foo")
   385  	assert.NilError(t, err)
   386  
   387  	foo, err := Parse(project, fooSvc)
   388  	assert.NilError(t, err)
   389  
   390  	t.Logf("foo: %+v", foo)
   391  	for _, c := range foo.Containers {
   392  		assert.Assert(t, in(c.RunArgs, "--net=host"))
   393  	}
   394  
   395  	barSvc, err := project.GetService("bar")
   396  	assert.NilError(t, err)
   397  
   398  	bar, err := Parse(project, barSvc)
   399  	assert.NilError(t, err)
   400  
   401  	t.Logf("bar: %+v", bar)
   402  	for _, c := range bar.Containers {
   403  		assert.Assert(t, in(c.RunArgs, "--net=container:nginx"))
   404  		assert.Assert(t, !in(c.RunArgs, "--hostname=bar"))
   405  	}
   406  
   407  }
   408  
   409  func TestParseConfigs(t *testing.T) {
   410  	t.Parallel()
   411  	const dockerComposeYAML = `
   412  services:
   413    foo:
   414      image: nginx:alpine
   415      secrets:
   416      - secret1
   417      - source: secret2
   418        target: secret2-foo
   419      - source: secret3
   420        target: /mnt/secret3-foo
   421      configs:
   422      - config1
   423      - source: config2
   424        target: /mnt/config2-foo
   425  secrets:
   426    secret1:
   427      file: ./secret1
   428    secret2:
   429      file: ./secret2
   430    secret3:
   431      file: ./secret3
   432  configs:
   433    config1:
   434      file: ./config1
   435    config2:
   436      file: ./config2
   437  `
   438  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   439  	defer comp.CleanUp()
   440  
   441  	project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
   442  	assert.NilError(t, err)
   443  
   444  	for _, f := range []string{"secret1", "secret2", "secret3", "config1", "config2"} {
   445  		err = os.WriteFile(filepath.Join(project.WorkingDir, f), []byte("content-"+f), 0444)
   446  		assert.NilError(t, err)
   447  	}
   448  
   449  	fooSvc, err := project.GetService("foo")
   450  	assert.NilError(t, err)
   451  
   452  	foo, err := Parse(project, fooSvc)
   453  	assert.NilError(t, err)
   454  
   455  	t.Logf("foo: %+v", foo)
   456  	for _, c := range foo.Containers {
   457  		assert.Assert(t, in(c.RunArgs, fmt.Sprintf("-v=%s:/run/secrets/secret1:ro", filepath.Join(project.WorkingDir, "secret1"))))
   458  		assert.Assert(t, in(c.RunArgs, fmt.Sprintf("-v=%s:/run/secrets/secret2-foo:ro", filepath.Join(project.WorkingDir, "secret2"))))
   459  		assert.Assert(t, in(c.RunArgs, fmt.Sprintf("-v=%s:/mnt/secret3-foo:ro", filepath.Join(project.WorkingDir, "secret3"))))
   460  		assert.Assert(t, in(c.RunArgs, fmt.Sprintf("-v=%s:/config1:ro", filepath.Join(project.WorkingDir, "config1"))))
   461  		assert.Assert(t, in(c.RunArgs, fmt.Sprintf("-v=%s:/mnt/config2-foo:ro", filepath.Join(project.WorkingDir, "config2"))))
   462  	}
   463  }
   464  
   465  func TestParseRestartPolicy(t *testing.T) {
   466  	t.Parallel()
   467  	const dockerComposeYAML = `
   468  services:
   469    onfailure_no_count:
   470      image: alpine:3.14
   471      restart: on-failure
   472    onfailure_with_count:
   473      image: alpine:3.14
   474      restart: on-failure:10
   475    onfailure_ignore:
   476      image: alpine:3.14
   477      restart: on-failure:3.14
   478    unless_stopped:
   479      image: alpine:3.14
   480      restart: unless-stopped
   481  `
   482  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   483  	defer comp.CleanUp()
   484  
   485  	project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
   486  	assert.NilError(t, err)
   487  
   488  	getContainersFromService := func(svcName string) []Container {
   489  		svcConfig, err := project.GetService(svcName)
   490  		assert.NilError(t, err)
   491  		svc, err := Parse(project, svcConfig)
   492  		assert.NilError(t, err)
   493  
   494  		return svc.Containers
   495  	}
   496  
   497  	var c Container
   498  	c = getContainersFromService("onfailure_no_count")[0]
   499  	assert.Assert(t, in(c.RunArgs, "--restart=on-failure"))
   500  
   501  	c = getContainersFromService("onfailure_with_count")[0]
   502  	assert.Assert(t, in(c.RunArgs, "--restart=on-failure:10"))
   503  
   504  	c = getContainersFromService("onfailure_ignore")[0]
   505  	assert.Assert(t, !in(c.RunArgs, "--restart=on-failure:3.14"))
   506  
   507  	c = getContainersFromService("unless_stopped")[0]
   508  	assert.Assert(t, in(c.RunArgs, "--restart=unless-stopped"))
   509  }