github.com/containers/podman/v5@v5.1.0-rc1/test/e2e/quadlet_test.go (about) 1 package integration 2 3 import ( 4 "encoding/csv" 5 "fmt" 6 "os" 7 "path/filepath" 8 "reflect" 9 "regexp" 10 "strings" 11 12 "github.com/containers/podman/v5/pkg/systemd/parser" 13 . "github.com/containers/podman/v5/test/utils" 14 "github.com/containers/podman/v5/version" 15 "github.com/mattn/go-shellwords" 16 17 . "github.com/onsi/ginkgo/v2" 18 . "github.com/onsi/gomega" 19 . "github.com/onsi/gomega/gexec" 20 ) 21 22 type quadletTestcase struct { 23 data []byte 24 serviceName string 25 checks [][]string 26 } 27 28 // Converts "foo@bar.container" to "foo@.container" 29 func getGenericTemplateFile(fileName string) (bool, string) { 30 extension := filepath.Ext(fileName) 31 base := strings.TrimSuffix(fileName, extension) 32 parts := strings.SplitN(base, "@", 2) 33 if len(parts) == 2 && len(parts[1]) > 0 { 34 return true, parts[0] + "@" + extension 35 } 36 return false, "" 37 } 38 39 func loadQuadletTestcase(path string) *quadletTestcase { 40 data, err := os.ReadFile(path) 41 Expect(err).ToNot(HaveOccurred()) 42 43 base := filepath.Base(path) 44 ext := filepath.Ext(base) 45 service := base[:len(base)-len(ext)] 46 switch ext { 47 case ".volume": 48 service += "-volume" 49 case ".network": 50 service += "-network" 51 case ".image": 52 service += "-image" 53 case ".pod": 54 service += "-pod" 55 } 56 service += ".service" 57 58 checks := make([][]string, 0) 59 60 for _, line := range strings.Split(string(data), "\n") { 61 if strings.HasPrefix(line, "##") { 62 words, err := shellwords.Parse(line[2:]) 63 Expect(err).ToNot(HaveOccurred()) 64 checks = append(checks, words) 65 } 66 } 67 68 return &quadletTestcase{ 69 data, 70 service, 71 checks, 72 } 73 } 74 75 func matchSublistAt(full []string, pos int, sublist []string) bool { 76 if len(sublist) > len(full)-pos { 77 return false 78 } 79 80 for i := range sublist { 81 if sublist[i] != full[pos+i] { 82 return false 83 } 84 } 85 return true 86 } 87 88 func matchSublistRegexAt(full []string, pos int, sublist []string) bool { 89 if len(sublist) > len(full)-pos { 90 return false 91 } 92 93 for i := range sublist { 94 matched, err := regexp.MatchString(sublist[i], full[pos+i]) 95 if err != nil || !matched { 96 return false 97 } 98 } 99 return true 100 } 101 102 func findSublist(full []string, sublist []string) int { 103 if len(sublist) > len(full) { 104 return -1 105 } 106 if len(sublist) == 0 { 107 return -1 108 } 109 for i := 0; i < len(full)-len(sublist)+1; i++ { 110 if matchSublistAt(full, i, sublist) { 111 return i 112 } 113 } 114 return -1 115 } 116 117 func findSublistRegex(full []string, sublist []string) int { 118 if len(sublist) > len(full) { 119 return -1 120 } 121 if len(sublist) == 0 { 122 return -1 123 } 124 for i := 0; i < len(full)-len(sublist)+1; i++ { 125 if matchSublistRegexAt(full, i, sublist) { 126 return i 127 } 128 } 129 return -1 130 } 131 132 func (t *quadletTestcase) assertStdErrContains(args []string, session *PodmanSessionIntegration) bool { 133 return strings.Contains(session.ErrorToString(), args[0]) 134 } 135 136 func (t *quadletTestcase) assertKeyIs(args []string, unit *parser.UnitFile) bool { 137 Expect(len(args)).To(BeNumerically(">=", 3)) 138 group := args[0] 139 key := args[1] 140 values := args[2:] 141 142 realValues := unit.LookupAll(group, key) 143 if len(realValues) != len(values) { 144 return false 145 } 146 147 for i := range realValues { 148 if realValues[i] != values[i] { 149 return false 150 } 151 } 152 return true 153 } 154 155 func (t *quadletTestcase) assertKeyIsRegex(args []string, unit *parser.UnitFile) bool { 156 Expect(len(args)).To(BeNumerically(">=", 3)) 157 group := args[0] 158 key := args[1] 159 values := args[2:] 160 161 realValues := unit.LookupAll(group, key) 162 if len(realValues) != len(values) { 163 return false 164 } 165 166 for i := range realValues { 167 matched, _ := regexp.MatchString(values[i], realValues[i]) 168 if !matched { 169 return false 170 } 171 } 172 return true 173 } 174 175 func (t *quadletTestcase) assertKeyContains(args []string, unit *parser.UnitFile) bool { 176 Expect(args).To(HaveLen(3)) 177 group := args[0] 178 key := args[1] 179 value := args[2] 180 181 realValue, ok := unit.LookupLast(group, key) 182 return ok && strings.Contains(realValue, value) 183 } 184 185 func (t *quadletTestcase) assertPodmanArgs(args []string, unit *parser.UnitFile, key string, allowRegex, globalOnly bool) bool { 186 podmanArgs, _ := unit.LookupLastArgs("Service", key) 187 if globalOnly { 188 podmanCmdLocation := findSublist(podmanArgs, []string{args[0]}) 189 if podmanCmdLocation == -1 { 190 return false 191 } 192 193 podmanArgs = podmanArgs[:podmanCmdLocation] 194 args = args[1:] 195 } 196 197 var location int 198 if allowRegex { 199 location = findSublistRegex(podmanArgs, args) 200 } else { 201 location = findSublist(podmanArgs, args) 202 } 203 204 return location != -1 205 } 206 207 func keyValueStringToMap(keyValueString, separator string) (map[string]string, error) { 208 keyValMap := make(map[string]string) 209 csvReader := csv.NewReader(strings.NewReader(keyValueString)) 210 csvReader.Comma = []rune(separator)[0] 211 keyVarList, err := csvReader.ReadAll() 212 if err != nil { 213 return nil, err 214 } 215 for _, param := range keyVarList[0] { 216 key, val, _ := strings.Cut(param, "=") 217 keyValMap[key] = val 218 } 219 220 return keyValMap, nil 221 } 222 223 func keyValMapEqualRegex(expectedKeyValMap, actualKeyValMap map[string]string) bool { 224 if len(expectedKeyValMap) != len(actualKeyValMap) { 225 return false 226 } 227 for key, expectedValue := range expectedKeyValMap { 228 actualValue, ok := actualKeyValMap[key] 229 if !ok { 230 return false 231 } 232 matched, err := regexp.MatchString(expectedValue, actualValue) 233 if err != nil || !matched { 234 return false 235 } 236 } 237 return true 238 } 239 240 func (t *quadletTestcase) assertPodmanArgsKeyVal(args []string, unit *parser.UnitFile, key string, allowRegex, globalOnly bool) bool { 241 podmanArgs, _ := unit.LookupLastArgs("Service", key) 242 243 if globalOnly { 244 podmanCmdLocation := findSublist(podmanArgs, []string{args[0]}) 245 if podmanCmdLocation == -1 { 246 return false 247 } 248 249 podmanArgs = podmanArgs[:podmanCmdLocation] 250 args = args[1:] 251 } 252 253 expectedKeyValMap, err := keyValueStringToMap(args[2], args[1]) 254 if err != nil { 255 return false 256 } 257 argKeyLocation := 0 258 for { 259 subListLocation := findSublist(podmanArgs[argKeyLocation:], []string{args[0]}) 260 if subListLocation == -1 { 261 break 262 } 263 264 argKeyLocation += subListLocation 265 actualKeyValMap, err := keyValueStringToMap(podmanArgs[argKeyLocation+1], args[1]) 266 if err != nil { 267 break 268 } 269 if allowRegex { 270 if keyValMapEqualRegex(expectedKeyValMap, actualKeyValMap) { 271 return true 272 } 273 } else if reflect.DeepEqual(expectedKeyValMap, actualKeyValMap) { 274 return true 275 } 276 277 argKeyLocation += 2 278 279 if argKeyLocation > len(podmanArgs) { 280 break 281 } 282 } 283 284 return false 285 } 286 287 func (t *quadletTestcase) assertPodmanFinalArgs(args []string, unit *parser.UnitFile, key string) bool { 288 podmanArgs, _ := unit.LookupLastArgs("Service", key) 289 if len(podmanArgs) < len(args) { 290 return false 291 } 292 return matchSublistAt(podmanArgs, len(podmanArgs)-len(args), args) 293 } 294 295 func (t *quadletTestcase) assertPodmanFinalArgsRegex(args []string, unit *parser.UnitFile, key string) bool { 296 podmanArgs, _ := unit.LookupLastArgs("Service", key) 297 if len(podmanArgs) < len(args) { 298 return false 299 } 300 return matchSublistRegexAt(podmanArgs, len(podmanArgs)-len(args), args) 301 } 302 303 func (t *quadletTestcase) assertStartPodmanArgs(args []string, unit *parser.UnitFile) bool { 304 return t.assertPodmanArgs(args, unit, "ExecStart", false, false) 305 } 306 307 func (t *quadletTestcase) assertStartPodmanArgsRegex(args []string, unit *parser.UnitFile) bool { 308 return t.assertPodmanArgs(args, unit, "ExecStart", true, false) 309 } 310 311 func (t *quadletTestcase) assertStartPodmanGlobalArgs(args []string, unit *parser.UnitFile) bool { 312 return t.assertPodmanArgs(args, unit, "ExecStart", false, true) 313 } 314 315 func (t *quadletTestcase) assertStartPodmanGlobalArgsRegex(args []string, unit *parser.UnitFile) bool { 316 return t.assertPodmanArgs(args, unit, "ExecStart", true, true) 317 } 318 319 func (t *quadletTestcase) assertStartPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool { 320 return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", false, false) 321 } 322 323 func (t *quadletTestcase) assertStartPodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool { 324 return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", true, false) 325 } 326 327 func (t *quadletTestcase) assertStartPodmanGlobalArgsKeyVal(args []string, unit *parser.UnitFile) bool { 328 return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", false, true) 329 } 330 331 func (t *quadletTestcase) assertStartPodmanGlobalArgsKeyValRegex(args []string, unit *parser.UnitFile) bool { 332 return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", true, true) 333 } 334 335 func (t *quadletTestcase) assertStartPodmanFinalArgs(args []string, unit *parser.UnitFile) bool { 336 return t.assertPodmanFinalArgs(args, unit, "ExecStart") 337 } 338 339 func (t *quadletTestcase) assertStartPodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool { 340 return t.assertPodmanFinalArgsRegex(args, unit, "ExecStart") 341 } 342 343 func (t *quadletTestcase) assertStartPrePodmanArgs(args []string, unit *parser.UnitFile) bool { 344 return t.assertPodmanArgs(args, unit, "ExecStartPre", false, false) 345 } 346 347 func (t *quadletTestcase) assertStartPrePodmanArgsRegex(args []string, unit *parser.UnitFile) bool { 348 return t.assertPodmanArgs(args, unit, "ExecStartPre", true, false) 349 } 350 351 func (t *quadletTestcase) assertStartPrePodmanGlobalArgs(args []string, unit *parser.UnitFile) bool { 352 return t.assertPodmanArgs(args, unit, "ExecStartPre", false, true) 353 } 354 355 func (t *quadletTestcase) assertStartPrePodmanGlobalArgsRegex(args []string, unit *parser.UnitFile) bool { 356 return t.assertPodmanArgs(args, unit, "ExecStartPre", true, true) 357 } 358 359 func (t *quadletTestcase) assertStartPrePodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool { 360 return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", false, false) 361 } 362 363 func (t *quadletTestcase) assertStartPrePodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool { 364 return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", true, false) 365 } 366 367 func (t *quadletTestcase) assertStartPrePodmanGlobalArgsKeyVal(args []string, unit *parser.UnitFile) bool { 368 return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", false, true) 369 } 370 371 func (t *quadletTestcase) assertStartPrePodmanGlobalArgsKeyValRegex(args []string, unit *parser.UnitFile) bool { 372 return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", true, true) 373 } 374 375 func (t *quadletTestcase) assertStartPrePodmanFinalArgs(args []string, unit *parser.UnitFile) bool { 376 return t.assertPodmanFinalArgs(args, unit, "ExecStartPre") 377 } 378 379 func (t *quadletTestcase) assertStartPrePodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool { 380 return t.assertPodmanFinalArgsRegex(args, unit, "ExecStartPre") 381 } 382 383 func (t *quadletTestcase) assertStopPodmanArgs(args []string, unit *parser.UnitFile) bool { 384 return t.assertPodmanArgs(args, unit, "ExecStop", false, false) 385 } 386 387 func (t *quadletTestcase) assertStopPodmanGlobalArgs(args []string, unit *parser.UnitFile) bool { 388 return t.assertPodmanArgs(args, unit, "ExecStop", false, true) 389 } 390 391 func (t *quadletTestcase) assertStopPodmanFinalArgs(args []string, unit *parser.UnitFile) bool { 392 return t.assertPodmanFinalArgs(args, unit, "ExecStop") 393 } 394 395 func (t *quadletTestcase) assertStopPodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool { 396 return t.assertPodmanFinalArgsRegex(args, unit, "ExecStop") 397 } 398 399 func (t *quadletTestcase) assertStopPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool { 400 return t.assertPodmanArgsKeyVal(args, unit, "ExecStop", false, false) 401 } 402 403 func (t *quadletTestcase) assertStopPodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool { 404 return t.assertPodmanArgsKeyVal(args, unit, "ExecStop", true, false) 405 } 406 407 func (t *quadletTestcase) assertStopPostPodmanArgs(args []string, unit *parser.UnitFile) bool { 408 return t.assertPodmanArgs(args, unit, "ExecStopPost", false, false) 409 } 410 411 func (t *quadletTestcase) assertStopPostPodmanGlobalArgs(args []string, unit *parser.UnitFile) bool { 412 return t.assertPodmanArgs(args, unit, "ExecStopPost", false, true) 413 } 414 415 func (t *quadletTestcase) assertStopPostPodmanFinalArgs(args []string, unit *parser.UnitFile) bool { 416 return t.assertPodmanFinalArgs(args, unit, "ExecStopPost") 417 } 418 419 func (t *quadletTestcase) assertStopPostPodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool { 420 return t.assertPodmanFinalArgsRegex(args, unit, "ExecStopPost") 421 } 422 423 func (t *quadletTestcase) assertStopPostPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool { 424 return t.assertPodmanArgsKeyVal(args, unit, "ExecStopPost", false, false) 425 } 426 427 func (t *quadletTestcase) assertStopPostPodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool { 428 return t.assertPodmanArgsKeyVal(args, unit, "ExecStopPost", true, false) 429 } 430 431 func (t *quadletTestcase) assertSymlink(args []string, unit *parser.UnitFile) bool { 432 Expect(args).To(HaveLen(2)) 433 symlink := args[0] 434 expectedTarget := args[1] 435 436 dir := filepath.Dir(unit.Path) 437 438 target, err := os.Readlink(filepath.Join(dir, symlink)) 439 Expect(err).ToNot(HaveOccurred()) 440 441 return expectedTarget == target 442 } 443 444 func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, session *PodmanSessionIntegration) error { 445 Expect(check).ToNot(BeEmpty()) 446 op := check[0] 447 args := make([]string, 0) 448 for _, a := range check[1:] { 449 // Apply \n and \t as they are used in the testcases 450 a = strings.ReplaceAll(a, "\\n", "\n") 451 a = strings.ReplaceAll(a, "\\t", "\t") 452 args = append(args, a) 453 } 454 invert := false 455 if op[0] == '!' { 456 invert = true 457 op = op[1:] 458 } 459 460 var ok bool 461 switch op { 462 case "assert-failed": 463 ok = true /* Handled separately */ 464 case "assert-stderr-contains": 465 ok = t.assertStdErrContains(args, session) 466 case "assert-key-is": 467 ok = t.assertKeyIs(args, unit) 468 case "assert-key-is-regex": 469 ok = t.assertKeyIsRegex(args, unit) 470 case "assert-key-contains": 471 ok = t.assertKeyContains(args, unit) 472 case "assert-podman-args": 473 ok = t.assertStartPodmanArgs(args, unit) 474 case "assert-podman-args-regex": 475 ok = t.assertStartPodmanArgsRegex(args, unit) 476 case "assert-podman-args-key-val": 477 ok = t.assertStartPodmanArgsKeyVal(args, unit) 478 case "assert-podman-args-key-val-regex": 479 ok = t.assertStartPodmanArgsKeyValRegex(args, unit) 480 case "assert-podman-global-args": 481 ok = t.assertStartPodmanGlobalArgs(args, unit) 482 case "assert-podman-global-args-regex": 483 ok = t.assertStartPodmanGlobalArgsRegex(args, unit) 484 case "assert-podman-global-args-key-val": 485 ok = t.assertStartPodmanGlobalArgsKeyVal(args, unit) 486 case "assert-podman-global-args-key-val-regex": 487 ok = t.assertStartPodmanGlobalArgsKeyValRegex(args, unit) 488 case "assert-podman-final-args": 489 ok = t.assertStartPodmanFinalArgs(args, unit) 490 case "assert-podman-final-args-regex": 491 ok = t.assertStartPodmanFinalArgsRegex(args, unit) 492 case "assert-podman-pre-args": 493 ok = t.assertStartPrePodmanArgs(args, unit) 494 case "assert-podman-pre-args-regex": 495 ok = t.assertStartPrePodmanArgsRegex(args, unit) 496 case "assert-podman-pre-args-key-val": 497 ok = t.assertStartPrePodmanArgsKeyVal(args, unit) 498 case "assert-podman-pre-args-key-val-regex": 499 ok = t.assertStartPrePodmanArgsKeyValRegex(args, unit) 500 case "assert-podman-pre-global-args": 501 ok = t.assertStartPrePodmanGlobalArgs(args, unit) 502 case "assert-podman-pre-global-args-regex": 503 ok = t.assertStartPrePodmanGlobalArgsRegex(args, unit) 504 case "assert-podman-pre-global-args-key-val": 505 ok = t.assertStartPrePodmanGlobalArgsKeyVal(args, unit) 506 case "assert-podman-pre-global-args-key-val-regex": 507 ok = t.assertStartPrePodmanGlobalArgsKeyValRegex(args, unit) 508 case "assert-podman-pre-final-args": 509 ok = t.assertStartPrePodmanFinalArgs(args, unit) 510 case "assert-podman-pre-final-args-regex": 511 ok = t.assertStartPrePodmanFinalArgsRegex(args, unit) 512 case "assert-symlink": 513 ok = t.assertSymlink(args, unit) 514 case "assert-podman-stop-args": 515 ok = t.assertStopPodmanArgs(args, unit) 516 case "assert-podman-stop-global-args": 517 ok = t.assertStopPodmanGlobalArgs(args, unit) 518 case "assert-podman-stop-final-args": 519 ok = t.assertStopPodmanFinalArgs(args, unit) 520 case "assert-podman-stop-final-args-regex": 521 ok = t.assertStopPodmanFinalArgsRegex(args, unit) 522 case "assert-podman-stop-args-key-val": 523 ok = t.assertStopPodmanArgsKeyVal(args, unit) 524 case "assert-podman-stop-args-key-val-regex": 525 ok = t.assertStopPodmanArgsKeyValRegex(args, unit) 526 case "assert-podman-stop-post-args": 527 ok = t.assertStopPostPodmanArgs(args, unit) 528 case "assert-podman-stop-post-global-args": 529 ok = t.assertStopPostPodmanGlobalArgs(args, unit) 530 case "assert-podman-stop-post-final-args": 531 ok = t.assertStopPostPodmanFinalArgs(args, unit) 532 case "assert-podman-stop-post-final-args-regex": 533 ok = t.assertStopPostPodmanFinalArgsRegex(args, unit) 534 case "assert-podman-stop-post-args-key-val": 535 ok = t.assertStopPostPodmanArgsKeyVal(args, unit) 536 case "assert-podman-stop-post-args-key-val-regex": 537 ok = t.assertStopPostPodmanArgsKeyValRegex(args, unit) 538 539 default: 540 return fmt.Errorf("Unsupported assertion %s", op) 541 } 542 if invert { 543 ok = !ok 544 } 545 546 if !ok { 547 s := "(nil)" 548 if unit != nil { 549 s, _ = unit.ToString() 550 } 551 return fmt.Errorf("Failed assertion for %s: %s\n\n%s", t.serviceName, strings.Join(check, " "), s) 552 } 553 return nil 554 } 555 556 func (t *quadletTestcase) check(generateDir string, session *PodmanSessionIntegration) { 557 expectFail := false 558 for _, c := range t.checks { 559 if c[0] == "assert-failed" { 560 expectFail = true 561 } 562 } 563 564 file := filepath.Join(generateDir, t.serviceName) 565 _, err := os.Stat(file) 566 if expectFail { 567 Expect(err).To(MatchError(os.ErrNotExist)) 568 } else { 569 Expect(err).ToNot(HaveOccurred()) 570 } 571 572 var unit *parser.UnitFile 573 if !expectFail { 574 unit, err = parser.ParseUnitFile(file) 575 Expect(err).ToNot(HaveOccurred()) 576 } 577 578 for _, check := range t.checks { 579 err := t.doAssert(check, unit, session) 580 Expect(err).ToNot(HaveOccurred()) 581 } 582 } 583 584 var _ = Describe("quadlet system generator", func() { 585 var ( 586 err error 587 generatedDir string 588 quadletDir string 589 ) 590 591 BeforeEach(func() { 592 generatedDir = filepath.Join(podmanTest.TempDir, "generated") 593 err = os.Mkdir(generatedDir, os.ModePerm) 594 Expect(err).ToNot(HaveOccurred()) 595 596 quadletDir = filepath.Join(podmanTest.TempDir, "quadlet") 597 err = os.Mkdir(quadletDir, os.ModePerm) 598 Expect(err).ToNot(HaveOccurred()) 599 }) 600 601 Describe("quadlet -version", func() { 602 It("Should print correct version", func() { 603 session := podmanTest.Quadlet([]string{"-version"}, "/something") 604 session.WaitWithDefaultTimeout() 605 Expect(session).Should(ExitCleanly()) 606 Expect(session.OutputToString()).To(Equal(version.Version.String())) 607 }) 608 }) 609 610 Describe("Running quadlet dryrun tests", func() { 611 It("Should exit with an error because of no files are found to parse", func() { 612 fileName := "basic.kube" 613 testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName)) 614 615 // Write the tested file to the quadlet dir 616 err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644) 617 Expect(err).ToNot(HaveOccurred()) 618 619 session := podmanTest.Quadlet([]string{"-dryrun"}, "/something") 620 session.WaitWithDefaultTimeout() 621 Expect(session).Should(Exit(0)) 622 623 current := session.ErrorToStringArray() 624 expected := "No files parsed from [/something]" 625 626 found := false 627 for _, line := range current { 628 if strings.Contains(line, expected) { 629 found = true 630 break 631 } 632 } 633 Expect(found).To(BeTrue()) 634 }) 635 636 It("Should fail on bad quadlet", func() { 637 quadletfile := fmt.Sprintf(`[Container] 638 Image=%s 639 BOGUS=foo 640 `, ALPINE) 641 642 quadletfilePath := filepath.Join(podmanTest.TempDir, "bogus.container") 643 err = os.WriteFile(quadletfilePath, []byte(quadletfile), 0644) 644 Expect(err).ToNot(HaveOccurred()) 645 defer os.Remove(quadletfilePath) 646 session := podmanTest.Quadlet([]string{"-dryrun"}, podmanTest.TempDir) 647 session.WaitWithDefaultTimeout() 648 Expect(session).Should(Exit(1)) 649 Expect(session.ErrorToString()).To(ContainSubstring("converting \"bogus.container\": unsupported key 'BOGUS' in group 'Container' in " + quadletfilePath)) 650 }) 651 652 It("Should scan and return output for files in subdirectories", func() { 653 dirName := "test_subdir" 654 655 err = CopyDirectory(filepath.Join("quadlet", dirName), quadletDir) 656 657 if err != nil { 658 GinkgoWriter.Println("error:", err) 659 } 660 661 session := podmanTest.Quadlet([]string{"-dryrun", "-user"}, quadletDir) 662 session.WaitWithDefaultTimeout() 663 664 current := session.OutputToStringArray() 665 expected := []string{ 666 "---mysleep.service---", 667 "---mysleep_1.service---", 668 "---mysleep_2.service---", 669 } 670 671 Expect(current).To(ContainElements(expected)) 672 }) 673 674 It("Should parse a kube file and print it to stdout", func() { 675 fileName := "basic.kube" 676 testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName)) 677 678 // quadlet uses PODMAN env to get a stable podman path 679 podmanPath, found := os.LookupEnv("PODMAN") 680 if !found { 681 podmanPath = podmanTest.PodmanBinary 682 } 683 684 // Write the tested file to the quadlet dir 685 err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644) 686 Expect(err).ToNot(HaveOccurred()) 687 688 session := podmanTest.Quadlet([]string{"-dryrun"}, quadletDir) 689 session.WaitWithDefaultTimeout() 690 Expect(session).Should(Exit(0)) 691 Expect(session.ErrorToString()).To(ContainSubstring("Loading source unit file ")) 692 693 current := session.OutputToStringArray() 694 expected := []string{ 695 "---basic.service---", 696 "## assert-podman-args \"kube\"", 697 "## assert-podman-args \"play\"", 698 "## assert-podman-final-args-regex .*/podman-e2e-.*/subtest-.*/quadlet/deployment.yml", 699 "## assert-podman-args \"--replace\"", 700 "## assert-podman-args \"--service-container=true\"", 701 "## assert-podman-stop-post-args \"kube\"", 702 "## assert-podman-stop-post-args \"down\"", 703 "## assert-podman-stop-post-final-args-regex .*/podman-e2e-.*/subtest-.*/quadlet/deployment.yml", 704 "## assert-key-is \"Unit\" \"RequiresMountsFor\" \"%t/containers\"", 705 "## assert-key-is \"Service\" \"KillMode\" \"mixed\"", 706 "## assert-key-is \"Service\" \"Type\" \"notify\"", 707 "## assert-key-is \"Service\" \"NotifyAccess\" \"all\"", 708 "## assert-key-is \"Service\" \"Environment\" \"PODMAN_SYSTEMD_UNIT=%n\"", 709 "## assert-key-is \"Service\" \"SyslogIdentifier\" \"%N\"", 710 "[X-Kube]", 711 "Yaml=deployment.yml", 712 "[Unit]", 713 fmt.Sprintf("SourcePath=%s/basic.kube", quadletDir), 714 "RequiresMountsFor=%t/containers", 715 "[Service]", 716 "KillMode=mixed", 717 "Environment=PODMAN_SYSTEMD_UNIT=%n", 718 "Type=notify", 719 "NotifyAccess=all", 720 "SyslogIdentifier=%N", 721 fmt.Sprintf("ExecStart=%s kube play --replace --service-container=true %s/deployment.yml", podmanPath, quadletDir), 722 fmt.Sprintf("ExecStopPost=%s kube down %s/deployment.yml", podmanPath, quadletDir), 723 } 724 725 Expect(current).To(Equal(expected)) 726 }) 727 }) 728 729 DescribeTable("Running quadlet test case", 730 func(fileName string, exitCode int, errString string) { 731 testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName)) 732 733 // Write the tested file to the quadlet dir 734 err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644) 735 Expect(err).ToNot(HaveOccurred()) 736 737 // Also copy any extra snippets 738 snippetdirs := []string{fileName + ".d"} 739 if ok, genericFileName := getGenericTemplateFile(fileName); ok { 740 snippetdirs = append(snippetdirs, genericFileName+".d") 741 } 742 for _, snippetdir := range snippetdirs { 743 dotdDir := filepath.Join("quadlet", snippetdir) 744 if s, err := os.Stat(dotdDir); err == nil && s.IsDir() { 745 dotdDirDest := filepath.Join(quadletDir, snippetdir) 746 err = os.Mkdir(dotdDirDest, os.ModePerm) 747 Expect(err).ToNot(HaveOccurred()) 748 err = CopyDirectory(dotdDir, dotdDirDest) 749 Expect(err).ToNot(HaveOccurred()) 750 } 751 } 752 753 // Run quadlet to convert the file 754 session := podmanTest.Quadlet([]string{"--user", "--no-kmsg-log", generatedDir}, quadletDir) 755 session.WaitWithDefaultTimeout() 756 Expect(session).Should(Exit(exitCode)) 757 758 // Print any stderr output 759 errs := session.ErrorToString() 760 if errs != "" { 761 GinkgoWriter.Println("error:", session.ErrorToString()) 762 } 763 Expect(errs).Should(ContainSubstring(errString)) 764 765 testcase.check(generatedDir, session) 766 }, 767 Entry("Basic container", "basic.container", 0, ""), 768 Entry("annotation.container", "annotation.container", 0, ""), 769 Entry("autoupdate.container", "autoupdate.container", 0, ""), 770 Entry("basepodman.container", "basepodman.container", 0, ""), 771 Entry("capabilities.container", "capabilities.container", 0, ""), 772 Entry("capabilities2.container", "capabilities2.container", 0, ""), 773 Entry("comment-with-continuation.container", "comment-with-continuation.container", 0, ""), 774 Entry("devices.container", "devices.container", 0, ""), 775 Entry("disableselinux.container", "disableselinux.container", 0, ""), 776 Entry("dns-options.container", "dns-options.container", 0, ""), 777 Entry("dns-search.container", "dns-search.container", 0, ""), 778 Entry("dns.container", "dns.container", 0, ""), 779 Entry("env-file.container", "env-file.container", 0, ""), 780 Entry("env-host-false.container", "env-host-false.container", 0, ""), 781 Entry("env-host.container", "env-host.container", 0, ""), 782 Entry("env.container", "env.container", 0, ""), 783 Entry("entrypoint.container", "entrypoint.container", 0, ""), 784 Entry("escapes.container", "escapes.container", 0, ""), 785 Entry("exec.container", "exec.container", 0, ""), 786 Entry("group-add.container", "group-add.container", 0, ""), 787 Entry("health.container", "health.container", 0, ""), 788 Entry("hostname.container", "hostname.container", 0, ""), 789 Entry("idmapping.container", "idmapping.container", 0, ""), 790 Entry("idmapping-with-remap.container", "idmapping-with-remap.container", 1, "converting \"idmapping-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"), 791 Entry("image.container", "image.container", 0, ""), 792 Entry("install.container", "install.container", 0, ""), 793 Entry("ip.container", "ip.container", 0, ""), 794 Entry("label.container", "label.container", 0, ""), 795 Entry("line-continuation-whitespace.container", "line-continuation-whitespace.container", 0, ""), 796 Entry("logdriver.container", "logdriver.container", 0, ""), 797 Entry("mask.container", "mask.container", 0, ""), 798 Entry("mount.container", "mount.container", 0, ""), 799 Entry("name.container", "name.container", 0, ""), 800 Entry("nestedselinux.container", "nestedselinux.container", 0, ""), 801 Entry("network.container", "network.container", 0, ""), 802 Entry("network.quadlet.container", "network.quadlet.container", 0, ""), 803 Entry("noimage.container", "noimage.container", 1, "converting \"noimage.container\": no Image or Rootfs key specified"), 804 Entry("notify.container", "notify.container", 0, ""), 805 Entry("notify-healthy.container", "notify-healthy.container", 0, ""), 806 Entry("oneshot.container", "oneshot.container", 0, ""), 807 Entry("other-sections.container", "other-sections.container", 0, ""), 808 Entry("pod.non-quadlet.container", "pod.non-quadlet.container", 1, "converting \"pod.non-quadlet.container\": pod test-pod is not Quadlet based"), 809 Entry("pod.not-found.container", "pod.not-found.container", 1, "converting \"pod.not-found.container\": quadlet pod unit not-found.pod does not exist"), 810 Entry("podmanargs.container", "podmanargs.container", 0, ""), 811 Entry("ports.container", "ports.container", 0, ""), 812 Entry("ports_ipv6.container", "ports_ipv6.container", 0, ""), 813 Entry("pull.container", "pull.container", 0, ""), 814 Entry("quotes.container", "quotes.container", 0, ""), 815 Entry("readonly.container", "readonly.container", 0, ""), 816 Entry("readonly-tmpfs.container", "readonly-tmpfs.container", 0, ""), 817 Entry("readonly-notmpfs.container", "readonly-notmpfs.container", 0, ""), 818 Entry("readwrite-notmpfs.container", "readwrite-notmpfs.container", 0, ""), 819 Entry("volatiletmp-readwrite.container", "volatiletmp-readwrite.container", 0, ""), 820 Entry("volatiletmp-readonly.container", "volatiletmp-readonly.container", 0, ""), 821 Entry("remap-auto.container", "remap-auto.container", 0, ""), 822 Entry("remap-auto2.container", "remap-auto2.container", 0, ""), 823 Entry("remap-keep-id.container", "remap-keep-id.container", 0, ""), 824 Entry("remap-keep-id2.container", "remap-keep-id2.container", 0, ""), 825 Entry("remap-manual.container", "remap-manual.container", 0, ""), 826 Entry("rootfs.container", "rootfs.container", 0, ""), 827 Entry("seccomp.container", "seccomp.container", 0, ""), 828 Entry("secrets.container", "secrets.container", 0, ""), 829 Entry("selinux.container", "selinux.container", 0, ""), 830 Entry("shmsize.container", "shmsize.container", 0, ""), 831 Entry("shortname.container", "shortname.container", 0, "Warning: shortname.container specifies the image \"shortname\" which not a fully qualified image name. This is not ideal for performance and security reasons. See the podman-pull manpage discussion of short-name-aliases.conf for details."), 832 Entry("stoptimeout.container", "stoptimeout.container", 0, ""), 833 Entry("subidmapping.container", "subidmapping.container", 0, ""), 834 Entry("subidmapping-with-remap.container", "subidmapping-with-remap.container", 1, "converting \"subidmapping-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"), 835 Entry("sysctl.container", "sysctl.container", 0, ""), 836 Entry("timezone.container", "timezone.container", 0, ""), 837 Entry("ulimit.container", "ulimit.container", 0, ""), 838 Entry("unmask.container", "unmask.container", 0, ""), 839 Entry("user.container", "user.container", 0, ""), 840 Entry("userns.container", "userns.container", 0, ""), 841 Entry("userns-with-remap.container", "userns-with-remap.container", 1, "converting \"userns-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"), 842 Entry("volume.container", "volume.container", 0, ""), 843 Entry("workingdir.container", "workingdir.container", 0, ""), 844 Entry("Container - global args", "globalargs.container", 0, ""), 845 Entry("Container - Containers Conf Modules", "containersconfmodule.container", 0, ""), 846 Entry("merged.container", "merged.container", 0, ""), 847 Entry("merged-override.container", "merged-override.container", 0, ""), 848 Entry("template@.container", "template@.container", 0, ""), 849 Entry("template@instance.container", "template@instance.container", 0, ""), 850 851 Entry("basic.volume", "basic.volume", 0, ""), 852 Entry("device-copy.volume", "device-copy.volume", 0, ""), 853 Entry("device.volume", "device.volume", 0, ""), 854 Entry("label.volume", "label.volume", 0, ""), 855 Entry("name.volume", "name.volume", 0, ""), 856 Entry("podmanargs.volume", "podmanargs.volume", 0, ""), 857 Entry("uid.volume", "uid.volume", 0, ""), 858 Entry("image.volume", "image.volume", 0, ""), 859 Entry("image-no-image.volume", "image-no-image.volume", 1, "converting \"image-no-image.volume\": the key Image is mandatory when using the image driver"), 860 Entry("Volume - global args", "globalargs.volume", 0, ""), 861 Entry("Volume - Containers Conf Modules", "containersconfmodule.volume", 0, ""), 862 863 Entry("Absolute Path", "absolute.path.kube", 0, ""), 864 Entry("Basic kube", "basic.kube", 0, ""), 865 Entry("Kube - ConfigMap", "configmap.kube", 0, ""), 866 Entry("Kube - Exit Code Propagation", "exit_code_propagation.kube", 0, ""), 867 Entry("Kube - Logdriver", "logdriver.kube", 0, ""), 868 Entry("Kube - Network", "network.kube", 0, ""), 869 Entry("Kube - PodmanArgs", "podmanargs.kube", 0, ""), 870 Entry("Kube - Publish IPv4 ports", "ports.kube", 0, ""), 871 Entry("Kube - Publish IPv6 ports", "ports_ipv6.kube", 0, ""), 872 Entry("Kube - Quadlet Network", "network.quadlet.kube", 0, ""), 873 Entry("Kube - User Remap Auto with IDs", "remap-auto2.kube", 0, ""), 874 Entry("Kube - User Remap Auto", "remap-auto.kube", 0, ""), 875 Entry("Kube - User Remap Manual", "remap-manual.kube", 1, "converting \"remap-manual.kube\": RemapUsers=manual is not supported"), 876 Entry("Syslog Identifier", "syslog.identifier.kube", 0, ""), 877 Entry("Kube - Working Directory YAML Absolute Path", "workingdir-yaml-abs.kube", 0, ""), 878 Entry("Kube - Working Directory YAML Relative Path", "workingdir-yaml-rel.kube", 0, ""), 879 Entry("Kube - Working Directory Unit", "workingdir-unit.kube", 0, ""), 880 Entry("Kube - Working Directory already in Service", "workingdir-service.kube", 0, ""), 881 Entry("Kube - global args", "globalargs.kube", 0, ""), 882 Entry("Kube - Containers Conf Modules", "containersconfmodule.kube", 0, ""), 883 Entry("Kube - Service Type=oneshot", "oneshot.kube", 0, ""), 884 Entry("Kube - Down force", "downforce.kube", 0, ""), 885 886 Entry("Network - Basic", "basic.network", 0, ""), 887 Entry("Network - Disable DNS", "disable-dns.network", 0, ""), 888 Entry("Network - DNS", "dns.network", 0, ""), 889 Entry("Network - Driver", "driver.network", 0, ""), 890 Entry("Network - Gateway not enough Subnet", "gateway.less-subnet.network", 1, "converting \"gateway.less-subnet.network\": cannot set more gateways than subnets"), 891 Entry("Network - Gateway without Subnet", "gateway.no-subnet.network", 1, "converting \"gateway.no-subnet.network\": cannot set gateway or range without subnet"), 892 Entry("Network - Gateway", "gateway.network", 0, ""), 893 Entry("Network - IPAM Driver", "ipam-driver.network", 0, ""), 894 Entry("Network - IPv6", "ipv6.network", 0, ""), 895 Entry("Network - Internal network", "internal.network", 0, ""), 896 Entry("Network - Label", "label.network", 0, ""), 897 Entry("Network - Multiple Options", "options.multiple.network", 0, ""), 898 Entry("Network - Name", "name.network", 0, ""), 899 Entry("Network - Options", "options.network", 0, ""), 900 Entry("Network - PodmanArgs", "podmanargs.network", 0, ""), 901 Entry("Network - Range not enough Subnet", "range.less-subnet.network", 1, "converting \"range.less-subnet.network\": cannot set more ranges than subnets"), 902 Entry("Network - Range without Subnet", "range.no-subnet.network", 1, "converting \"range.no-subnet.network\": cannot set gateway or range without subnet"), 903 Entry("Network - Range", "range.network", 0, ""), 904 Entry("Network - Subnets", "subnets.network", 0, ""), 905 Entry("Network - multiple subnet, gateway and range", "subnet-trio.multiple.network", 0, ""), 906 Entry("Network - subnet, gateway and range", "subnet-trio.network", 0, ""), 907 Entry("Network - global args", "globalargs.network", 0, ""), 908 Entry("Network - Containers Conf Modules", "containersconfmodule.network", 0, ""), 909 910 Entry("Image - Basic", "basic.image", 0, ""), 911 Entry("Image - No Image", "no-image.image", 1, "converting \"no-image.image\": no Image key specified"), 912 Entry("Image - Architecture", "arch.image", 0, ""), 913 Entry("Image - Auth File", "auth.image", 0, ""), 914 Entry("Image - Certificates", "certs.image", 0, ""), 915 Entry("Image - Credentials", "creds.image", 0, ""), 916 Entry("Image - Decryption Key", "decrypt.image", 0, ""), 917 Entry("Image - OS Key", "os.image", 0, ""), 918 Entry("Image - Variant Key", "variant.image", 0, ""), 919 Entry("Image - All Tags", "all-tags.image", 0, ""), 920 Entry("Image - TLS Verify", "tls-verify.image", 0, ""), 921 Entry("Image - Arch and OS", "arch-os.image", 0, ""), 922 Entry("Image - global args", "globalargs.image", 0, ""), 923 Entry("Image - Containers Conf Modules", "containersconfmodule.image", 0, ""), 924 925 Entry("basic.pod", "basic.pod", 0, ""), 926 Entry("name.pod", "name.pod", 0, ""), 927 Entry("network.pod", "network.pod", 0, ""), 928 Entry("network-quadlet.pod", "network.quadlet.pod", 0, ""), 929 Entry("podmanargs.pod", "podmanargs.pod", 0, ""), 930 Entry("volume.pod", "volume.pod", 0, ""), 931 ) 932 933 })