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  })