Files
podman/test/e2e/quadlet_test.go
Paul Holzinger 67e0fa8b89 quadlet: add default network dependencies to all units
There is no good reason for the special case, kube and pod units
definitely need it. Volume and network units maybe not but for
consistency we add it there as well. This makes the docs much easier to
write and understand for users as the behavior will not differ.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2024-10-18 14:01:22 +02:00

1128 lines
43 KiB
Go

//go:build linux || freebsd
package integration
import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"github.com/containers/podman/v5/pkg/systemd/parser"
. "github.com/containers/podman/v5/test/utils"
"github.com/containers/podman/v5/version"
"github.com/mattn/go-shellwords"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
)
type quadletTestcase struct {
data []byte
serviceName string
checks [][]string
}
// Converts "foo@bar.container" to "foo@.container"
func getGenericTemplateFile(fileName string) (bool, string) {
extension := filepath.Ext(fileName)
base := strings.TrimSuffix(fileName, extension)
parts := strings.SplitN(base, "@", 2)
if len(parts) == 2 && len(parts[1]) > 0 {
return true, parts[0] + "@" + extension
}
return false, ""
}
func calcServiceName(path string) string {
base := filepath.Base(path)
ext := filepath.Ext(base)
service := base[:len(base)-len(ext)]
switch ext {
case ".volume":
service += "-volume"
case ".network":
service += "-network"
case ".image":
service += "-image"
case ".build":
service += "-build"
case ".pod":
service += "-pod"
}
return service
}
func loadQuadletTestcase(path string) *quadletTestcase {
return loadQuadletTestcaseWithServiceName(path, "")
}
func loadQuadletTestcaseWithServiceName(path, serviceName string) *quadletTestcase {
data, err := os.ReadFile(path)
Expect(err).ToNot(HaveOccurred())
var service string
if len(serviceName) > 0 {
service = serviceName
} else {
service = calcServiceName(path)
}
service += ".service"
checks := make([][]string, 0)
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "##") {
words, err := shellwords.Parse(line[2:])
Expect(err).ToNot(HaveOccurred())
checks = append(checks, words)
}
}
return &quadletTestcase{
data,
service,
checks,
}
}
func matchSublistAt(full []string, pos int, sublist []string) bool {
if len(sublist) > len(full)-pos {
return false
}
for i := range sublist {
if sublist[i] != full[pos+i] {
return false
}
}
return true
}
func matchSublistRegexAt(full []string, pos int, sublist []string) bool {
if len(sublist) > len(full)-pos {
return false
}
for i := range sublist {
matched, err := regexp.MatchString(sublist[i], full[pos+i])
if err != nil || !matched {
return false
}
}
return true
}
func findSublist(full []string, sublist []string) int {
if len(sublist) > len(full) {
return -1
}
if len(sublist) == 0 {
return -1
}
for i := 0; i < len(full)-len(sublist)+1; i++ {
if matchSublistAt(full, i, sublist) {
return i
}
}
return -1
}
func findSublistRegex(full []string, sublist []string) int {
if len(sublist) > len(full) {
return -1
}
if len(sublist) == 0 {
return -1
}
for i := 0; i < len(full)-len(sublist)+1; i++ {
if matchSublistRegexAt(full, i, sublist) {
return i
}
}
return -1
}
func (t *quadletTestcase) assertStdErrContains(args []string, session *PodmanSessionIntegration) bool {
return strings.Contains(session.ErrorToString(), args[0])
}
func (t *quadletTestcase) assertKeyIs(args []string, unit *parser.UnitFile) bool {
Expect(len(args)).To(BeNumerically(">=", 3))
group := args[0]
key := args[1]
values := args[2:]
realValues := unit.LookupAll(group, key)
if len(realValues) != len(values) {
return false
}
for i := range realValues {
if realValues[i] != values[i] {
return false
}
}
return true
}
func (t *quadletTestcase) assertKeyIsEmpty(args []string, unit *parser.UnitFile) bool {
Expect(args).To(HaveLen(2))
group := args[0]
key := args[1]
realValues := unit.LookupAll(group, key)
return len(realValues) == 0
}
func (t *quadletTestcase) assertKeyIsRegex(args []string, unit *parser.UnitFile) bool {
Expect(len(args)).To(BeNumerically(">=", 3))
group := args[0]
key := args[1]
values := args[2:]
realValues := unit.LookupAll(group, key)
if len(realValues) != len(values) {
return false
}
for i := range realValues {
matched, _ := regexp.MatchString(values[i], realValues[i])
if !matched {
return false
}
}
return true
}
func (t *quadletTestcase) assertLastKeyIsRegex(args []string, unit *parser.UnitFile) bool {
Expect(len(args)).To(BeNumerically(">=", 3))
group := args[0]
key := args[1]
regex := args[2]
value, ok := unit.LookupLast(group, key)
if !ok {
return false
}
matched, err := regexp.MatchString(regex, value)
if err != nil || !matched {
return false
}
return true
}
func (t *quadletTestcase) assertKeyContains(args []string, unit *parser.UnitFile) bool {
Expect(args).To(HaveLen(3))
group := args[0]
key := args[1]
value := args[2]
realValue, ok := unit.LookupLast(group, key)
return ok && strings.Contains(realValue, value)
}
func (t *quadletTestcase) assertPodmanArgs(args []string, unit *parser.UnitFile, key string, allowRegex, globalOnly bool) bool {
podmanArgs, _ := unit.LookupLastArgs("Service", key)
if globalOnly {
podmanCmdLocation := findSublist(podmanArgs, []string{args[0]})
if podmanCmdLocation == -1 {
return false
}
podmanArgs = podmanArgs[:podmanCmdLocation]
args = args[1:]
}
var location int
if allowRegex {
location = findSublistRegex(podmanArgs, args)
} else {
location = findSublist(podmanArgs, args)
}
return location != -1
}
func keyValueStringToMap(keyValueString, separator string) (map[string]string, error) {
keyValMap := make(map[string]string)
csvReader := csv.NewReader(strings.NewReader(keyValueString))
csvReader.Comma = []rune(separator)[0]
keyVarList, err := csvReader.ReadAll()
if err != nil {
return nil, err
}
for _, param := range keyVarList[0] {
key, val, _ := strings.Cut(param, "=")
keyValMap[key] = val
}
return keyValMap, nil
}
func keyValMapEqualRegex(expectedKeyValMap, actualKeyValMap map[string]string) bool {
if len(expectedKeyValMap) != len(actualKeyValMap) {
return false
}
for key, expectedValue := range expectedKeyValMap {
actualValue, ok := actualKeyValMap[key]
if !ok {
return false
}
matched, err := regexp.MatchString(expectedValue, actualValue)
if err != nil || !matched {
return false
}
}
return true
}
func (t *quadletTestcase) assertPodmanArgsKeyVal(args []string, unit *parser.UnitFile, key string, allowRegex, globalOnly bool) bool {
podmanArgs, _ := unit.LookupLastArgs("Service", key)
if globalOnly {
podmanCmdLocation := findSublist(podmanArgs, []string{args[0]})
if podmanCmdLocation == -1 {
return false
}
podmanArgs = podmanArgs[:podmanCmdLocation]
args = args[1:]
}
expectedKeyValMap, err := keyValueStringToMap(args[2], args[1])
if err != nil {
return false
}
argKeyLocation := 0
for {
subListLocation := findSublist(podmanArgs[argKeyLocation:], []string{args[0]})
if subListLocation == -1 {
break
}
argKeyLocation += subListLocation
actualKeyValMap, err := keyValueStringToMap(podmanArgs[argKeyLocation+1], args[1])
if err != nil {
break
}
if allowRegex {
if keyValMapEqualRegex(expectedKeyValMap, actualKeyValMap) {
return true
}
} else if reflect.DeepEqual(expectedKeyValMap, actualKeyValMap) {
return true
}
argKeyLocation += 2
if argKeyLocation > len(podmanArgs) {
break
}
}
return false
}
func (t *quadletTestcase) assertPodmanFinalArgs(args []string, unit *parser.UnitFile, key string) bool {
podmanArgs, _ := unit.LookupLastArgs("Service", key)
if len(podmanArgs) < len(args) {
return false
}
return matchSublistAt(podmanArgs, len(podmanArgs)-len(args), args)
}
func (t *quadletTestcase) assertPodmanFinalArgsRegex(args []string, unit *parser.UnitFile, key string) bool {
podmanArgs, _ := unit.LookupLastArgs("Service", key)
if len(podmanArgs) < len(args) {
return false
}
return matchSublistRegexAt(podmanArgs, len(podmanArgs)-len(args), args)
}
func (t *quadletTestcase) assertStartPodmanArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStart", false, false)
}
func (t *quadletTestcase) assertStartPodmanArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStart", true, false)
}
func (t *quadletTestcase) assertStartPodmanGlobalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStart", false, true)
}
func (t *quadletTestcase) assertStartPodmanGlobalArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStart", true, true)
}
func (t *quadletTestcase) assertStartPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", false, false)
}
func (t *quadletTestcase) assertStartPodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", true, false)
}
func (t *quadletTestcase) assertStartPodmanGlobalArgsKeyVal(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", false, true)
}
func (t *quadletTestcase) assertStartPodmanGlobalArgsKeyValRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStart", true, true)
}
func (t *quadletTestcase) assertStartPodmanFinalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgs(args, unit, "ExecStart")
}
func (t *quadletTestcase) assertStartPodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgsRegex(args, unit, "ExecStart")
}
func (t *quadletTestcase) assertStartPrePodmanArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStartPre", false, false)
}
func (t *quadletTestcase) assertStartPrePodmanArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStartPre", true, false)
}
func (t *quadletTestcase) assertStartPrePodmanGlobalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStartPre", false, true)
}
func (t *quadletTestcase) assertStartPrePodmanGlobalArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStartPre", true, true)
}
func (t *quadletTestcase) assertStartPrePodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", false, false)
}
func (t *quadletTestcase) assertStartPrePodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", true, false)
}
func (t *quadletTestcase) assertStartPrePodmanGlobalArgsKeyVal(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", false, true)
}
func (t *quadletTestcase) assertStartPrePodmanGlobalArgsKeyValRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStartPre", true, true)
}
func (t *quadletTestcase) assertStartPrePodmanFinalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgs(args, unit, "ExecStartPre")
}
func (t *quadletTestcase) assertStartPrePodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgsRegex(args, unit, "ExecStartPre")
}
func (t *quadletTestcase) assertStopPodmanArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStop", false, false)
}
func (t *quadletTestcase) assertStopPodmanGlobalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStop", false, true)
}
func (t *quadletTestcase) assertStopPodmanFinalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgs(args, unit, "ExecStop")
}
func (t *quadletTestcase) assertStopPodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgsRegex(args, unit, "ExecStop")
}
func (t *quadletTestcase) assertStopPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStop", false, false)
}
func (t *quadletTestcase) assertStopPodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStop", true, false)
}
func (t *quadletTestcase) assertStopPostPodmanArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStopPost", false, false)
}
func (t *quadletTestcase) assertStopPostPodmanGlobalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgs(args, unit, "ExecStopPost", false, true)
}
func (t *quadletTestcase) assertStopPostPodmanFinalArgs(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgs(args, unit, "ExecStopPost")
}
func (t *quadletTestcase) assertStopPostPodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanFinalArgsRegex(args, unit, "ExecStopPost")
}
func (t *quadletTestcase) assertStopPostPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStopPost", false, false)
}
func (t *quadletTestcase) assertStopPostPodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool {
return t.assertPodmanArgsKeyVal(args, unit, "ExecStopPost", true, false)
}
func (t *quadletTestcase) assertSymlink(args []string, unit *parser.UnitFile) bool {
Expect(args).To(HaveLen(2))
symlink := args[0]
expectedTarget := args[1]
dir := filepath.Dir(unit.Path)
target, err := os.Readlink(filepath.Join(dir, symlink))
Expect(err).ToNot(HaveOccurred())
return expectedTarget == target
}
func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, session *PodmanSessionIntegration) error {
Expect(check).ToNot(BeEmpty())
op := check[0]
args := make([]string, 0)
for _, a := range check[1:] {
// Apply \n and \t as they are used in the testcases
a = strings.ReplaceAll(a, "\\n", "\n")
a = strings.ReplaceAll(a, "\\t", "\t")
args = append(args, a)
}
invert := false
if op[0] == '!' {
invert = true
op = op[1:]
}
var ok bool
switch op {
case "assert-failed":
ok = true /* Handled separately */
case "assert-stderr-contains":
ok = t.assertStdErrContains(args, session)
case "assert-key-is":
ok = t.assertKeyIs(args, unit)
case "assert-key-is-empty":
ok = t.assertKeyIsEmpty(args, unit)
case "assert-key-is-regex":
ok = t.assertKeyIsRegex(args, unit)
case "assert-key-contains":
ok = t.assertKeyContains(args, unit)
case "assert-last-key-is-regex":
ok = t.assertLastKeyIsRegex(args, unit)
case "assert-podman-args":
ok = t.assertStartPodmanArgs(args, unit)
case "assert-podman-args-regex":
ok = t.assertStartPodmanArgsRegex(args, unit)
case "assert-podman-args-key-val":
ok = t.assertStartPodmanArgsKeyVal(args, unit)
case "assert-podman-args-key-val-regex":
ok = t.assertStartPodmanArgsKeyValRegex(args, unit)
case "assert-podman-global-args":
ok = t.assertStartPodmanGlobalArgs(args, unit)
case "assert-podman-global-args-regex":
ok = t.assertStartPodmanGlobalArgsRegex(args, unit)
case "assert-podman-global-args-key-val":
ok = t.assertStartPodmanGlobalArgsKeyVal(args, unit)
case "assert-podman-global-args-key-val-regex":
ok = t.assertStartPodmanGlobalArgsKeyValRegex(args, unit)
case "assert-podman-final-args":
ok = t.assertStartPodmanFinalArgs(args, unit)
case "assert-podman-final-args-regex":
ok = t.assertStartPodmanFinalArgsRegex(args, unit)
case "assert-podman-pre-args":
ok = t.assertStartPrePodmanArgs(args, unit)
case "assert-podman-pre-args-regex":
ok = t.assertStartPrePodmanArgsRegex(args, unit)
case "assert-podman-pre-args-key-val":
ok = t.assertStartPrePodmanArgsKeyVal(args, unit)
case "assert-podman-pre-args-key-val-regex":
ok = t.assertStartPrePodmanArgsKeyValRegex(args, unit)
case "assert-podman-pre-global-args":
ok = t.assertStartPrePodmanGlobalArgs(args, unit)
case "assert-podman-pre-global-args-regex":
ok = t.assertStartPrePodmanGlobalArgsRegex(args, unit)
case "assert-podman-pre-global-args-key-val":
ok = t.assertStartPrePodmanGlobalArgsKeyVal(args, unit)
case "assert-podman-pre-global-args-key-val-regex":
ok = t.assertStartPrePodmanGlobalArgsKeyValRegex(args, unit)
case "assert-podman-pre-final-args":
ok = t.assertStartPrePodmanFinalArgs(args, unit)
case "assert-podman-pre-final-args-regex":
ok = t.assertStartPrePodmanFinalArgsRegex(args, unit)
case "assert-symlink":
ok = t.assertSymlink(args, unit)
case "assert-podman-stop-args":
ok = t.assertStopPodmanArgs(args, unit)
case "assert-podman-stop-global-args":
ok = t.assertStopPodmanGlobalArgs(args, unit)
case "assert-podman-stop-final-args":
ok = t.assertStopPodmanFinalArgs(args, unit)
case "assert-podman-stop-final-args-regex":
ok = t.assertStopPodmanFinalArgsRegex(args, unit)
case "assert-podman-stop-args-key-val":
ok = t.assertStopPodmanArgsKeyVal(args, unit)
case "assert-podman-stop-args-key-val-regex":
ok = t.assertStopPodmanArgsKeyValRegex(args, unit)
case "assert-podman-stop-post-args":
ok = t.assertStopPostPodmanArgs(args, unit)
case "assert-podman-stop-post-global-args":
ok = t.assertStopPostPodmanGlobalArgs(args, unit)
case "assert-podman-stop-post-final-args":
ok = t.assertStopPostPodmanFinalArgs(args, unit)
case "assert-podman-stop-post-final-args-regex":
ok = t.assertStopPostPodmanFinalArgsRegex(args, unit)
case "assert-podman-stop-post-args-key-val":
ok = t.assertStopPostPodmanArgsKeyVal(args, unit)
case "assert-podman-stop-post-args-key-val-regex":
ok = t.assertStopPostPodmanArgsKeyValRegex(args, unit)
default:
return fmt.Errorf("Unsupported assertion %s", op)
}
if invert {
ok = !ok
}
if !ok {
s := "(nil)"
if unit != nil {
s, _ = unit.ToString()
}
return fmt.Errorf("Failed assertion for %s: %s\n\n%s", t.serviceName, strings.Join(check, " "), s)
}
return nil
}
func (t *quadletTestcase) check(generateDir string, session *PodmanSessionIntegration) {
expectFail := false
for _, c := range t.checks {
if c[0] == "assert-failed" {
expectFail = true
}
}
file := filepath.Join(generateDir, t.serviceName)
_, err := os.Stat(file)
if expectFail {
Expect(err).To(MatchError(os.ErrNotExist))
} else {
Expect(err).ToNot(HaveOccurred())
}
var unit *parser.UnitFile
if !expectFail {
unit, err = parser.ParseUnitFile(file)
Expect(err).ToNot(HaveOccurred())
}
for _, check := range t.checks {
err := t.doAssert(check, unit, session)
Expect(err).ToNot(HaveOccurred())
}
}
var _ = Describe("quadlet system generator", func() {
var (
err error
generatedDir string
quadletDir string
runQuadletTestCaseWithServiceName = func(fileName string, exitCode int, errString string, serviceName string) {
testcase := loadQuadletTestcaseWithServiceName(filepath.Join("quadlet", fileName), serviceName)
// Write the tested file to the quadlet dir
err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644)
Expect(err).ToNot(HaveOccurred())
// Also copy any extra snippets
snippetdirs := []string{fileName + ".d"}
if ok, genericFileName := getGenericTemplateFile(fileName); ok {
snippetdirs = append(snippetdirs, genericFileName+".d")
}
for _, snippetdir := range snippetdirs {
dotdDir := filepath.Join("quadlet", snippetdir)
if s, err := os.Stat(dotdDir); err == nil && s.IsDir() {
dotdDirDest := filepath.Join(quadletDir, snippetdir)
err = os.Mkdir(dotdDirDest, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = CopyDirectory(dotdDir, dotdDirDest)
Expect(err).ToNot(HaveOccurred())
}
}
// Run quadlet to convert the file
var args []string
if isRootless() {
args = append(args, "--user")
}
args = append(args, "--no-kmsg-log", generatedDir)
session := podmanTest.Quadlet(args, quadletDir)
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(exitCode))
// Print any stderr output
errs := session.ErrorToString()
if errs != "" {
GinkgoWriter.Println("error:", session.ErrorToString())
}
Expect(errs).Should(ContainSubstring(errString))
testcase.check(generatedDir, session)
}
runQuadletTestCase = func(fileName string, exitCode int, errString string) {
runQuadletTestCaseWithServiceName(fileName, exitCode, errString, "")
}
runSuccessQuadletTestCase = func(fileName string) {
runQuadletTestCase(fileName, 0, "")
}
runErrorQuadletTestCase = func(fileName, errString string) {
runQuadletTestCase(fileName, 1, errString)
}
runWarningQuadletTestCase = func(fileName, errString string) {
runQuadletTestCase(fileName, 0, errString)
}
)
BeforeEach(func() {
generatedDir = filepath.Join(podmanTest.TempDir, "generated")
err = os.Mkdir(generatedDir, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
quadletDir = filepath.Join(podmanTest.TempDir, "quadlet")
err = os.Mkdir(quadletDir, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
})
Describe("quadlet -version", func() {
It("Should print correct version", func() {
session := podmanTest.Quadlet([]string{"-version"}, "/something")
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
Expect(session.OutputToString()).To(Equal(version.Version.String()))
})
})
Describe("Running quadlet dryrun tests", func() {
It("Should exit with an error because of no files are found to parse", func() {
fileName := "basic.kube"
testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName))
// Write the tested file to the quadlet dir
err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644)
Expect(err).ToNot(HaveOccurred())
session := podmanTest.Quadlet([]string{"-dryrun"}, "/something")
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
current := session.ErrorToStringArray()
expected := "No files parsed from [/something]"
found := false
for _, line := range current {
if strings.Contains(line, expected) {
found = true
break
}
}
Expect(found).To(BeTrue())
})
It("Should fail on bad quadlet", func() {
quadletfile := fmt.Sprintf(`[Container]
Image=%s
BOGUS=foo
`, ALPINE)
quadletfilePath := filepath.Join(podmanTest.TempDir, "bogus.container")
err = os.WriteFile(quadletfilePath, []byte(quadletfile), 0644)
Expect(err).ToNot(HaveOccurred())
defer os.Remove(quadletfilePath)
session := podmanTest.Quadlet([]string{"-dryrun"}, podmanTest.TempDir)
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(1))
Expect(session.ErrorToString()).To(ContainSubstring("converting \"bogus.container\": unsupported key 'BOGUS' in group 'Container' in " + quadletfilePath))
})
It("Should scan and return output for files in subdirectories", func() {
dirName := "test_subdir"
err = CopyDirectory(filepath.Join("quadlet", dirName), quadletDir)
if err != nil {
GinkgoWriter.Println("error:", err)
}
session := podmanTest.Quadlet([]string{"-dryrun", "-user"}, quadletDir)
session.WaitWithDefaultTimeout()
current := session.OutputToStringArray()
expected := []string{
"---mysleep.service---",
"---mysleep_1.service---",
"---mysleep_2.service---",
}
Expect(current).To(ContainElements(expected))
})
It("Should parse a kube file and print it to stdout", func() {
fileName := "basic.kube"
testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName))
// quadlet uses PODMAN env to get a stable podman path
podmanPath, found := os.LookupEnv("PODMAN")
if !found {
podmanPath = podmanTest.PodmanBinary
}
// Write the tested file to the quadlet dir
err = os.WriteFile(filepath.Join(quadletDir, fileName), testcase.data, 0644)
Expect(err).ToNot(HaveOccurred())
session := podmanTest.Quadlet([]string{"-dryrun"}, quadletDir)
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
Expect(session.ErrorToString()).To(ContainSubstring("Loading source unit file "))
current := session.OutputToStringArray()
expected := []string{
"---basic.service---",
"## assert-podman-args \"kube\"",
"## assert-podman-args \"play\"",
"## assert-podman-final-args-regex .*/podman-e2e-.*/subtest-.*/quadlet/deployment.yml",
"## assert-podman-args \"--replace\"",
"## assert-podman-args \"--service-container=true\"",
"## assert-podman-stop-post-args \"kube\"",
"## assert-podman-stop-post-args \"down\"",
"## assert-podman-stop-post-final-args-regex .*/podman-e2e-.*/subtest-.*/quadlet/deployment.yml",
"## assert-key-is \"Unit\" \"RequiresMountsFor\" \"%t/containers\"",
"## assert-key-is \"Service\" \"KillMode\" \"mixed\"",
"## assert-key-is \"Service\" \"Type\" \"notify\"",
"## assert-key-is \"Service\" \"NotifyAccess\" \"all\"",
"## assert-key-is \"Service\" \"Environment\" \"PODMAN_SYSTEMD_UNIT=%n\"",
"## assert-key-is \"Service\" \"SyslogIdentifier\" \"%N\"",
"[X-Kube]",
"Yaml=deployment.yml",
"[Unit]",
"Wants=network-online.target",
"After=network-online.target",
fmt.Sprintf("SourcePath=%s/basic.kube", quadletDir),
"RequiresMountsFor=%t/containers",
"[Service]",
"KillMode=mixed",
"Environment=PODMAN_SYSTEMD_UNIT=%n",
"Type=notify",
"NotifyAccess=all",
"SyslogIdentifier=%N",
fmt.Sprintf("ExecStart=%s kube play --replace --service-container=true %s/deployment.yml", podmanPath, quadletDir),
fmt.Sprintf("ExecStopPost=%s kube down %s/deployment.yml", podmanPath, quadletDir),
}
Expect(current).To(Equal(expected))
})
})
DescribeTable("Running success quadlet test case",
runSuccessQuadletTestCase,
Entry("Basic container", "basic.container"),
Entry("annotation.container", "annotation.container"),
Entry("autoupdate.container", "autoupdate.container"),
Entry("basepodman.container", "basepodman.container"),
Entry("capabilities.container", "capabilities.container"),
Entry("capabilities2.container", "capabilities2.container"),
Entry("comment-with-continuation.container", "comment-with-continuation.container"),
Entry("devices.container", "devices.container"),
Entry("disableselinux.container", "disableselinux.container"),
Entry("dns-options.container", "dns-options.container"),
Entry("dns-search.container", "dns-search.container"),
Entry("dns.container", "dns.container"),
Entry("env-file.container", "env-file.container"),
Entry("env-host-false.container", "env-host-false.container"),
Entry("env-host.container", "env-host.container"),
Entry("env.container", "env.container"),
Entry("entrypoint.container", "entrypoint.container"),
Entry("escapes.container", "escapes.container"),
Entry("exec.container", "exec.container"),
Entry("group-add.container", "group-add.container"),
Entry("health.container", "health.container"),
Entry("host.container", "host.container"),
Entry("hostname.container", "hostname.container"),
Entry("idmapping.container", "idmapping.container"),
Entry("image.container", "image.container"),
Entry("install.container", "install.container"),
Entry("ip.container", "ip.container"),
Entry("label.container", "label.container"),
Entry("line-continuation-whitespace.container", "line-continuation-whitespace.container"),
Entry("logdriver.container", "logdriver.container"),
Entry("logopt.container", "logopt.container"),
Entry("mask.container", "mask.container"),
Entry("name.container", "name.container"),
Entry("nestedselinux.container", "nestedselinux.container"),
Entry("network.container", "network.container"),
Entry("notify.container", "notify.container"),
Entry("notify-healthy.container", "notify-healthy.container"),
Entry("oneshot.container", "oneshot.container"),
Entry("other-sections.container", "other-sections.container"),
Entry("podmanargs.container", "podmanargs.container"),
Entry("ports.container", "ports.container"),
Entry("ports_ipv6.container", "ports_ipv6.container"),
Entry("pull.container", "pull.container"),
Entry("quotes.container", "quotes.container"),
Entry("readonly.container", "readonly.container"),
Entry("readonly-tmpfs.container", "readonly-tmpfs.container"),
Entry("readonly-notmpfs.container", "readonly-notmpfs.container"),
Entry("readwrite-notmpfs.container", "readwrite-notmpfs.container"),
Entry("volatiletmp-readwrite.container", "volatiletmp-readwrite.container"),
Entry("volatiletmp-readonly.container", "volatiletmp-readonly.container"),
Entry("remap-auto.container", "remap-auto.container"),
Entry("remap-auto2.container", "remap-auto2.container"),
Entry("remap-keep-id.container", "remap-keep-id.container"),
Entry("remap-keep-id2.container", "remap-keep-id2.container"),
Entry("remap-manual.container", "remap-manual.container"),
Entry("rootfs.container", "rootfs.container"),
Entry("seccomp.container", "seccomp.container"),
Entry("secrets.container", "secrets.container"),
Entry("selinux.container", "selinux.container"),
Entry("shmsize.container", "shmsize.container"),
Entry("stopsigal.container", "stopsignal.container"),
Entry("stoptimeout.container", "stoptimeout.container"),
Entry("subidmapping.container", "subidmapping.container"),
Entry("sysctl.container", "sysctl.container"),
Entry("timezone.container", "timezone.container"),
Entry("ulimit.container", "ulimit.container"),
Entry("unmask.container", "unmask.container"),
Entry("user.container", "user.container"),
Entry("userns.container", "userns.container"),
Entry("workingdir.container", "workingdir.container"),
Entry("Container - global args", "globalargs.container"),
Entry("Container - Containers Conf Modules", "containersconfmodule.container"),
Entry("merged.container", "merged.container"),
Entry("merged-override.container", "merged-override.container"),
Entry("template@.container", "template@.container"),
Entry("template@instance.container", "template@instance.container"),
Entry("Unit After Override", "unit-after-override.container"),
Entry("NetworkAlias", "network-alias.container"),
Entry("CgroupMode", "cgroups-mode.container"),
Entry("Container - No Default Dependencies", "no_deps.container"),
Entry("basic.volume", "basic.volume"),
Entry("device-copy.volume", "device-copy.volume"),
Entry("device.volume", "device.volume"),
Entry("label.volume", "label.volume"),
Entry("name.volume", "name.volume"),
Entry("podmanargs.volume", "podmanargs.volume"),
Entry("uid.volume", "uid.volume"),
Entry("image.volume", "image.volume"),
Entry("Volume - global args", "globalargs.volume"),
Entry("Volume - Containers Conf Modules", "containersconfmodule.volume"),
Entry("Absolute Path", "absolute.path.kube"),
Entry("Basic kube", "basic.kube"),
Entry("Kube - ConfigMap", "configmap.kube"),
Entry("Kube - Exit Code Propagation", "exit_code_propagation.kube"),
Entry("Kube - Logdriver", "logdriver.kube"),
Entry("Kube - Logopt", "logopt.kube"),
Entry("Kube - Network", "network.kube"),
Entry("Kube - PodmanArgs", "podmanargs.kube"),
Entry("Kube - Publish IPv4 ports", "ports.kube"),
Entry("Kube - Publish IPv6 ports", "ports_ipv6.kube"),
Entry("Kube - User Remap Auto with IDs", "remap-auto2.kube"),
Entry("Kube - User Remap Auto", "remap-auto.kube"),
Entry("Syslog Identifier", "syslog.identifier.kube"),
Entry("Kube - Working Directory YAML Absolute Path", "workingdir-yaml-abs.kube"),
Entry("Kube - Working Directory YAML Relative Path", "workingdir-yaml-rel.kube"),
Entry("Kube - Working Directory Unit", "workingdir-unit.kube"),
Entry("Kube - Working Directory already in Service", "workingdir-service.kube"),
Entry("Kube - global args", "globalargs.kube"),
Entry("Kube - Containers Conf Modules", "containersconfmodule.kube"),
Entry("Kube - Service Type=oneshot", "oneshot.kube"),
Entry("Kube - Down force", "downforce.kube"),
Entry("Network - Basic", "basic.network"),
Entry("Network - Disable DNS", "disable-dns.network"),
Entry("Network - DNS", "dns.network"),
Entry("Network - Driver", "driver.network"),
Entry("Network - Gateway", "gateway.network"),
Entry("Network - IPAM Driver", "ipam-driver.network"),
Entry("Network - IPv6", "ipv6.network"),
Entry("Network - Internal network", "internal.network"),
Entry("Network - Label", "label.network"),
Entry("Network - Multiple Options", "options.multiple.network"),
Entry("Network - Name", "name.network"),
Entry("Network - Options", "options.network"),
Entry("Network - PodmanArgs", "podmanargs.network"),
Entry("Network - Range", "range.network"),
Entry("Network - Subnets", "subnets.network"),
Entry("Network - multiple subnet, gateway and range", "subnet-trio.multiple.network"),
Entry("Network - subnet, gateway and range", "subnet-trio.network"),
Entry("Network - global args", "globalargs.network"),
Entry("Network - Containers Conf Modules", "containersconfmodule.network"),
Entry("Image - Basic", "basic.image"),
Entry("Image - Architecture", "arch.image"),
Entry("Image - Auth File", "auth.image"),
Entry("Image - Certificates", "certs.image"),
Entry("Image - Credentials", "creds.image"),
Entry("Image - Decryption Key", "decrypt.image"),
Entry("Image - OS Key", "os.image"),
Entry("Image - Variant Key", "variant.image"),
Entry("Image - All Tags", "all-tags.image"),
Entry("Image - TLS Verify", "tls-verify.image"),
Entry("Image - Arch and OS", "arch-os.image"),
Entry("Image - global args", "globalargs.image"),
Entry("Image - Containers Conf Modules", "containersconfmodule.image"),
Entry("Image - Unit After Override", "unit-after-override.image"),
Entry("Image - No Default Dependencies", "no_deps.image"),
Entry("Build - Basic", "basic.build"),
Entry("Build - Annotation Key", "annotation.build"),
Entry("Build - Arch Key", "arch.build"),
Entry("Build - AuthFile Key", "authfile.build"),
Entry("Build - DNS Key", "dns.build"),
Entry("Build - DNSOptions Key", "dns-options.build"),
Entry("Build - DNSSearch Key", "dns-search.build"),
Entry("Build - Environment Key", "env.build"),
Entry("Build - File Key absolute", "file-abs.build"),
Entry("Build - File Key relative", "file-rel.build"),
Entry("Build - File Key HTTP(S) URL", "file-https.build"),
Entry("Build - ForceRM Key", "force-rm.build"),
Entry("Build - GlobalArgs", "globalargs.build"),
Entry("Build - GroupAdd Key", "group-add.build"),
Entry("Build - Containers Conf Modules", "containersconfmodule.build"),
Entry("Build - Label Key", "label.build"),
Entry("Build - Multiple Tags", "multiple-tags.build"),
Entry("Build - Network Key host", "network.build"),
Entry("Build - PodmanArgs", "podmanargs.build"),
Entry("Build - Pull Key", "pull.build"),
Entry("Build - Secrets", "secrets.build"),
Entry("Build - SetWorkingDirectory is absolute path", "setworkingdirectory-is-abs.build"),
Entry("Build - SetWorkingDirectory is absolute File= path", "setworkingdirectory-is-file-abs.build"),
Entry("Build - SetWorkingDirectory is relative path", "setworkingdirectory-is-rel.build"),
Entry("Build - SetWorkingDirectory is relative File= path", "setworkingdirectory-is-file-rel.build"),
Entry("Build - SetWorkingDirectory is https://.git URL", "setworkingdirectory-is-https-git.build"),
Entry("Build - SetWorkingDirectory is git:// URL", "setworkingdirectory-is-git.build"),
Entry("Build - SetWorkingDirectory is github.com URL", "setworkingdirectory-is-github.build"),
Entry("Build - SetWorkingDirectory is archive URL", "setworkingdirectory-is-archive.build"),
Entry("Build - Target Key", "target.build"),
Entry("Build - TLSVerify Key", "tls-verify.build"),
Entry("Build - Variant Key", "variant.build"),
Entry("Build - No Default Dependencies", "no_deps.build"),
Entry("Pod - Basic", "basic.pod"),
Entry("Pod - DNS", "dns.pod"),
Entry("Pod - DNS Option", "dns-option.pod"),
Entry("Pod - DNS Search", "dns-search.pod"),
Entry("Pod - Host", "host.pod"),
Entry("Pod - IP", "ip.pod"),
Entry("Pod - Name", "name.pod"),
Entry("Pod - Network", "network.pod"),
Entry("Pod - PodmanArgs", "podmanargs.pod"),
Entry("Pod - NetworkAlias", "network-alias.pod"),
Entry("Pod - Remap auto", "remap-auto.pod"),
Entry("Pod - Remap auto2", "remap-auto2.pod"),
Entry("Pod - Remap keep-id", "remap-keep-id.pod"),
Entry("Pod - Remap manual", "remap-manual.pod"),
)
DescribeTable("Running expected warning quadlet test case",
runWarningQuadletTestCase,
Entry("shortname.container", "shortname.container", "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."),
)
DescribeTable("Running expected error quadlet test case",
runErrorQuadletTestCase,
Entry("idmapping-with-remap.container", "idmapping-with-remap.container", "converting \"idmapping-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"),
Entry("noimage.container", "noimage.container", "converting \"noimage.container\": no Image or Rootfs key specified"),
Entry("pod.non-quadlet.container", "pod.non-quadlet.container", "converting \"pod.non-quadlet.container\": pod test-pod is not Quadlet based"),
Entry("pod.not-found.container", "pod.not-found.container", "converting \"pod.not-found.container\": quadlet pod unit not-found.pod does not exist"),
Entry("subidmapping-with-remap.container", "subidmapping-with-remap.container", "converting \"subidmapping-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"),
Entry("userns-with-remap.container", "userns-with-remap.container", "converting \"userns-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"),
Entry("image-no-image.volume", "image-no-image.volume", "converting \"image-no-image.volume\": the key Image is mandatory when using the image driver"),
Entry("Volume - Quadlet image (.build) not found", "build-not-found.quadlet.volume", "converting \"build-not-found.quadlet.volume\": requested Quadlet image not-found.build was not found"),
Entry("Volume - Quadlet image (.image) not found", "image-not-found.quadlet.volume", "converting \"image-not-found.quadlet.volume\": requested Quadlet image not-found.image was not found"),
Entry("Kube - User Remap Manual", "remap-manual.kube", "converting \"remap-manual.kube\": RemapUsers=manual is not supported"),
Entry("Network - Gateway not enough Subnet", "gateway.less-subnet.network", "converting \"gateway.less-subnet.network\": cannot set more gateways than subnets"),
Entry("Network - Gateway without Subnet", "gateway.no-subnet.network", "converting \"gateway.no-subnet.network\": cannot set gateway or range without subnet"),
Entry("Network - Range not enough Subnet", "range.less-subnet.network", "converting \"range.less-subnet.network\": cannot set more ranges than subnets"),
Entry("Network - Range without Subnet", "range.no-subnet.network", "converting \"range.no-subnet.network\": cannot set gateway or range without subnet"),
Entry("Image - No Image", "no-image.image", "converting \"no-image.image\": no Image key specified"),
Entry("Build - File Key relative no WD", "file-rel-no-wd.build", "converting \"file-rel-no-wd.build\": relative path in File key requires SetWorkingDirectory key to be set"),
Entry("Build - Neither WorkingDirectory nor File Key", "neither-workingdirectory-nor-file.build", "converting \"neither-workingdirectory-nor-file.build\": neither SetWorkingDirectory, nor File key specified"),
Entry("Build - No ImageTag Key", "no-imagetag.build", "converting \"no-imagetag.build\": no ImageTag key specified"),
)
DescribeTable("Running success quadlet with ServiceName test case",
func(fileName, serviceName string) {
runQuadletTestCaseWithServiceName(fileName, 0, "", serviceName)
},
Entry("Build", "service-name.build", "basic"),
Entry("Container", "service-name.container", "basic"),
Entry("Image", "service-name.image", "basic"),
Entry("Kube", "service-name.kube", "basic"),
Entry("Network", "service-name.network", "basic"),
Entry("Pod", "service-name.pod", "basic"),
Entry("Volume", "service-name.volume", "basic"),
)
DescribeTable("Running quadlet success test case with dependencies",
func(fileName string, dependencyFiles []string) {
// Write additional files this test depends on to the quadlet dir
for _, dependencyFileName := range dependencyFiles {
dependencyTestCase := loadQuadletTestcase(filepath.Join("quadlet", dependencyFileName))
err = os.WriteFile(filepath.Join(quadletDir, dependencyFileName), dependencyTestCase.data, 0644)
Expect(err).ToNot(HaveOccurred())
}
runSuccessQuadletTestCase(fileName)
},
Entry("Container - Mount", "mount.container", []string{"basic.volume"}),
Entry("Container - Quadlet Network", "network.quadlet.container", []string{"basic.network"}),
Entry("Container - Quadlet Volume", "volume.container", []string{"basic.volume"}),
Entry("Container - Mount overriding service name", "mount.servicename.container", []string{"service-name.volume"}),
Entry("Container - Quadlet Network overriding service name", "network.quadlet.servicename.container", []string{"service-name.network"}),
Entry("Container - Quadlet Volume overriding service name", "volume.servicename.container", []string{"service-name.volume"}),
Entry("Container - Quadlet build with multiple tags", "build.multiple-tags.container", []string{"multiple-tags.build"}),
Entry("Container - Reuse another container's network", "network.reuse.container", []string{"basic.container"}),
Entry("Container - Reuse another named container's network", "network.reuse.name.container", []string{"name.container"}),
Entry("Volume - Quadlet image (.build)", "build.quadlet.volume", []string{"basic.build"}),
Entry("Volume - Quadlet image (.image)", "image.quadlet.volume", []string{"basic.image"}),
Entry("Volume - Quadlet image (.build) overriding service name", "build.quadlet.servicename.volume", []string{"service-name.build"}),
Entry("Volume - Quadlet image (.image) overriding service name", "image.quadlet.servicename.volume", []string{"service-name.image"}),
Entry("Kube - Quadlet Network", "network.quadlet.kube", []string{"basic.network"}),
Entry("Kube - Quadlet Network overriding service name", "network.quadlet.servicename.kube", []string{"service-name.network"}),
Entry("Build - Network Key quadlet", "network.quadlet.build", []string{"basic.network"}),
Entry("Build - Volume Key", "volume.build", []string{"basic.volume"}),
Entry("Build - Volume Key quadlet", "volume.quadlet.build", []string{"basic.volume"}),
Entry("Build - Network Key quadlet overriding service name", "network.quadlet.servicename.build", []string{"service-name.network"}),
Entry("Build - Volume Key quadlet overriding service name", "volume.quadlet.servicename.build", []string{"service-name.volume"}),
Entry("Pod - Quadlet Network", "network.quadlet.pod", []string{"basic.network"}),
Entry("Pod - Quadlet Volume", "volume.pod", []string{"basic.volume"}),
Entry("Pod - Quadlet Network overriding service name", "network.servicename.quadlet.pod", []string{"service-name.network"}),
Entry("Pod - Quadlet Volume overriding service name", "volume.servicename.pod", []string{"service-name.volume"}),
)
})