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 }