diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index a261ab9d28..c5e0e77f64 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -45,12 +45,15 @@ var ( ) var ( - void struct{} - supportedExtensions = map[string]struct{}{ - ".container": void, - ".volume": void, - ".kube": void, - ".network": void, + void struct{} + // Key: Extension + // Value: Processing order for resource naming dependecies + supportedExtensions = map[string]int{ + ".container": 3, + ".volume": 2, + ".kube": 3, + ".network": 2, + ".image": 1, } ) @@ -364,13 +367,16 @@ func isUnambiguousName(imageName string) bool { // // We implement a simple version of this from scratch here to avoid // a huge dependency in the generator just for a warning. -func warnIfAmbiguousName(container *parser.UnitFile) { - imageName, ok := container.Lookup(quadlet.ContainerGroup, quadlet.KeyImage) +func warnIfAmbiguousName(unit *parser.UnitFile, group string) { + imageName, ok := unit.Lookup(group, quadlet.KeyImage) if !ok { return } + if strings.HasSuffix(imageName, ".image") { + return + } if !isUnambiguousName(imageName) { - Logf("Warning: %s specifies the image \"%s\" 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.", container.Filename, imageName) + Logf("Warning: %s specifies the image \"%s\" 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.", unit.Filename, imageName) } } @@ -452,8 +458,15 @@ func process() error { // Sort unit files according to potential inter-dependencies, with Volume and Network units // taking precedence over all others. sort.Slice(units, func(i, j int) bool { - name := units[i].Filename - return strings.HasSuffix(name, ".volume") || strings.HasSuffix(name, ".network") + getOrder := func(i int) int { + ext := filepath.Ext(units[i].Filename) + order, ok := supportedExtensions[ext] + if !ok { + return 0 + } + return order + } + return getOrder(i) < getOrder(j) }) // A map of network/volume unit file-names, against their calculated names, as needed by Podman. @@ -466,14 +479,18 @@ func process() error { switch { case strings.HasSuffix(unit.Filename, ".container"): - warnIfAmbiguousName(unit) + warnIfAmbiguousName(unit, quadlet.ContainerGroup) service, err = quadlet.ConvertContainer(unit, resourceNames, isUserFlag) case strings.HasSuffix(unit.Filename, ".volume"): - service, name, err = quadlet.ConvertVolume(unit, unit.Filename) + warnIfAmbiguousName(unit, quadlet.VolumeGroup) + service, name, err = quadlet.ConvertVolume(unit, unit.Filename, resourceNames) case strings.HasSuffix(unit.Filename, ".kube"): service, err = quadlet.ConvertKube(unit, resourceNames, isUserFlag) case strings.HasSuffix(unit.Filename, ".network"): service, name, err = quadlet.ConvertNetwork(unit, unit.Filename) + case strings.HasSuffix(unit.Filename, ".image"): + warnIfAmbiguousName(unit, quadlet.ImageGroup) + service, name, err = quadlet.ConvertImage(unit) default: Logf("Unsupported file type %q", unit.Filename) continue diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 8d396e6bc1..ddbff23ca7 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -360,8 +360,13 @@ The image to run in the container. It is recommended to use a fully qualified image name rather than a short name, both for performance and robustness reasons. -The format of the name is the same as when passed to `podman run`, so it supports e.g., using -`:tag` or using digests guarantee a specific image version. +The format of the name is the same as when passed to `podman pull`. So, it supports using +`:tag` or digests to guarantee the specific image version. + +As a special case, if the `name` of the image ends with `.image`, Quadlet will use the image +pulled by the corresponding `.image` file, and the generated systemd service contains +a dependency on the `$name-image.service`. +Note that the corresponding `.image` file must exist. ### `IP=` @@ -626,7 +631,7 @@ This key may be used multiple times ### `ExitCodePropagation=` -Control how the main PID of the systemd service should exit. The following values are supported: +Control how the main PID of the systemd service should exit. The following values are supported: - `all`: exit non-zero if all containers have failed (i.e., exited non-zero) - `any`: exit non-zero if any container has failed - `none`: exit zero and ignore failed containers @@ -640,7 +645,7 @@ Equivalent to the Podman `--log-driver` option. ### `Mask=` -Specify the paths to mask separated by a colon. `Mask=/path/1:/path/2`. A masked path cannot be accessed inside the container. +Specify the paths to mask separated by a colon. `Mask=/path/1:/path/2`. A masked path cannot be accessed inside the container. ### `Network=` @@ -792,7 +797,7 @@ This is equivalent to the Podman `--ipam-driver` option ### `IPRange=` -Allocate container IP from a range. The range must be a either a complete subnet in CIDR notation or be +Allocate container IP from a range. The range must be a either a complete subnet in CIDR notation or be in the `-` syntax which allows for a more flexible range compared to the CIDR subnet. The ip-range option must be used with a subnet option. @@ -863,15 +868,17 @@ as Podman otherwise creates volumes with the default options. Valid options for `[Volume]` are listed below: -| **[Volume] options** | **podman volume create equivalent** | -|---------------------------|-------------------------------------| -| Device=tmpfs | --opt device=tmpfs | -| Copy=true | --opt copy | -| Group=192 | --opt group=192 | -| Label="foo=bar" | --label "foo=bar" | -| Options=XYZ | --opt XYZ | -| PodmanArgs=--driver=image | --driver=image | -| VolumeName=foo | podman volume create foo | +| **[Volume] options** | **podman volume create equivalent** | +|-------------------------------------|-------------------------------------------| +| Device=tmpfs | --opt device=tmpfs | +| Driver=image | --driver=image | +| Copy=true | --opt copy | +| Group=192 | --opt group=192 | +| Image=quay.io/centos/centos\:latest | --opt image=quay.io/centos/centos\:latest | +| Label="foo=bar" | --label "foo=bar" | +| Options=XYZ | --opt XYZ | +| PodmanArgs=--driver=image | --driver=image | +| VolumeName=foo | podman volume create foo | Supported keys in `[Volume]` section are: @@ -884,10 +891,30 @@ volume on the first run. The path of a device which is mounted for the volume. +### `Driver=` + +Specify the volume driver name. When set to `image`, the `Image` key must also be set. + +This is equivalent to the Podman `--driver` option. + ### `Group=` The host (numeric) GID, or group name to use as the group for the volume +### `Image=` + +Specifies the image the volume is based on when `Driver` is set to the `image`. +It is recommended to use a fully qualified image name rather than a short name, both for +performance and robustness reasons. + +The format of the name is the same as when passed to `podman pull`. So, it supports using +`:tag` or digests to guarantee the specific image version. + +As a special case, if the `name` of the image ends with `.image`, Quadlet will use the image +pulled by the corresponding `.image` file, and the generated systemd service contains +a dependency on the `$name-image.service`. +Note that the corresponding `.image` file must exist. + ### `Label=` Set one or more OCI labels on the volume. The format is a list of @@ -926,6 +953,107 @@ 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. +## Image units [Image] + +Image files are named with a `.image` extension and contain a section `[Image]` describing the +container image pull command. The generated service is a one-time command that ensures that the image +exists on the host, pulling it if needed. + +Using image units allows containers and volumes to depend on images being automatically pulled. This is +particularly interesting when using special options to control image pulls. + +Valid options for `[Image]` are listed below: + +| **[Image] options** | **podman image pull equivalent** | +|-------------------------------------|-------------------------------------------------| +| AllTags=true | --all-tags | +| Arch=aarch64 | --arch=aarch64 | +| AuthFile=/etc/registry/auth\.json | --authfile=/etc/registry/auth\.json | +| CertDir=/etc/registery/certs | --cert-dir=/etc/registery/certs | +| Creds=myname\:mypassword | --creds=myname\:mypassword | +| DecryptionKey=/etc/registery\.key | --decryption-key=/etc/registery\.key | +| Image=quay.io/centos/centos\:latest | podman image pull quay.io/centos/centos\:latest | +| OS=windows | --os=windows | +| PodmanArgs=--os=linux | --os=linux | +| TLSVerify=false | --tls-verify=false | +| Variant=arm/v7 | --variant=arm/v7 | + +### `AllTags=` + +All tagged images in the repository are pulled. + +This is equivalent to the Podman `--all-tags` option. + +### `Arch=` + +Override the architecture, defaults to hosts, of the image to be pulled. + +This is equivalent to the Podman `--arch` option. + +### `AuthFile=` + +Path of the authentication file. + +This is equivalent to the Podman `--authfile` option. + +### `CertDir=` + +Use certificates at path (*.crt, *.cert, *.key) to connect to the registry. + +This is equivalent to the Podman `--cert-dir` option. + +### `Creds=` + +The `[username[:password]]` to use to authenticate with the registry, if required. + +This is equivalent to the Podman `--creds` option. + +### `DecryptionKey=` + +The `[key[:passphrase]]` to be used for decryption of images. + +This is equivalent to the Podman `--decryption-key` option. + +### `Image=` + +The image to pull. +It is recommended to use a fully qualified image name rather than a short name, both for +performance and robustness reasons. + +The format of the name is the same as when passed to `podman pull`. So, it supports using +`:tag` or digests to guarantee the specific image version. + +### `OS=` + +Override the OS, defaults to hosts, of the image to be pulled. + +This is equivalent to the Podman `--os` option. + +### `PodmanArgs=` + +This key contains a list of arguments passed directly to the end of the `podman image pull` 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. + +### `TLSVerify=` + +Require HTTPS and verification of certificates when contacting registries. + +This is equivalent to the Podman `--tls-verify` option. + +### `Variant=` + +Override the default architecture variant of the container image. + +This is equivalent to the Podman `--variant` option. + ## EXAMPLES Example `test.container`: diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index 9642689e43..29fbe4283c 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -29,10 +29,12 @@ const ( ServiceGroup = "Service" UnitGroup = "Unit" VolumeGroup = "Volume" + ImageGroup = "Image" XContainerGroup = "X-Container" XKubeGroup = "X-Kube" XNetworkGroup = "X-Network" XVolumeGroup = "X-Volume" + XImageGroup = "X-Image" ) // Systemd Unit file keys @@ -44,8 +46,14 @@ const ( const ( KeyAddCapability = "AddCapability" KeyAddDevice = "AddDevice" + KeyAllTags = "AllTags" KeyAnnotation = "Annotation" + KeyArch = "Arch" + KeyAuthFile = "AuthFile" KeyAutoUpdate = "AutoUpdate" + KeyCertDir = "CertDir" + KeyCreds = "Creds" + KeyDecryptionKey = "DecryptionKey" KeyConfigMap = "ConfigMap" KeyContainerName = "ContainerName" KeyCopy = "Copy" @@ -53,6 +61,7 @@ const ( KeyDNS = "DNS" KeyDNSOption = "DNSOption" KeyDNSSearch = "DNSSearch" + KeyDriver = "Driver" KeyDropCapability = "DropCapability" KeyEnvironment = "Environment" KeyEnvironmentFile = "EnvironmentFile" @@ -94,6 +103,7 @@ const ( KeyNoNewPrivileges = "NoNewPrivileges" KeyNotify = "Notify" KeyOptions = "Options" + KeyOS = "OS" KeyPidsLimit = "PidsLimit" KeyPodmanArgs = "PodmanArgs" KeyPublishPort = "PublishPort" @@ -116,12 +126,14 @@ const ( KeyShmSize = "ShmSize" KeySysctl = "Sysctl" KeyTimezone = "Timezone" + KeyTLSVerify = "TLSVerify" KeyTmpfs = "Tmpfs" KeyType = "Type" KeyUlimit = "Ulimit" KeyUnmask = "Unmask" KeyUser = "User" KeyUserNS = "UserNS" + KeyVariant = "Variant" KeyVolatileTmp = "VolatileTmp" KeyVolume = "Volume" KeyVolumeName = "VolumeName" @@ -206,7 +218,9 @@ var ( supportedVolumeKeys = map[string]bool{ KeyCopy: true, KeyDevice: true, + KeyDriver: true, KeyGroup: true, + KeyImage: true, KeyLabel: true, KeyOptions: true, KeyPodmanArgs: true, @@ -249,6 +263,21 @@ var ( KeyUserNS: true, KeyYaml: true, } + + // Supported keys in "Image" group + supportedImageKeys = map[string]bool{ + KeyAllTags: true, + KeyArch: true, + KeyAuthFile: true, + KeyCertDir: true, + KeyCreds: true, + KeyDecryptionKey: true, + KeyImage: true, + KeyOS: true, + KeyPodmanArgs: true, + KeyTLSVerify: true, + KeyVariant: true, + } ) func replaceExtension(name string, extension string, extraPrefix string, extraSuffix string) string { @@ -349,6 +378,13 @@ func ConvertContainer(container *parser.UnitFile, names map[string]string, isUse return nil, fmt.Errorf("the Image And Rootfs keys conflict can not be specified together") } + if len(image) > 0 { + var err error + if image, err = handleImageSource(image, service, names); err != nil { + return nil, err + } + } + containerName, ok := container.Lookup(ContainerGroup, KeyContainerName) if !ok || len(containerName) == 0 { // By default, We want to name the container by the service name @@ -860,7 +896,7 @@ func ConvertNetwork(network *parser.UnitFile, name string) (*parser.UnitFile, st // The original Volume group is kept around as X-Volume. // Also returns the canonical volume name, either auto-generated or user-defined via the VolumeName // key-value. -func ConvertVolume(volume *parser.UnitFile, name string) (*parser.UnitFile, string, error) { +func ConvertVolume(volume *parser.UnitFile, name string, names map[string]string) (*parser.UnitFile, string, error) { service := volume.Dup() service.Filename = replaceExtension(volume.Filename, ".service", "", "-volume") @@ -884,60 +920,81 @@ func ConvertVolume(volume *parser.UnitFile, name string) (*parser.UnitFile, stri podman := NewPodmanCmdline("volume", "create", "--ignore") - var opts strings.Builder - opts.WriteString("o=") - - if volume.HasKey(VolumeGroup, "User") { - uid := volume.LookupUint32(VolumeGroup, "User", 0) - if opts.Len() > 2 { - opts.WriteString(",") - } - opts.WriteString(fmt.Sprintf("uid=%d", uid)) - } - - if volume.HasKey(VolumeGroup, "Group") { - gid := volume.LookupUint32(VolumeGroup, "Group", 0) - if opts.Len() > 2 { - opts.WriteString(",") - } - opts.WriteString(fmt.Sprintf("gid=%d", gid)) - } - - copy, ok := volume.LookupBoolean(VolumeGroup, KeyCopy) + driver, ok := volume.Lookup(VolumeGroup, KeyDriver) if ok { - if copy { - podman.add("--opt", "copy") - } else { - podman.add("--opt", "nocopy") + podman.addf("--driver=%s", driver) + } + + var opts strings.Builder + + if driver == "image" { + opts.WriteString("image=") + + imageName, ok := volume.Lookup(VolumeGroup, KeyImage) + if !ok { + return nil, "", fmt.Errorf("the key %s is mandatory when using the image driver", KeyImage) } - } - - devValid := false - - dev, ok := volume.Lookup(VolumeGroup, KeyDevice) - if ok && len(dev) != 0 { - podman.add("--opt", fmt.Sprintf("device=%s", dev)) - devValid = true - } - - devType, ok := volume.Lookup(VolumeGroup, KeyType) - if ok && len(devType) != 0 { - if devValid { - podman.add("--opt", fmt.Sprintf("type=%s", devType)) - } else { - return nil, "", fmt.Errorf("key Type can't be used without Device") + imageName, err := handleImageSource(imageName, service, names) + if err != nil { + return nil, "", err } - } - mountOpts, ok := volume.Lookup(VolumeGroup, KeyOptions) - if ok && len(mountOpts) != 0 { - if devValid { + opts.WriteString(imageName) + } else { + opts.WriteString("o=") + + if volume.HasKey(VolumeGroup, "User") { + uid := volume.LookupUint32(VolumeGroup, "User", 0) if opts.Len() > 2 { opts.WriteString(",") } - opts.WriteString(mountOpts) - } else { - return nil, "", fmt.Errorf("key Options can't be used without Device") + opts.WriteString(fmt.Sprintf("uid=%d", uid)) + } + + if volume.HasKey(VolumeGroup, "Group") { + gid := volume.LookupUint32(VolumeGroup, "Group", 0) + if opts.Len() > 2 { + opts.WriteString(",") + } + opts.WriteString(fmt.Sprintf("gid=%d", gid)) + } + + copy, ok := volume.LookupBoolean(VolumeGroup, KeyCopy) + if ok { + if copy { + podman.add("--opt", "copy") + } else { + podman.add("--opt", "nocopy") + } + } + + devValid := false + + dev, ok := volume.Lookup(VolumeGroup, KeyDevice) + if ok && len(dev) != 0 { + podman.add("--opt", fmt.Sprintf("device=%s", dev)) + devValid = true + } + + devType, ok := volume.Lookup(VolumeGroup, KeyType) + if ok && len(devType) != 0 { + if devValid { + podman.add("--opt", fmt.Sprintf("type=%s", devType)) + } else { + return nil, "", fmt.Errorf("key Type can't be used without Device") + } + } + + mountOpts, ok := volume.Lookup(VolumeGroup, KeyOptions) + if ok && len(mountOpts) != 0 { + if devValid { + if opts.Len() > 2 { + opts.WriteString(",") + } + opts.WriteString(mountOpts) + } else { + return nil, "", fmt.Errorf("key Options can't be used without Device") + } } } @@ -1082,6 +1139,70 @@ func ConvertKube(kube *parser.UnitFile, names map[string]string, isUser bool) (* return service, nil } +func ConvertImage(image *parser.UnitFile) (*parser.UnitFile, string, error) { + service := image.Dup() + service.Filename = replaceExtension(image.Filename, ".service", "", "-image") + + if image.Path != "" { + service.Add(UnitGroup, "SourcePath", image.Path) + } + + if err := checkForUnknownKeys(image, ImageGroup, supportedImageKeys); err != nil { + return nil, "", err + } + + imageName, ok := image.Lookup(ImageGroup, KeyImage) + if !ok || len(imageName) == 0 { + return nil, "", fmt.Errorf("no Image key specified") + } + + /* Rename old Network group to x-Network so that systemd ignores it */ + service.RenameGroup(ImageGroup, XImageGroup) + + // Need the containers filesystem mounted to start podman + service.Add(UnitGroup, "RequiresMountsFor", "%t/containers") + + podman := NewPodmanCmdline("image", "pull") + + stringKeys := map[string]string{ + KeyArch: "--arch", + KeyAuthFile: "--authfile", + KeyCertDir: "--cert-dir", + KeyCreds: "--creds", + KeyDecryptionKey: "--decryption-key", + KeyOS: "--os", + KeyVariant: "--variant", + } + + boolKeys := map[string]string{ + KeyAllTags: "--all-tags", + KeyTLSVerify: "--tls-verify", + } + + for key, flag := range stringKeys { + lookupAndAddString(image, ImageGroup, key, flag, podman) + } + + for key, flag := range boolKeys { + lookupAndAddBoolean(image, ImageGroup, key, flag, podman) + } + + handlePodmanArgs(image, ImageGroup, podman) + + podman.add(imageName) + + 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, imageName, nil +} + func handleUserRemap(unitFile *parser.UnitFile, groupName string, podman *PodmanCmdline, isUser, supportManual bool) error { // ignore Remap keys if UserNS is set if userns, ok := unitFile.Lookup(groupName, KeyUserNS); ok && len(userns) > 0 { @@ -1383,3 +1504,37 @@ func handleSetWorkingDirectory(kube, serviceUnitFile *parser.UnitFile) error { return nil } + +func lookupAndAddString(unit *parser.UnitFile, group, key, flag string, podman *PodmanCmdline) { + val, ok := unit.Lookup(group, key) + if ok && len(val) > 0 { + podman.addf("%s=%s", flag, val) + } +} + +func lookupAndAddBoolean(unit *parser.UnitFile, group, key, flag string, podman *PodmanCmdline) { + val, ok := unit.LookupBoolean(group, key) + if ok { + podman.addBool(flag, val) + } +} + +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) + } + + // 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/all-tags.image b/test/e2e/quadlet/all-tags.image new file mode 100644 index 0000000000..a6923b2b23 --- /dev/null +++ b/test/e2e/quadlet/all-tags.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --all-tags + +[Image] +Image=localhost/imagename +AllTags=yes diff --git a/test/e2e/quadlet/arch-os.image b/test/e2e/quadlet/arch-os.image new file mode 100644 index 0000000000..e2ce94297e --- /dev/null +++ b/test/e2e/quadlet/arch-os.image @@ -0,0 +1,8 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --os=windows +## assert-podman-args --arch=arm64 + +[Image] +Image=localhost/imagename +OS=windows +Arch=arm64 diff --git a/test/e2e/quadlet/arch.image b/test/e2e/quadlet/arch.image new file mode 100644 index 0000000000..ef657888be --- /dev/null +++ b/test/e2e/quadlet/arch.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --arch=aarch64 + +[Image] +Image=localhost/imagename +Arch=aarch64 diff --git a/test/e2e/quadlet/auth.image b/test/e2e/quadlet/auth.image new file mode 100644 index 0000000000..6c8d3ee393 --- /dev/null +++ b/test/e2e/quadlet/auth.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --authfile=/etc/certs/auth.json + +[Image] +Image=localhost/imagename +AuthFile=/etc/certs/auth.json diff --git a/test/e2e/quadlet/basic.image b/test/e2e/quadlet/basic.image new file mode 100644 index 0000000000..b0502a7a57 --- /dev/null +++ b/test/e2e/quadlet/basic.image @@ -0,0 +1,8 @@ +## assert-podman-final-args localhost/imagename +## assert-key-is "Unit" "RequiresMountsFor" "%t/containers" +## assert-key-is "Service" "Type" "oneshot" +## assert-key-is "Service" "RemainAfterExit" "yes" +## assert-key-is "Service" "SyslogIdentifier" "%N" + +[Image] +Image=localhost/imagename diff --git a/test/e2e/quadlet/certs.image b/test/e2e/quadlet/certs.image new file mode 100644 index 0000000000..397ed9bfdb --- /dev/null +++ b/test/e2e/quadlet/certs.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --cert-dir=/etc/certs/auth.json + +[Image] +Image=localhost/imagename +CertDir=/etc/certs/auth.json diff --git a/test/e2e/quadlet/creds.image b/test/e2e/quadlet/creds.image new file mode 100644 index 0000000000..7c1ec6c828 --- /dev/null +++ b/test/e2e/quadlet/creds.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --creds=myname:mypassword + +[Image] +Image=localhost/imagename +Creds=myname:mypassword diff --git a/test/e2e/quadlet/decrypt.image b/test/e2e/quadlet/decrypt.image new file mode 100644 index 0000000000..40c6bb20c4 --- /dev/null +++ b/test/e2e/quadlet/decrypt.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --decryption-key=/etc/keys/decrypt:passphrase + +[Image] +Image=localhost/imagename +DecryptionKey=/etc/keys/decrypt:passphrase diff --git a/test/e2e/quadlet/image-no-image.volume b/test/e2e/quadlet/image-no-image.volume new file mode 100644 index 0000000000..e0ccfa41eb --- /dev/null +++ b/test/e2e/quadlet/image-no-image.volume @@ -0,0 +1,5 @@ +## assert-failed +## assert-stderr-contains "the key Image is mandatory when using the image driver" + +[Volume] +Driver=image diff --git a/test/e2e/quadlet/image.volume b/test/e2e/quadlet/image.volume new file mode 100644 index 0000000000..2c22403b71 --- /dev/null +++ b/test/e2e/quadlet/image.volume @@ -0,0 +1,6 @@ +## assert-podman-args --driver=image +## assert-podman-args --opt image=localhost/imagename + +[Volume] +Driver=image +Image=localhost/imagename diff --git a/test/e2e/quadlet/no-image.image b/test/e2e/quadlet/no-image.image new file mode 100644 index 0000000000..4822b7a833 --- /dev/null +++ b/test/e2e/quadlet/no-image.image @@ -0,0 +1,4 @@ +## assert-failed +## assert-stderr-contains "no Image key specified" + +[Image] diff --git a/test/e2e/quadlet/os.image b/test/e2e/quadlet/os.image new file mode 100644 index 0000000000..1a15453106 --- /dev/null +++ b/test/e2e/quadlet/os.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --os=windows + +[Image] +Image=localhost/imagename +OS=windows diff --git a/test/e2e/quadlet/tls-verify.image b/test/e2e/quadlet/tls-verify.image new file mode 100644 index 0000000000..993554396e --- /dev/null +++ b/test/e2e/quadlet/tls-verify.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --tls-verify=false + +[Image] +Image=localhost/imagename +TLSVerify=no diff --git a/test/e2e/quadlet/variant.image b/test/e2e/quadlet/variant.image new file mode 100644 index 0000000000..08744aa42c --- /dev/null +++ b/test/e2e/quadlet/variant.image @@ -0,0 +1,6 @@ +## assert-podman-final-args localhost/imagename +## assert-podman-args --variant=arm/v5 + +[Image] +Image=localhost/imagename +Variant=arm/v5 diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index 46c020e286..ed39474939 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -35,6 +35,8 @@ func loadQuadletTestcase(path string) *quadletTestcase { service += "-volume" case ".network": service += "-network" + case ".image": + service += "-image" } service += ".service" @@ -635,6 +637,8 @@ BOGUS=foo Entry("name.volume", "name.volume", 0, ""), Entry("podmanargs.volume", "podmanargs.volume", 0, ""), Entry("uid.volume", "uid.volume", 0, ""), + Entry("image.volume", "image.volume", 0, ""), + 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("Absolute Path", "absolute.path.kube", 0, ""), Entry("Basic kube", "basic.kube", 0, ""), @@ -676,6 +680,19 @@ BOGUS=foo Entry("Network - Subnets", "subnets.network", 0, ""), Entry("Network - multiple subnet, gateway and range", "subnet-trio.multiple.network", 0, ""), Entry("Network - subnet, gateway and range", "subnet-trio.network", 0, ""), + + Entry("Image - Basic", "basic.image", 0, ""), + Entry("Image - No Image", "no-image.image", 1, "converting \"no-image.image\": no Image key specified"), + Entry("Image - Architecture", "arch.image", 0, ""), + Entry("Image - Auth File", "auth.image", 0, ""), + Entry("Image - Certificates", "certs.image", 0, ""), + Entry("Image - Credentials", "creds.image", 0, ""), + Entry("Image - Decryption Key", "decrypt.image", 0, ""), + Entry("Image - OS Key", "os.image", 0, ""), + Entry("Image - Variant Key", "variant.image", 0, ""), + Entry("Image - All Tags", "all-tags.image", 0, ""), + Entry("Image - TLS Verify", "tls-verify.image", 0, ""), + Entry("Image - Arch and OS", "arch-os.image", 0, ""), ) }) diff --git a/test/system/252-quadlet.bats b/test/system/252-quadlet.bats index 81b009071c..bf60670dee 100644 --- a/test/system/252-quadlet.bats +++ b/test/system/252-quadlet.bats @@ -4,6 +4,8 @@ # load helpers +load helpers.network +load helpers.registry load helpers.systemd UNIT_FILES=() @@ -1013,4 +1015,126 @@ EOF run_podman rmi $(pause_image) } +@test "quadlet - image files" { + registry=localhost:${PODMAN_LOGIN_REGISTRY_PORT} + image_on_local_registry=$registry/quadlet_image_test:$(random_string) + authfile=$PODMAN_TMPDIR/authfile.json + + # First, start the registry and populate the authfile that we can use for the test. + start_registry + run_podman login --authfile=$authfile \ + --tls-verify=false \ + --username ${PODMAN_LOGIN_USER} \ + --password ${PODMAN_LOGIN_PASS} \ + $registry + + run_podman image tag $IMAGE $image_on_local_registry + run_podman image push --tls-verify=false --authfile=$authfile $image_on_local_registry + + local image_for_test=$image_on_local_registry + + # Remove the local image to make sure it will be pulled again + run_podman image rm --ignore $image_for_test + + local quadlet_image_unit=image_test_$(random_string).image + local quadlet_image_file=$PODMAN_TMPDIR/$quadlet_image_unit + cat > $quadlet_image_file < $quadlet_volume_file < $quadlet_container_file <