diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index 621f22c197..c443b05f26 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -49,12 +49,13 @@ var ( // Key: Extension // Value: Processing order for resource naming dependencies supportedExtensions = map[string]int{ - ".container": 3, + ".container": 4, ".volume": 2, - ".kube": 3, + ".kube": 4, ".network": 2, ".image": 1, - ".pod": 4, + ".build": 3, + ".pod": 5, } ) @@ -474,7 +475,7 @@ func warnIfAmbiguousName(unit *parser.UnitFile, group string) { if !ok { return } - if strings.HasSuffix(imageName, ".image") { + if strings.HasSuffix(imageName, ".build") || strings.HasSuffix(imageName, ".image") { return } if !isUnambiguousName(imageName) { @@ -499,6 +500,19 @@ func generatePodsInfoMap(units []*parser.UnitFile) map[string]*quadlet.PodInfo { return podsInfoMap } +func prefillBuiltImageNames(units []*parser.UnitFile, resourceNames map[string]string) { + for _, unit := range units { + if !strings.HasSuffix(unit.Filename, ".build") { + continue + } + + imageName := quadlet.GetBuiltImageName(unit) + if len(imageName) > 0 { + resourceNames[unit.Filename] = imageName + } + } +} + func main() { if err := process(); err != nil { Logf("%s", err.Error()) @@ -600,6 +614,12 @@ func process() error { // A map of network/volume unit file-names, against their calculated names, as needed by Podman. var resourceNames = make(map[string]string) + // Prefill resouceNames for .build files. This is significantly less complex than + // pre-computing all resourceNames for all Quadlet types (which is rather complex for a few + // types), but still breaks the dependency cycle between .volume and .build ([Volume] can + // have Image=some.build, and [Build] can have Volume=some.volume:/some-volume) + prefillBuiltImageNames(units, resourceNames) + for _, unit := range units { var service *parser.UnitFile var name string @@ -619,6 +639,8 @@ func process() error { case strings.HasSuffix(unit.Filename, ".image"): warnIfAmbiguousName(unit, quadlet.ImageGroup) service, name, err = quadlet.ConvertImage(unit) + case strings.HasSuffix(unit.Filename, ".build"): + service, name, err = quadlet.ConvertBuild(unit, resourceNames) case strings.HasSuffix(unit.Filename, ".pod"): service, err = quadlet.ConvertPod(unit, unit.Filename, podsInfoMap, resourceNames) default: diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 3015ee326b..94489b2b56 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -6,7 +6,7 @@ podman\-systemd.unit - systemd units using Podman Quadlet ## SYNOPSIS -*name*.container, *name*.volume, *name*.network, *name*.kube *name*.image, *name*.pod +*name*.container, *name*.volume, *name*.network, *name*.kube *name*.image, *name*.build *name*.pod ### Podman rootful unit search path @@ -30,7 +30,7 @@ Symbolic links below the search paths are not supported. ## DESCRIPTION -Podman supports starting containers (and creating volumes) via systemd by using a +Podman supports building, and starting containers (and creating volumes) via systemd by using a [systemd generator](https://www.freedesktop.org/software/systemd/man/systemd.generator.html). These files are read during boot (and when `systemctl daemon-reload` is run) and generate corresponding regular systemd service unit files. Both system and user systemd units are supported. @@ -39,7 +39,7 @@ the [Service] table and [Install] tables pass directly to systemd and are handle See systemd.unit(5) man page for more information. The Podman generator reads the search paths above and reads files with the extensions `.container` -`.volume`, `.network`, `.pod` and `.kube`, and for each file generates a similarly named `.service` file. Be aware that +`.volume`, `.network`, `.build`, `.pod` and `.kube`, and for each file generates a similarly named `.service` file. Be aware that existing vendor services (i.e., in `/usr/`) are replaced if they have the same name. The generated unit files can be started and managed with `systemctl` like any other systemd service. `systemctl {--user} list-unit-files` lists existing unit files on the system. @@ -65,7 +65,7 @@ session gets started. For unit files placed in subdirectories within /etc/containers/systemd/user/${UID}/ and the other user unit search paths, Quadlet will recursively search and run the unit files present in these subdirectories. -Note: When a Quadlet is starting, Podman often pulls one more container images which may take a considerable amount of time. +Note: When a Quadlet is starting, Podman often pulls or builds one more container images which may take a considerable amount of time. Systemd defaults service start time to 90 seconds, or fails the service. Pre-pulling the image or extending the systemd timeout time for the service using the *TimeoutStartSec* Service option can fix the problem. @@ -82,7 +82,7 @@ Quadlet requires the use of cgroup v2, use `podman info --format {{.Host.Cgroups By default, the `Type` field of the `Service` section of the Quadlet file does not need to be set. Quadlet will set it to `notify` for `.container` and `.kube` files, -`forking` for `.pod` files, and `oneshot` for `.volume`, `.network` and `.image` files. +`forking` for `.pod` files, and `oneshot` for `.volume`, `.network`, `.build`, and `.image` files. However, `Type` may be explicitly set to `oneshot` for `.container` and `.kube` files when no containers are expected to run once `podman` exits. @@ -1324,6 +1324,251 @@ The (optional) name of the Podman volume. If this is not specified, the default `systemd-%N` is used, which is the same as the unit name but with a `systemd-` prefix to avoid conflicts with user-managed volumes. +## Build units [Build] + +Build files are named with a `.build` extension and contain a section `[Build]` describing the image +build command. The generated service is a one-time command that ensures that the image is built on +the host from a supplied Containerfile and context directory. Subsequent (re-)starts of the +generated built service will usually finish quickly, as image layer caching will skip unchanged +build steps. + +A minimal `.build` unit needs at least the `ImageTag=` key, and either of `File=` or +`SetWorkingDirectory=` keys. + +Using build units allows containers and volumes to depend on images being built locally. This can be +interesting for creating container images not available on container registries, or for local +testing and development. + +Valid options for `[Build]` are listed below: + +| **[Build] options** | **podman build equivalent** | +|-------------------------------------|---------------------------------------------| +| Annotation=annotation=value | --annotation=annotation=value | +| Arch=aarch64 | --arch=aarch64 | +| AuthFile=/etc/registry/auth\.json | --authfile=/etc/registry/auth\.json | +| ContainersConfModule=/etc/nvd\.conf | --module=/etc/nvd\.conf | +| DNS=192.168.55.1 | --dns=192.168.55.1 | +| DNSOption=ndots:1 | --dns-option=ndots:1 | +| DNSSearch=foo.com | --dns-search=foo.com | +| Environment=foo=bar | --env foo=bar | +| File=/path/to/Containerfile | --file=/path/to/Containerfile | +| ForceRM=false | --force-rm=false | +| GlobalArgs=--log-level=debug | --log-level=debug | +| GroupAdd=keep-groups | --group-add=keep-groups | +| ImageTag=localhost/imagename | --tag=localhost/imagename | +| Label=label | --label=label | +| Network=host | --network=host | +| PodmanArgs=--add-host foobar | --add-host foobar | +| Pull=never | --pull=never | +| Secret=secret | --secret=id=mysecret,src=path | +| SetWorkingDirectory=unit | Set `WorkingDirectory` of systemd unit file | +| Target=my-app | --target=my-app | +| TLSVerify=false | --tls-verify=false | +| Variant=arm/v7 | --variant=arm/v7 | +| Volume=/source:/dest | --volume /source:/dest | + +### `Annotation=` + +Add an image *annotation* (e.g. annotation=*value*) to the image metadata. Can be used multiple +times. + +This is equivalant to the `--annotation` option of `podman build`. + +### `Arch=` + +Override the architecture, defaults to hosts', of the image to be built. + +This is equivalent to the `--arch` option of `podman build`. + +### `AuthFile=` + +Path of the authentication file. + +This is equivalent to the `--authfile` option of `podman build`. + +### `ContainersConfModule=` + +Load the specified containers.conf(5) module. Equivalent to the Podman `--module` option. + +This key can be listed multiple times. + +### `DNS=` + +Set network-scoped DNS resolver/nameserver for the build container. + +This key can be listed multiple times. + +This is equivalent to the `--dns` option of `podman build`. + +### `DNSOption=` + +Set custom DNS options. + +This key can be listed multiple times. + +This is equivalent to the `--dns-option` option of `podman build`. + +### `DNSSearch=` + +Set custom DNS search domains. Use **DNSSearch=.** to remove the search domain. + +This key can be listed multiple times. + +This is equivalent to the `--dns-search` option of `podman build`. + +### `Environment=` + +Add a value (e.g. env=*value*) to the built image. This uses the same format as [services in +systemd](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Environment=) and can be +listed multiple times. + +### `File=` + +Specifies a Containerfile which contains instructions for building the image. A URL starting with +`http(s)://` allows you to specify a remote Containerfile to be downloaded. Note that for a given +relative path to a Containerfile, or when using a `http(s)://` URL, you also must set +`SetWorkingDirectory=` in order for `podman build` to find a valid context directory for the +resources specified in the Containerfile. + +Note that setting a `File=` field is mandatory for a `.build` file, unless `SetWorkingDirectory` (or +a `WorkingDirectory` in the `Service` group) has also been set. + +This is equivalent to the `--file` option of `podman build`. + +### `ForceRM=` + +Always remove intermediate containers after a build, even if the build fails (default true). + +This is equivalent to the `--force-rm` option of `podman build`. + +### `GlobalArgs=` + +This key contains a list of arguments passed directly between `podman` and `build` in the generated +file. It can be used to access Podman features otherwise unsupported by the generator. Since the +generator is unaware of what unexpected interactions can be caused by these arguments, it is not +recommended to use this option. + +The format of this is a space separated list of arguments, which can optionally be individually +escaped to allow inclusion of whitespace and other control characters. + +This key can be listed multiple times. + +### `GroupAdd=` + +Assign additional groups to the primary user running within the container process. Also supports the +`keep-groups` special flag. + +This is equivalent to the `--group-add` option of `podman build`. + +### `ImageTag=` + +Specifies the name which is assigned to the resulting image if the build process completes +successfully. + +This is equivalent to the `--tag` option of `podman build`. + +### `Label=` + +Add an image *label* (e.g. label=*value*) to the image metadata. Can be used multiple times. + +This is equivalent to the `--label` option of `podman build`. + +### `Network=` + +Sets the configuration for network namespaces when handling RUN instructions. This has the same +format as the `--network` option to `podman build`. For example, use `host` to use the host network, +or `none` to not set up networking. + +As a special case, if the `name` of the network ends with `.network`, Quadlet will look for the +corresponding `.network` Quadlet unit. If found, Quadlet will use the name of the Network set in the +Unit, otherwise, `systemd-$name` is used. The generated systemd service contains a dependency on the +service unit generated for that `.network` unit, or on `$name-network.service` if the `.network` +unit is not found. + +This key can be listed multiple times. + +### `PodmanArgs=` + +This key contains a list of arguments passed directly to the end of the `podman build` command +in the generated file (right before the image name in the command line). It can be used to +access Podman features otherwise unsupported by the generator. Since the generator is unaware +of what unexpected interactions can be caused by these arguments, it is not recommended to use +this option. + +The format of this is a space separated list of arguments, which can optionally be individually +escaped to allow inclusion of whitespace and other control characters. + +This key can be listed multiple times. + +### `Pull=` + +Set the image pull policy. + +This is equivalent to the `--pull` option of `podman build`. + +### `Secret=` + +Pass secret information used in Containerfile build stages in a safe way. + +This is equivalent to the `--secret` option of `podman build` and generally has the form +`secret[,opt=opt ...]`. + +### `SetWorkingDirectory=` + +Provide context (a working directory) to `podman build`. Supported values are a path, a URL, or the +special keys `file` or `unit` to set the context directory to the parent directory of the file from +the `File=` key or to that of the Quadlet `.build` unit file, respectively. This allows Quadlet to +resolve relative paths. + +When using one of the special keys (`file` or `unit`), the `WorkingDirectory` field of the `Service` +group of the Systemd service unit will also be set to accordingly. Alternatively, users can +explicitly set the `WorkingDirectory` field of the `Service` group in the `.build` file. Please note +that if the `WorkingDirectory` field of the `Service` group is set by the user, Quadlet will not +overwrite it even if `SetWorkingDirectory` is set to `file` or `unit`. + +By providing a URL to `SetWorkingDirectory=` you can instruct `podman build` to clone a Git +repository or download an archive file extracted to a temporary location by `podman build` as build +context. Note that in this case, the `WorkingDirectory` of the Systemd service unit is left +untouched by Quadlet. + +Note that providing context directory is mandatory for a `.build` file, unless a `File=` key has +also been provided. + +### `Target=` + +Set the target build stage to build. Commands in the Containerfile after the target stage are +skipped. + +This is equivalent to the `--target` option of `podman build`. + +### `TLSVerify=` + +Require HTTPS and verification of certificates when contacting registries. + +This is equivalent to the `--tls-verify` option of `podman build`. + +### `Variant=` + +Override the default architecture variant of the container image to be built. + +This is equivalent to the `--variant` option of `podman build`. + +### `Volume=` + +Mount a volume to containers when executing RUN instructions during the build. This is equivalent to +the `--volume` option of `podman build`, and generally has the form +`[[SOURCE-VOLUME|HOST-DIR:]CONTAINER-DIR[:OPTIONS]]`. + +If `SOURCE-VOLUME` starts with `.`, Quadlet resolves the path relative to the location of the unit file. + +As a special case, if `SOURCE-VOLUME` ends with `.volume`, Quadlet will look for the corresponding +`.volume` Quadlet unit. If found, Quadlet will use the name of the Volume set in the Unit, +otherwise, `systemd-$name` is used. The generated systemd service contains a dependency on the +service unit generated for that `.volume` unit, or on `$name-volume.service` if the `.volume` unit +is not found + +This key can be listed multiple times. + ## Image units [Image] Image files are named with a `.image` extension and contain a section `[Image]` describing the @@ -1510,6 +1755,26 @@ Yaml=/opt/k8s/deployment.yml WantedBy=multi-user.target default.target ``` +Example for locally built image to be used in a container: + +`test.build` +``` +[Build] +# Tag the image to be built +ImageTag=localhost/imagename + +# Set the working directory to the path of the unit file, +# expecting to find a Containerfile/Dockerfile +# + other files needed to build the image +SetWorkingDirectory=unit +``` + +`test.container` +``` +[Container] +Image=test.build +``` + Example `test.volume`: ``` diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index 9c13d88dcb..6cb33bb09f 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -35,12 +35,14 @@ const ( UnitGroup = "Unit" VolumeGroup = "Volume" ImageGroup = "Image" + BuildGroup = "Build" XContainerGroup = "X-Container" XKubeGroup = "X-Kube" XNetworkGroup = "X-Network" XPodGroup = "X-Pod" XVolumeGroup = "X-Volume" XImageGroup = "X-Image" + XBuildGroup = "X-Build" ) // Systemd Unit file keys @@ -78,6 +80,8 @@ const ( KeyExec = "Exec" KeyExitCodePropagation = "ExitCodePropagation" KeyExposeHostPort = "ExposeHostPort" + KeyFile = "File" + KeyForceRM = "ForceRM" KeyGateway = "Gateway" KeyGIDMap = "GIDMap" KeyGlobalArgs = "GlobalArgs" @@ -142,6 +146,7 @@ const ( KeySubnet = "Subnet" KeySubUIDMap = "SubUIDMap" KeySysctl = "Sysctl" + KeyTarget = "Target" KeyTimezone = "Timezone" KeyTLSVerify = "TLSVerify" KeyTmpfs = "Tmpfs" @@ -165,6 +170,7 @@ type PodInfo struct { } var ( + URL = regexp.Delayed(`^((https?)|(git)://)|(github\.com/).+$`) validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`) // Supported keys in "Container" group @@ -323,6 +329,33 @@ var ( KeyVariant: true, } + // Supported keys in "Build" group + supportedBuildKeys = map[string]bool{ + KeyAnnotation: true, + KeyArch: true, + KeyAuthFile: true, + KeyContainersConfModule: true, + KeyDNS: true, + KeyDNSOption: true, + KeyDNSSearch: true, + KeyEnvironment: true, + KeyFile: true, + KeyForceRM: true, + KeyGlobalArgs: true, + KeyGroupAdd: true, + KeyImageTag: true, + KeyLabel: true, + KeyNetwork: true, + KeyPodmanArgs: true, + KeyPull: true, + KeySecret: true, + KeySetWorkingDirectory: true, + KeyTarget: true, + KeyTLSVerify: true, + KeyVariant: true, + KeyVolume: true, + } + supportedPodKeys = map[string]bool{ KeyContainersConfModule: true, KeyGlobalArgs: true, @@ -345,6 +378,10 @@ func replaceExtension(name string, extension string, extraPrefix string, extraSu return extraPrefix + baseName + extraSuffix + extension } +func isURL(urlCandidate string) bool { + return URL.MatchString(urlCandidate) +} + func isPortRange(port string) bool { return validPortRange.MatchString(port) } @@ -1178,7 +1215,7 @@ func ConvertKube(kube *parser.UnitFile, names map[string]string, isUser bool) (* execStop.add(yamlPath) service.AddCmdline(ServiceGroup, "ExecStopPost", execStop.Args) - err = handleSetWorkingDirectory(kube, service) + _, err = handleSetWorkingDirectory(kube, service, KubeGroup) if err != nil { return nil, err } @@ -1264,6 +1301,157 @@ func ConvertImage(image *parser.UnitFile) (*parser.UnitFile, string, error) { return service, imageName, nil } +func ConvertBuild(build *parser.UnitFile, names map[string]string) (*parser.UnitFile, string, error) { + service := build.Dup() + service.Filename = replaceExtension(build.Filename, ".service", "", "-build") + + // Add a dependency on network-online.target so the image pull does not happen + // before network is ready + // https://github.com/containers/podman/issues/21873 + // Prepend the lines, so the user-provided values + // override the default ones. + service.PrependUnitLine(UnitGroup, "After", "network-online.target") + service.PrependUnitLine(UnitGroup, "Wants", "network-online.target") + + /* Rename old Build group to X-Build so that systemd ignores it */ + service.RenameGroup(BuildGroup, XBuildGroup) + + // Need the containers filesystem mounted to start podman + service.Add(UnitGroup, "RequiresMountsFor", "%t/containers") + + if build.Path != "" { + service.Add(UnitGroup, "SourcePath", build.Path) + } + + if err := checkForUnknownKeys(build, BuildGroup, supportedBuildKeys); err != nil { + return nil, "", err + } + + podman := createBasePodmanCommand(build, BuildGroup) + podman.add("build") + + stringKeys := map[string]string{ + KeyArch: "--arch", + KeyAuthFile: "--authfile", + KeyPull: "--pull", + KeyTarget: "--target", + KeyVariant: "--variant", + } + + boolKeys := map[string]string{ + KeyTLSVerify: "--tls-verify", + KeyForceRM: "--force-rm", + } + + for key, flag := range stringKeys { + lookupAndAddString(build, BuildGroup, key, flag, podman) + } + + for key, flag := range boolKeys { + lookupAndAddBoolean(build, BuildGroup, key, flag, podman) + } + + annotations := build.LookupAllKeyVal(BuildGroup, KeyAnnotation) + podman.addAnnotations(annotations) + + dns := build.LookupAll(BuildGroup, KeyDNS) + for _, ipAddr := range dns { + podman.addf("--dns=%s", ipAddr) + } + + dnsOptions := build.LookupAll(BuildGroup, KeyDNSOption) + for _, dnsOption := range dnsOptions { + podman.addf("--dns-option=%s", dnsOption) + } + + dnsSearches := build.LookupAll(BuildGroup, KeyDNSSearch) + for _, dnsSearch := range dnsSearches { + podman.addf("--dns-search=%s", dnsSearch) + } + + podmanEnv := build.LookupAllKeyVal(BuildGroup, KeyEnvironment) + podman.addEnv(podmanEnv) + + groupsAdd := build.LookupAll(BuildGroup, KeyGroupAdd) + for _, groupAdd := range groupsAdd { + if len(groupAdd) > 0 { + podman.addf("--group-add=%s", groupAdd) + } + } + + labels := build.LookupAllKeyVal(BuildGroup, KeyLabel) + podman.addLabels(labels) + + builtImageName, ok := names[build.Filename] + if !ok { + return nil, "", fmt.Errorf("no ImageTag key specified") + } + + podman.addf("--tag=%s", builtImageName) + + addNetworks(build, BuildGroup, service, names, podman) + + secrets := build.LookupAllArgs(BuildGroup, KeySecret) + for _, secret := range secrets { + podman.add("--secret", secret) + } + + if err := addVolumes(build, service, BuildGroup, names, podman); err != nil { + return nil, "", err + } + + // In order to build an image locally, we need either a File key pointing directly at a + // Containerfile, or we need a context or WorkingDirectory containing all required files. + // SetWorkingDirectory= can also be a path, a URL to either a Containerfile, a Git repo, or + // an archive. + context, err := handleSetWorkingDirectory(build, service, BuildGroup) + if err != nil { + return nil, "", err + } + + workingDirectory, okWD := service.Lookup(ServiceGroup, ServiceKeyWorkingDirectory) + filePath, okFile := build.Lookup(BuildGroup, KeyFile) + if (!okWD || len(workingDirectory) == 0) && (!okFile || len(filePath) == 0) && len(context) == 0 { + return nil, "", fmt.Errorf("neither SetWorkingDirectory, nor File key specified") + } + + if len(filePath) > 0 { + podman.addf("--file=%s", filePath) + } + + handlePodmanArgs(build, BuildGroup, podman) + + // Context or WorkingDirectory has to be last argument + if len(context) > 0 { + podman.add(context) + } else if !filepath.IsAbs(filePath) && !isURL(filePath) { + // Special handling for relative filePaths + if len(workingDirectory) == 0 { + return nil, "", fmt.Errorf("relative path in File key requires SetWorkingDirectory key to be set") + } + podman.add(workingDirectory) + } + + service.AddCmdline(ServiceGroup, "ExecStart", podman.Args) + + service.Setv(ServiceGroup, + "Type", "oneshot", + "RemainAfterExit", "yes", + + // The default syslog identifier is the exec basename (podman) + // which isn't very useful here + "SyslogIdentifier", "%N") + + return service, builtImageName, nil +} + +func GetBuiltImageName(buildUnit *parser.UnitFile) string { + if builtImageName, ok := buildUnit.Lookup(BuildGroup, KeyImageTag); ok { + return builtImageName + } + return "" +} + func GetPodServiceName(podUnit *parser.UnitFile) string { return replaceExtension(podUnit.Filename, "", "", "-pod") } @@ -1686,39 +1874,67 @@ func handlePodmanArgs(unitFile *parser.UnitFile, groupName string, podman *Podma } } -func handleSetWorkingDirectory(kube, serviceUnitFile *parser.UnitFile) error { - // If WorkingDirectory is already set in the Service section do not change it - workingDir, ok := kube.Lookup(ServiceGroup, ServiceKeyWorkingDirectory) - if ok && len(workingDir) > 0 { - return nil - } - - setWorkingDirectory, ok := kube.Lookup(KubeGroup, KeySetWorkingDirectory) +func handleSetWorkingDirectory(quadletUnitFile, serviceUnitFile *parser.UnitFile, quadletGroup string) (string, error) { + setWorkingDirectory, ok := quadletUnitFile.Lookup(quadletGroup, KeySetWorkingDirectory) if !ok || len(setWorkingDirectory) == 0 { - return nil + return "", nil } var relativeToFile string + var context string switch strings.ToLower(setWorkingDirectory) { case "yaml": - relativeToFile, ok = kube.Lookup(KubeGroup, KeyYaml) + if quadletGroup != KubeGroup { + return "", fmt.Errorf("SetWorkingDirectory=%s is only supported in .kube files", setWorkingDirectory) + } + + relativeToFile, ok = quadletUnitFile.Lookup(quadletGroup, KeyYaml) if !ok { - return fmt.Errorf("no Yaml key specified") + return "", fmt.Errorf("no Yaml key specified") + } + case "file": + if quadletGroup != BuildGroup { + return "", fmt.Errorf("SetWorkingDirectory=%s is only supported in .build files", setWorkingDirectory) + } + + relativeToFile, ok = quadletUnitFile.Lookup(quadletGroup, KeyFile) + if !ok { + return "", fmt.Errorf("no File key specified") } case "unit": - relativeToFile = kube.Path + relativeToFile = quadletUnitFile.Path default: - return fmt.Errorf("unsupported value for %s: %s ", ServiceKeyWorkingDirectory, setWorkingDirectory) + // Path / URL handling is for .build files only + if quadletGroup != BuildGroup { + return "", fmt.Errorf("unsupported value for %s: %s ", ServiceKeyWorkingDirectory, setWorkingDirectory) + } + + // Any value other than the above cases will be returned as context + context = setWorkingDirectory + + // If we have a relative path, set the WorkingDirectory to that of the + // quadletUnitFile + if !filepath.IsAbs(context) { + relativeToFile = quadletUnitFile.Path + } } - fileInWorkingDir, err := getAbsolutePath(kube, relativeToFile) - if err != nil { - return err + if len(relativeToFile) > 0 && !isURL(context) { + // If WorkingDirectory is already set in the Service section do not change it + workingDir, ok := quadletUnitFile.Lookup(ServiceGroup, ServiceKeyWorkingDirectory) + if ok && len(workingDir) > 0 { + return "", nil + } + + fileInWorkingDir, err := getAbsolutePath(quadletUnitFile, relativeToFile) + if err != nil { + return "", err + } + + serviceUnitFile.Add(ServiceGroup, ServiceKeyWorkingDirectory, filepath.Dir(fileInWorkingDir)) } - serviceUnitFile.Add(ServiceGroup, ServiceKeyWorkingDirectory, filepath.Dir(fileInWorkingDir)) - - return nil + return context, nil } func lookupAndAddString(unit *parser.UnitFile, group, key, flag string, podman *PodmanCmdline) { @@ -1736,20 +1952,22 @@ func lookupAndAddBoolean(unit *parser.UnitFile, group, key, flag string, podman } func handleImageSource(quadletImageName string, serviceUnitFile *parser.UnitFile, names map[string]string) (string, error) { - if strings.HasSuffix(quadletImageName, ".image") { - // since there is no default name conversion, the actual image name must exist in the names map - imageName, ok := names[quadletImageName] - if !ok { - return "", fmt.Errorf("requested Quadlet image %s was not found", imageName) + for _, suffix := range []string{".build", ".image"} { + if strings.HasSuffix(quadletImageName, suffix) { + // since there is no default name conversion, the actual image name must exist in the names map + imageName, ok := names[quadletImageName] + if !ok { + return "", fmt.Errorf("requested Quadlet image %s was not found", quadletImageName) + } + + // the systemd unit name is $name-$suffix.service + imageServiceName := replaceExtension(quadletImageName, ".service", "", fmt.Sprintf("-%s", suffix[1:])) + + serviceUnitFile.Add(UnitGroup, "Requires", imageServiceName) + serviceUnitFile.Add(UnitGroup, "After", imageServiceName) + + quadletImageName = imageName } - - // the systemd unit name is $name-image.service - imageServiceName := replaceExtension(quadletImageName, ".service", "", "-image") - - serviceUnitFile.Add(UnitGroup, "Requires", imageServiceName) - serviceUnitFile.Add(UnitGroup, "After", imageServiceName) - - quadletImageName = imageName } return quadletImageName, nil diff --git a/test/e2e/quadlet/annotation.build b/test/e2e/quadlet/annotation.build new file mode 100644 index 0000000000..6f0fd9281a --- /dev/null +++ b/test/e2e/quadlet/annotation.build @@ -0,0 +1,14 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args "--annotation" "org.foo.Arg0=arg0" +## assert-podman-args "--annotation" "org.foo.Arg1=arg1" +## assert-podman-args "--annotation" "org.foo.Arg2=arg 2" +## assert-podman-args "--annotation" "org.foo.Arg3=arg3" +## assert-podman-args --tag=localhost/imagename + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Annotation=org.foo.Arg1=arg1 "org.foo.Arg2=arg 2" \ + org.foo.Arg3=arg3 + +Annotation=org.foo.Arg0=arg0 diff --git a/test/e2e/quadlet/arch.build b/test/e2e/quadlet/arch.build new file mode 100644 index 0000000000..38c597db89 --- /dev/null +++ b/test/e2e/quadlet/arch.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --arch=aarch64 + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Arch=aarch64 diff --git a/test/e2e/quadlet/authfile.build b/test/e2e/quadlet/authfile.build new file mode 100644 index 0000000000..42d74118d8 --- /dev/null +++ b/test/e2e/quadlet/authfile.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --authfile=/etc/certs/auth.json + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +AuthFile=/etc/certs/auth.json diff --git a/test/e2e/quadlet/basic.build b/test/e2e/quadlet/basic.build new file mode 100644 index 0000000000..878d2787c8 --- /dev/null +++ b/test/e2e/quadlet/basic.build @@ -0,0 +1,13 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-key-is "Unit" "After" "network-online.target" +## assert-key-is "Unit" "Wants" "network-online.target" +## assert-key-is "Unit" "RequiresMountsFor" "%t/containers" +## assert-key-is-regex "Service" "WorkingDirectory" "/.*/podman-e2e-.*/subtest-.*/quadlet" +## assert-key-is "Service" "Type" "oneshot" +## assert-key-is "Service" "RemainAfterExit" "yes" +## assert-key-is "Service" "SyslogIdentifier" "%N" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit diff --git a/test/e2e/quadlet/build-not-found.quadlet.volume b/test/e2e/quadlet/build-not-found.quadlet.volume new file mode 100644 index 0000000000..fdbd6d6f04 --- /dev/null +++ b/test/e2e/quadlet/build-not-found.quadlet.volume @@ -0,0 +1,6 @@ +## assert-failed +## assert-stderr-contains "requested Quadlet image not-found.build was not found" + +[Volume] +Driver=image +Image=not-found.build diff --git a/test/e2e/quadlet/build.quadlet.volume b/test/e2e/quadlet/build.quadlet.volume new file mode 100644 index 0000000000..177e98981e --- /dev/null +++ b/test/e2e/quadlet/build.quadlet.volume @@ -0,0 +1,8 @@ +## assert-podman-args --driver=image +## assert-podman-args --opt image=localhost/imagename +## assert-key-is "Unit" "Requires" "basic-build.service" +## assert-key-is "Unit" "After" "basic-build.service" + +[Volume] +Driver=image +Image=basic.build diff --git a/test/e2e/quadlet/containersconfmodule.build b/test/e2e/quadlet/containersconfmodule.build new file mode 100644 index 0000000000..ac929ccd00 --- /dev/null +++ b/test/e2e/quadlet/containersconfmodule.build @@ -0,0 +1,8 @@ +## assert-podman-global-args "build" "--module=/etc/container/1.conf" +## assert-podman-global-args "build" "--module=/etc/container/2.conf" + +[Build] +ImageTag=image:latest +SetWorkingDirectory=unit +ContainersConfModule=/etc/container/1.conf +ContainersConfModule=/etc/container/2.conf diff --git a/test/e2e/quadlet/dns-options.build b/test/e2e/quadlet/dns-options.build new file mode 100644 index 0000000000..5bdfb202df --- /dev/null +++ b/test/e2e/quadlet/dns-options.build @@ -0,0 +1,10 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args "--dns-option=ndots:1" +## assert-podman-args "--dns-option=color:blue" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +DNSOption=ndots:1 +DNSOption=color:blue diff --git a/test/e2e/quadlet/dns-search.build b/test/e2e/quadlet/dns-search.build new file mode 100644 index 0000000000..f2340bacd0 --- /dev/null +++ b/test/e2e/quadlet/dns-search.build @@ -0,0 +1,10 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args "--dns-search=foo.com" +## assert-podman-args "--dns-search=bar.com" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +DNSSearch=foo.com +DNSSearch=bar.com diff --git a/test/e2e/quadlet/dns.build b/test/e2e/quadlet/dns.build new file mode 100644 index 0000000000..aaf96ce171 --- /dev/null +++ b/test/e2e/quadlet/dns.build @@ -0,0 +1,10 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args "--dns=8.7.7.7" +## assert-podman-args "--dns=8.8.8.8" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +DNS=8.7.7.7 +DNS=8.8.8.8 diff --git a/test/e2e/quadlet/env.build b/test/e2e/quadlet/env.build new file mode 100644 index 0000000000..55a09b2397 --- /dev/null +++ b/test/e2e/quadlet/env.build @@ -0,0 +1,14 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --env "FOO1=foo1" +## assert-podman-args --env "FOO2=foo2 " +## assert-podman-args --env "FOO3=foo3" +## assert-podman-args --env "REPLACE=replaced" +## assert-podman-args --env "FOO4=foo\\nfoo" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Environment=FOO1=foo1 "FOO2=foo2 " \ + FOO3=foo3 REPLACE=replace +Environment=REPLACE=replaced 'FOO4=foo\nfoo' diff --git a/test/e2e/quadlet/file-abs.build b/test/e2e/quadlet/file-abs.build new file mode 100644 index 0000000000..89c2877a6a --- /dev/null +++ b/test/e2e/quadlet/file-abs.build @@ -0,0 +1,5 @@ +## assert-podman-final-args --file=/etc/containers/systemd/Containerfile + +[Build] +File=/etc/containers/systemd/Containerfile +ImageTag=localhost/imagename diff --git a/test/e2e/quadlet/file-https.build b/test/e2e/quadlet/file-https.build new file mode 100644 index 0000000000..9cb2971561 --- /dev/null +++ b/test/e2e/quadlet/file-https.build @@ -0,0 +1,6 @@ +## assert-podman-args --tag=localhost/podman-hello +## assert-podman-args --file=https://raw.githubusercontent.com/containers/PodmanHello/main/Containerfile + +[Build] +File=https://raw.githubusercontent.com/containers/PodmanHello/main/Containerfile +ImageTag=localhost/podman-hello diff --git a/test/e2e/quadlet/file-rel-no-wd.build b/test/e2e/quadlet/file-rel-no-wd.build new file mode 100644 index 0000000000..a1776d51bf --- /dev/null +++ b/test/e2e/quadlet/file-rel-no-wd.build @@ -0,0 +1,6 @@ +## assert-failed +## assert-stderr-contains "relative path in File key requires SetWorkingDirectory key to be set" + +[Build] +ImageTag=localhost/imagename +File=Containerfile diff --git a/test/e2e/quadlet/file-rel.build b/test/e2e/quadlet/file-rel.build new file mode 100644 index 0000000000..3cc60e500e --- /dev/null +++ b/test/e2e/quadlet/file-rel.build @@ -0,0 +1,7 @@ +## assert-podman-final-args . +## assert-podman-args-regex "--file=Containerfile" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=. +File=Containerfile diff --git a/test/e2e/quadlet/force-rm.build b/test/e2e/quadlet/force-rm.build new file mode 100644 index 0000000000..7a17e60f26 --- /dev/null +++ b/test/e2e/quadlet/force-rm.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --force-rm=false + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +ForceRM=no diff --git a/test/e2e/quadlet/globalargs.build b/test/e2e/quadlet/globalargs.build new file mode 100644 index 0000000000..01c13bed2c --- /dev/null +++ b/test/e2e/quadlet/globalargs.build @@ -0,0 +1,9 @@ +## assert-podman-global-args "build" "--identity=path=/etc/identity" +## assert-podman-global-args "build" "--syslog" +## assert-podman-global-args "build" "--log-level=debug" + +[Build] +ImageTag=image:latest +SetWorkingDirectory=unit +GlobalArgs=--identity=path=/etc/identity +GlobalArgs=--syslog --log-level=debug diff --git a/test/e2e/quadlet/group-add.build b/test/e2e/quadlet/group-add.build new file mode 100644 index 0000000000..f2a54a50f1 --- /dev/null +++ b/test/e2e/quadlet/group-add.build @@ -0,0 +1,8 @@ +## assert-podman-args "--group-add=keep-groups" +## assert-podman-args "--group-add=users" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +GroupAdd=keep-groups +GroupAdd=users diff --git a/test/e2e/quadlet/image-not-found.quadlet.volume b/test/e2e/quadlet/image-not-found.quadlet.volume new file mode 100644 index 0000000000..bbb9851a2d --- /dev/null +++ b/test/e2e/quadlet/image-not-found.quadlet.volume @@ -0,0 +1,6 @@ +## assert-failed +## assert-stderr-contains "requested Quadlet image not-found.image was not found" + +[Volume] +Driver=image +Image=not-found.image diff --git a/test/e2e/quadlet/image.quadlet.volume b/test/e2e/quadlet/image.quadlet.volume new file mode 100644 index 0000000000..8a64b00768 --- /dev/null +++ b/test/e2e/quadlet/image.quadlet.volume @@ -0,0 +1,8 @@ +## assert-podman-args --driver=image +## assert-podman-args --opt image=localhost/imagename +## assert-key-is "Unit" "Requires" "basic-image.service" +## assert-key-is "Unit" "After" "basic-image.service" + +[Volume] +Driver=image +Image=basic.image diff --git a/test/e2e/quadlet/label.build b/test/e2e/quadlet/label.build new file mode 100644 index 0000000000..688c2b2455 --- /dev/null +++ b/test/e2e/quadlet/label.build @@ -0,0 +1,14 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args "--label" "org.foo.Arg0=arg0" +## assert-podman-args "--label" "org.foo.Arg1=arg1" +## assert-podman-args "--label" "org.foo.Arg2=arg 2" +## assert-podman-args "--label" "org.foo.Arg3=arg3" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Label=org.foo.Arg1=arg1 "org.foo.Arg2=arg 2" \ + org.foo.Arg3=arg3 + +Label=org.foo.Arg0=arg0 diff --git a/test/e2e/quadlet/neither-workingdirectory-nor-file.build b/test/e2e/quadlet/neither-workingdirectory-nor-file.build new file mode 100644 index 0000000000..4dab5e7f31 --- /dev/null +++ b/test/e2e/quadlet/neither-workingdirectory-nor-file.build @@ -0,0 +1,5 @@ +## assert-failed +## assert-stderr-contains "neither SetWorkingDirectory, nor File key specified" + +[Build] +ImageTag=localhost/imagename diff --git a/test/e2e/quadlet/network.build b/test/e2e/quadlet/network.build new file mode 100644 index 0000000000..cc9fddd26c --- /dev/null +++ b/test/e2e/quadlet/network.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args "--network=host" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Network=host diff --git a/test/e2e/quadlet/network.quadlet.build b/test/e2e/quadlet/network.quadlet.build new file mode 100644 index 0000000000..6423aa090d --- /dev/null +++ b/test/e2e/quadlet/network.quadlet.build @@ -0,0 +1,8 @@ +## assert-podman-args "--network=systemd-basic" +## assert-key-is "Unit" "Requires" "basic-network.service" +## assert-key-is "Unit" "After" "network-online.target" "basic-network.service" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Network=basic.network diff --git a/test/e2e/quadlet/no-imagetag.build b/test/e2e/quadlet/no-imagetag.build new file mode 100644 index 0000000000..6f19b80764 --- /dev/null +++ b/test/e2e/quadlet/no-imagetag.build @@ -0,0 +1,5 @@ +## assert-failed +## assert-stderr-contains "no ImageTag key specified" + +[Build] +SetWorkingDirectory=unit diff --git a/test/e2e/quadlet/podmanargs.build b/test/e2e/quadlet/podmanargs.build new file mode 100644 index 0000000000..f64ce76149 --- /dev/null +++ b/test/e2e/quadlet/podmanargs.build @@ -0,0 +1,14 @@ +## assert-podman-args "--foo" +## assert-podman-args "--bar" +## assert-podman-args "--also" +## assert-podman-args "--with-key=value" +## assert-podman-args "--with-space" "yes" + +[Build] +ImageTag=image:latest +SetWorkingDirectory=unit +PodmanArgs="--foo" \ + --bar +PodmanArgs=--also +PodmanArgs=--with-key=value +PodmanArgs=--with-space yes diff --git a/test/e2e/quadlet/pull.build b/test/e2e/quadlet/pull.build new file mode 100644 index 0000000000..7aad2ca20a --- /dev/null +++ b/test/e2e/quadlet/pull.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --pull=never + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Pull=never diff --git a/test/e2e/quadlet/secrets.build b/test/e2e/quadlet/secrets.build new file mode 100644 index 0000000000..8d1df9cb23 --- /dev/null +++ b/test/e2e/quadlet/secrets.build @@ -0,0 +1,8 @@ +## assert-podman-args "--secret" "mysecret" +## assert-podman-args "--secret" "id=mysecret,src=mysecret.txt" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Secret=mysecret +Secret=id=mysecret,src=mysecret.txt diff --git a/test/e2e/quadlet/setworkingdirectory-is-abs.build b/test/e2e/quadlet/setworkingdirectory-is-abs.build new file mode 100644 index 0000000000..e408e3b291 --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-abs.build @@ -0,0 +1,5 @@ +## assert-podman-final-args /etc/containers/systemd + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=/etc/containers/systemd diff --git a/test/e2e/quadlet/setworkingdirectory-is-archive.build b/test/e2e/quadlet/setworkingdirectory-is-archive.build new file mode 100644 index 0000000000..7e40fe4c60 --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-archive.build @@ -0,0 +1,8 @@ +## assert-podman-final-args https://github.com/containers/PodmanHello/archive/refs/heads/main.tar.gz +## assert-podman-args --tag=localhost/podman-hello-archive +## assert-podman-args --file=PodmanHello-main/Containerfile + +[Build] +ImageTag=localhost/podman-hello-archive +File=PodmanHello-main/Containerfile +SetWorkingDirectory=https://github.com/containers/PodmanHello/archive/refs/heads/main.tar.gz diff --git a/test/e2e/quadlet/setworkingdirectory-is-file-abs.build b/test/e2e/quadlet/setworkingdirectory-is-file-abs.build new file mode 100644 index 0000000000..0ebf1918b1 --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-file-abs.build @@ -0,0 +1,7 @@ +## assert-podman-args --file=/etc/containers/systemd/Containerfile +## assert-key-is "Service" "WorkingDirectory" "/etc/containers/systemd" + +[Build] +File=/etc/containers/systemd/Containerfile +ImageTag=localhost/imagename +SetWorkingDirectory=file diff --git a/test/e2e/quadlet/setworkingdirectory-is-file-rel.build b/test/e2e/quadlet/setworkingdirectory-is-file-rel.build new file mode 100644 index 0000000000..472d84ffca --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-file-rel.build @@ -0,0 +1,7 @@ +## assert-podman-args --file=Containerfile +## assert-key-is-regex "Service" "WorkingDirectory" "/.*/podman-e2e-.*/subtest-.*/quadlet" + +[Build] +ImageTag=localhost/imagename +File=Containerfile +SetWorkingDirectory=file diff --git a/test/e2e/quadlet/setworkingdirectory-is-git.build b/test/e2e/quadlet/setworkingdirectory-is-git.build new file mode 100644 index 0000000000..f85718f608 --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-git.build @@ -0,0 +1,6 @@ +## assert-podman-final-args git://git@git.sr.ht/~emersion/sr.ht-container-compose +## assert-podman-args --tag=localhost/podman-hello + +[Build] +ImageTag=localhost/podman-hello +SetWorkingDirectory=git://git@git.sr.ht/~emersion/sr.ht-container-compose diff --git a/test/e2e/quadlet/setworkingdirectory-is-github.build b/test/e2e/quadlet/setworkingdirectory-is-github.build new file mode 100644 index 0000000000..484bc21cd1 --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-github.build @@ -0,0 +1,6 @@ +## assert-podman-final-args github.com/containers/PodmanHello.git +## assert-podman-args --tag=localhost/podman-hello + +[Build] +ImageTag=localhost/podman-hello +SetWorkingDirectory=github.com/containers/PodmanHello.git diff --git a/test/e2e/quadlet/setworkingdirectory-is-https-git.build b/test/e2e/quadlet/setworkingdirectory-is-https-git.build new file mode 100644 index 0000000000..ec03f8dd2f --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-https-git.build @@ -0,0 +1,6 @@ +## assert-podman-final-args https://github.com/containers/PodmanHello.git +## assert-podman-args --tag=localhost/podman-hello + +[Build] +ImageTag=localhost/podman-hello +SetWorkingDirectory=https://github.com/containers/PodmanHello.git diff --git a/test/e2e/quadlet/setworkingdirectory-is-rel.build b/test/e2e/quadlet/setworkingdirectory-is-rel.build new file mode 100644 index 0000000000..5df3b7ee32 --- /dev/null +++ b/test/e2e/quadlet/setworkingdirectory-is-rel.build @@ -0,0 +1,6 @@ +## assert-podman-final-args . +## assert-key-is-regex "Service" "WorkingDirectory" "/.*/podman-e2e-.*/subtest-.*/quadlet" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=. diff --git a/test/e2e/quadlet/target.build b/test/e2e/quadlet/target.build new file mode 100644 index 0000000000..6af72c1c82 --- /dev/null +++ b/test/e2e/quadlet/target.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --target=my-app + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Target=my-app diff --git a/test/e2e/quadlet/tls-verify.build b/test/e2e/quadlet/tls-verify.build new file mode 100644 index 0000000000..0970bbab7a --- /dev/null +++ b/test/e2e/quadlet/tls-verify.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --tls-verify=false + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +TLSVerify=no diff --git a/test/e2e/quadlet/variant.build b/test/e2e/quadlet/variant.build new file mode 100644 index 0000000000..ffc03b1ac7 --- /dev/null +++ b/test/e2e/quadlet/variant.build @@ -0,0 +1,8 @@ +## assert-podman-final-args-regex /.*/podman-e2e-.*/subtest-.*/quadlet +## assert-podman-args --tag=localhost/imagename +## assert-podman-args --variant=arm/v7 + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Variant=arm/v7 diff --git a/test/e2e/quadlet/volume.build b/test/e2e/quadlet/volume.build new file mode 100644 index 0000000000..f63ac66fc6 --- /dev/null +++ b/test/e2e/quadlet/volume.build @@ -0,0 +1,17 @@ +## assert-podman-args -v /host/dir:/container/volume +## assert-podman-args -v /host/dir2:/container/volume2:Z +## assert-podman-args-regex -v .*/podman-e2e-.*/subtest-.*/quadlet/host/dir3:/container/volume3 +## assert-podman-args -v named:/container/named +## assert-podman-args -v systemd-quadlet:/container/quadlet +## assert-podman-args -v %h/container:/container/volume4 + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Volume=/host/dir:/container/volume +Volume=/host/dir2:/container/volume2:Z +Volume=./host/dir3:/container/volume3 +Volume=/container/empty +Volume=named:/container/named +Volume=quadlet.volume:/container/quadlet +Volume=%h/container:/container/volume4 diff --git a/test/e2e/quadlet/volume.quadlet.build b/test/e2e/quadlet/volume.quadlet.build new file mode 100644 index 0000000000..6cb57bda13 --- /dev/null +++ b/test/e2e/quadlet/volume.quadlet.build @@ -0,0 +1,8 @@ +## assert-podman-args "-v" "systemd-basic:/volume/basic" +## assert-key-is "Unit" "Requires" "basic-volume.service" +## assert-key-is "Unit" "After" "network-online.target" "basic-volume.service" + +[Build] +ImageTag=localhost/imagename +SetWorkingDirectory=unit +Volume=basic.volume:/volume/basic diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index db63c4f1e7..4280955847 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -50,6 +50,8 @@ func loadQuadletTestcase(path string) *quadletTestcase { service += "-network" case ".image": service += "-image" + case ".build": + service += "-build" case ".pod": service += "-pod" } @@ -606,6 +608,44 @@ var _ = Describe("quadlet system generator", func() { err error generatedDir string quadletDir string + + runQuadletTestCase = func(fileName string, exitCode int, errString string) { + 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()) + + // 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 + session := podmanTest.Quadlet([]string{"--user", "--no-kmsg-log", generatedDir}, 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) + } ) BeforeEach(func() { @@ -747,43 +787,7 @@ BOGUS=foo }) DescribeTable("Running quadlet test case", - func(fileName string, exitCode int, errString string) { - 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()) - - // 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 - session := podmanTest.Quadlet([]string{"--user", "--no-kmsg-log", generatedDir}, 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, Entry("Basic container", "basic.container", 0, ""), Entry("annotation.container", "annotation.container", 0, ""), Entry("autoupdate.container", "autoupdate.container", 0, ""), @@ -880,6 +884,8 @@ BOGUS=foo 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"), Entry("Volume - global args", "globalargs.volume", 0, ""), Entry("Volume - Containers Conf Modules", "containersconfmodule.volume", 0, ""), + Entry("Volume - Quadlet image (.build) not found", "build-not-found.quadlet.volume", 1, "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", 1, "converting \"image-not-found.quadlet.volume\": requested Quadlet image not-found.image was not found"), Entry("Absolute Path", "absolute.path.kube", 0, ""), Entry("Basic kube", "basic.kube", 0, ""), @@ -944,6 +950,44 @@ BOGUS=foo Entry("Image - Containers Conf Modules", "containersconfmodule.image", 0, ""), Entry("Image - Unit After Override", "unit-after-override.image", 0, ""), + Entry("Build - Basic", "basic.build", 0, ""), + Entry("Build - Annotation Key", "annotation.build", 0, ""), + Entry("Build - Arch Key", "arch.build", 0, ""), + Entry("Build - AuthFile Key", "authfile.build", 0, ""), + Entry("Build - DNS Key", "dns.build", 0, ""), + Entry("Build - DNSOptions Key", "dns-options.build", 0, ""), + Entry("Build - DNSSearch Key", "dns-search.build", 0, ""), + Entry("Build - Environment Key", "env.build", 0, ""), + Entry("Build - File Key absolute", "file-abs.build", 0, ""), + Entry("Build - File Key relative", "file-rel.build", 0, ""), + Entry("Build - File Key relative no WD", "file-rel-no-wd.build", 1, "converting \"file-rel-no-wd.build\": relative path in File key requires SetWorkingDirectory key to be set"), + Entry("Build - File Key HTTP(S) URL", "file-https.build", 0, ""), + Entry("Build - ForceRM Key", "force-rm.build", 0, ""), + Entry("Build - GlobalArgs", "globalargs.build", 0, ""), + Entry("Build - GroupAdd Key", "group-add.build", 0, ""), + Entry("Build - Containers Conf Modules", "containersconfmodule.build", 0, ""), + Entry("Build - Label Key", "label.build", 0, ""), + Entry("Build - Neither WorkingDirectory nor File Key", "neither-workingdirectory-nor-file.build", 1, "converting \"neither-workingdirectory-nor-file.build\": neither SetWorkingDirectory, nor File key specified"), + Entry("Build - Network Key host", "network.build", 0, ""), + Entry("Build - Network Key quadlet", "network.quadlet.build", 0, ""), + Entry("Build - No ImageTag Key", "no-imagetag.build", 1, "converting \"no-imagetag.build\": no ImageTag key specified"), + Entry("Build - PodmanArgs", "podmanargs.build", 0, ""), + Entry("Build - Pull Key", "pull.build", 0, ""), + Entry("Build - Secrets", "secrets.build", 0, ""), + Entry("Build - SetWorkingDirectory is absolute path", "setworkingdirectory-is-abs.build", 0, ""), + Entry("Build - SetWorkingDirectory is absolute File= path", "setworkingdirectory-is-file-abs.build", 0, ""), + Entry("Build - SetWorkingDirectory is relative path", "setworkingdirectory-is-rel.build", 0, ""), + Entry("Build - SetWorkingDirectory is relative File= path", "setworkingdirectory-is-file-rel.build", 0, ""), + Entry("Build - SetWorkingDirectory is https://.git URL", "setworkingdirectory-is-https-git.build", 0, ""), + Entry("Build - SetWorkingDirectory is git:// URL", "setworkingdirectory-is-git.build", 0, ""), + Entry("Build - SetWorkingDirectory is github.com URL", "setworkingdirectory-is-github.build", 0, ""), + Entry("Build - SetWorkingDirectory is archive URL", "setworkingdirectory-is-archive.build", 0, ""), + Entry("Build - Target Key", "target.build", 0, ""), + Entry("Build - TLSVerify Key", "tls-verify.build", 0, ""), + Entry("Build - Variant Key", "variant.build", 0, ""), + Entry("Build - Volume Key", "volume.build", 0, ""), + Entry("Build - Volume Key quadlet", "volume.quadlet.build", 0, ""), + Entry("basic.pod", "basic.pod", 0, ""), Entry("name.pod", "name.pod", 0, ""), Entry("network.pod", "network.pod", 0, ""), @@ -952,4 +996,19 @@ BOGUS=foo Entry("volume.pod", "volume.pod", 0, ""), ) + DescribeTable("Running quadlet test case with dependencies", + func(fileName string, exitCode int, errString 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()) + } + + runQuadletTestCase(fileName, exitCode, errString) + }, + Entry("Volume - Quadlet image (.build)", "build.quadlet.volume", 0, "", []string{"basic.build"}), + Entry("Volume - Quadlet image (.image)", "image.quadlet.volume", 0, "", []string{"basic.image"}), + ) + })