]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
portable: add support for confext 29839/head
authorMaanya Goenka <maanyagoenka@microsoft.com>
Wed, 16 Aug 2023 18:43:06 +0000 (18:43 +0000)
committerLuca Boccassi <bluca@debian.org>
Fri, 3 Nov 2023 16:59:58 +0000 (16:59 +0000)
Support confexts for portable services

docs/PORTABLE_SERVICES.md
man/org.freedesktop.portable1.xml
man/portablectl.xml
src/portable/portable.c
src/portable/portable.h
src/portable/portablectl.c
test/test-functions
test/units/testsuite-29.sh

index 8d65c9002d0a361d990275eaf616a78009bd0b82..7f07f231dab7a92902aab25072eda5a3e29d79eb 100644 (file)
@@ -277,12 +277,14 @@ following must be also be observed:
 
 2. The upper extension images must contain an extension-release file in
    `/usr/lib/extension-release.d/`, with an `ID=` and `SYSEXT_LEVEL=`/`VERSION_ID=`
-   matching the base image.
+   matching the base image for sysexts, or `/etc/extension-release.d/`, with an
+   `ID=` and `CONFEXT_LEVEL=`/`VERSION_ID=` matching the base image for confexts.
 
 3. The base/OS image does not need to have any unit files.
 
-4. The upper extension images must contain at least one matching unit file
-   each, with the right name prefix and suffix (see above).
+4. The upper sysext images must contain at least one matching unit file each,
+   with the right name prefix and suffix (see above). Confext images do not have
+   to contain units.
 
 5. As with the base/OS image, each upper extension image must be a plain
    sub-directory, btrfs subvolume, or a raw disk image.
@@ -354,10 +356,10 @@ underscore (`_`) as separator. If only either one is found, it will be used by i
 The field will be named `PORTABLE_NAME_AND_VERSION=`.
 
 In case extensions are used, the same fields in the same order are, but prefixed by
-`SYSEXT_`, are parsed from each `extension-release` file, and are appended to the
-journal as log entries, using `PORTABLE_EXTENSION_NAME_AND_VERSION=` as the field
-name. The base layer's field will be named `PORTABLE_ROOT_NAME_AND_VERSION=` instead
-of `PORTABLE_NAME_AND_VERSION=` in this case.
+`SYSEXT_`/`CONFEXT_`, are parsed from each `extension-release` file, and are appended
+to the journal as log entries, using `PORTABLE_EXTENSION_NAME_AND_VERSION=` as the
+field name. The base layer's field will be named `PORTABLE_ROOT_NAME_AND_VERSION=`
+instead of `PORTABLE_NAME_AND_VERSION=` in this case.
 
 For example, a portable service `app0` using two extensions `app0.raw` and
 `app1.raw` (with `SYSEXT_ID=app`, and `SYSEXT_VERSION_ID=` `0` and `1` in their
index e5902a032894963a7ede32b29c265e39be4240fb..118d86d727a9e5ee8bcb3b49357370ed5d054190 100644 (file)
@@ -309,14 +309,14 @@ node /org/freedesktop/portable1 {
       <function>ReattachImageWithExtensions()</function> methods take in options as flags instead of
       booleans to allow for extendability. <varname>SD_SYSTEMD_PORTABLE_FORCE_ATTACH</varname> will cause
       safety checks that ensure the units are not running while the new image is attached or detached
-      to be skipped. <varname>SD_SYSTEMD_PORTABLE_FORCE_SYSEXT</varname> will cause the check that the
+      to be skipped. <varname>SD_SYSTEMD_PORTABLE_FORCE_EXTENSION</varname> will cause the check that the
       <filename>extension-release.<replaceable>NAME</replaceable></filename> file in the extension image
       matches the image name to be skipped. They are defined as follows:</para>
 
       <programlisting>
-#define SD_SYSTEMD_PORTABLE_RUNTIME         (UINT64_C(1) &lt;&lt; 0)
-#define SD_SYSTEMD_PORTABLE_FORCE_ATTACH    (UINT64_C(1) &lt;&lt; 1)
-#define SD_SYSTEMD_PORTABLE_FORCE_SYSEXT    (UINT64_C(1) &lt;&lt; 2)
+#define SD_SYSTEMD_PORTABLE_RUNTIME            (UINT64_C(1) &lt;&lt; 0)
+#define SD_SYSTEMD_PORTABLE_FORCE_ATTACH       (UINT64_C(1) &lt;&lt; 1)
+#define SD_SYSTEMD_PORTABLE_FORCE_EXTENSION    (UINT64_C(1) &lt;&lt; 2)
       </programlisting>
     </refsect2>
 
index c7962f23494064e7ca7ab6c053b93ba392f53ad0..03ca65e0cb1a70d98521cff69bc8a488fa05d2ca 100644 (file)
         multiple times, in which case the order in which images are laid down follows the rules specified in
         <citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>
         for the <varname>ExtensionImages=</varname> directive and for the
-        <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry> tool.
+        <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry> and.
+        <citerefentry><refentrytitle>systemd-confext</refentrytitle><manvolnum>8</manvolnum></citerefentry> tools.
         The images must contain an <filename>extension-release</filename> file with metadata that matches
         what is defined in the <filename>os-release</filename> of <replaceable>IMAGE</replaceable>. See:
         <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
index d72f3a02db1f83f9b4356670813c31d80cb10a83..d4b448a6274ab2f892716e24da75a1f10eb435fd 100644 (file)
@@ -198,8 +198,18 @@ static int extract_now(
 
         /* First, find os-release/extension-release and send it upstream (or just save it). */
         if (path_is_extension) {
-                os_release_id = strjoina("/usr/lib/extension-release.d/extension-release.", image_name);
+                ImageClass class = IMAGE_SYSEXT;
+
                 r = open_extension_release(where, IMAGE_SYSEXT, image_name, relax_extension_release_check, &os_release_path, &os_release_fd);
+                if (r == -ENOENT) {
+                        r = open_extension_release(where, IMAGE_CONFEXT, image_name, relax_extension_release_check, &os_release_path, &os_release_fd);
+                        if (r >= 0)
+                                class = IMAGE_CONFEXT;
+                }
+                if (r < 0)
+                        return log_error_errno(r, "Failed to open extension release from '%s': %m", image_name);
+
+                os_release_id = strjoina((class == IMAGE_SYSEXT) ? "/usr/lib" : "/etc", "/extension-release.d/extension-release.", image_name);
         } else {
                 os_release_id = "/etc/os-release";
                 r = open_os_release(where, &os_release_path, &os_release_fd);
@@ -530,7 +540,7 @@ static int extract_image_and_extensions(
                 const char *name_or_path,
                 char **matches,
                 char **extension_image_paths,
-                bool validate_sysext,
+                bool validate_extension,
                 bool relax_extension_release_check,
                 const ImagePolicy *image_policy,
                 Image **ret_image,
@@ -541,7 +551,7 @@ static int extract_image_and_extensions(
                 char ***ret_valid_prefixes,
                 sd_bus_error *error) {
 
-        _cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL;
+        _cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL, *confext_level = NULL;
         _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
         _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
         _cleanup_hashmap_free_ Hashmap *unit_files = NULL;
@@ -596,13 +606,14 @@ static int extract_image_and_extensions(
         /* If we are layering extension images on top of a runtime image, check that the os-release and
          * extension-release metadata match, otherwise reject it immediately as invalid, or it will fail when
          * the units are started. Also, collect valid portable prefixes if caller requested that. */
-        if (validate_sysext || ret_valid_prefixes) {
+        if (validate_extension || ret_valid_prefixes) {
                 _cleanup_free_ char *prefixes = NULL;
 
                 r = parse_env_file_fd(os_release->fd, os_release->name,
                                      "ID", &id,
                                      "VERSION_ID", &version_id,
                                      "SYSEXT_LEVEL", &sysext_level,
+                                     "CONFEXT_LEVEL", &confext_level,
                                      "PORTABLE_PREFIXES", &prefixes);
                 if (r < 0)
                         return r;
@@ -638,15 +649,18 @@ static int extract_image_and_extensions(
                 if (r < 0)
                         return r;
 
-                if (!validate_sysext && !ret_valid_prefixes && !ret_extension_releases)
+                if (!validate_extension && !ret_valid_prefixes && !ret_extension_releases)
                         continue;
 
                 r = load_env_file_pairs_fd(extension_release_meta->fd, extension_release_meta->name, &extension_release);
                 if (r < 0)
                         return r;
 
-                if (validate_sysext) {
+                if (validate_extension) {
                         r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release, IMAGE_SYSEXT);
+                        if (r < 0)
+                                r = extension_release_validate(ext->path, id, version_id, confext_level, "portable", extension_release, IMAGE_CONFEXT);
+
                         if (r == 0)
                                 return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path);
                         if (r < 0)
@@ -717,8 +731,8 @@ int portable_extract(
                         name_or_path,
                         matches,
                         extension_image_paths,
-                        /* validate_sysext= */ false,
-                        /* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT),
+                        /* validate_extension= */ false,
+                        /* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION),
                         image_policy,
                         &image,
                         &extension_images,
@@ -979,16 +993,18 @@ static int append_release_log_fields(
         static const char *const field_versions[_IMAGE_CLASS_MAX][4]= {
                  [IMAGE_PORTABLE] = { "IMAGE_VERSION", "VERSION_ID", "BUILD_ID", NULL },
                  [IMAGE_SYSEXT] = { "SYSEXT_IMAGE_VERSION", "SYSEXT_VERSION_ID", "SYSEXT_BUILD_ID", NULL },
+                 [IMAGE_CONFEXT] = { "CONFEXT_IMAGE_VERSION", "CONFEXT_VERSION_ID", "CONFEXT_BUILD_ID", NULL },
         };
         static const char *const field_ids[_IMAGE_CLASS_MAX][3]= {
                  [IMAGE_PORTABLE] = { "IMAGE_ID", "ID", NULL },
                  [IMAGE_SYSEXT] = { "SYSEXT_IMAGE_ID", "SYSEXT_ID", NULL },
+                 [IMAGE_CONFEXT] = { "CONFEXT_IMAGE_ID", "CONFEXT_ID", NULL },
         };
         _cleanup_strv_free_ char **fields = NULL;
         const char *id = NULL, *version = NULL;
         int r;
 
-        assert(IN_SET(type, IMAGE_PORTABLE, IMAGE_SYSEXT));
+        assert(IN_SET(type, IMAGE_PORTABLE, IMAGE_SYSEXT, IMAGE_CONFEXT));
         assert(!strv_isempty((char *const *)field_ids[type]));
         assert(!strv_isempty((char *const *)field_versions[type]));
         assert(field_name);
@@ -1111,7 +1127,7 @@ static int install_chroot_dropin(
                                                /* With --force tell PID1 to avoid enforcing that the image <name> and
                                                 * extension-release.<name> have to match. */
                                                !IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) &&
-                                                   FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT) ?
+                                                   FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION) ?
                                                        ":x-systemd.relax-extension-release-check\n" :
                                                        "\n",
                                                /* In PORTABLE= we list the 'main' image name for this unit
@@ -1131,6 +1147,13 @@ static int install_chroot_dropin(
                                                               "PORTABLE_EXTENSION_NAME_AND_VERSION");
                                 if (r < 0)
                                         return r;
+
+                                r = append_release_log_fields(&text,
+                                                              ordered_hashmap_get(extension_releases, ext->name),
+                                                              IMAGE_CONFEXT,
+                                                              "PORTABLE_EXTENSION_NAME_AND_VERSION");
+                                if (r < 0)
+                                        return r;
                         }
         }
 
@@ -1432,8 +1455,8 @@ int portable_attach(
                         name_or_path,
                         matches,
                         extension_image_paths,
-                        /* validate_sysext= */ true,
-                        /* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT),
+                        /* validate_extension= */ true,
+                        /* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION),
                         image_policy,
                         &image,
                         &extension_images,
index c61d65fed3500f94874d415ac658bbe37da1eb39..c4a9d5103e6355d2ed3f73989b611aff2ba9a7ff 100644 (file)
@@ -18,19 +18,19 @@ typedef struct PortableMetadata {
 } PortableMetadata;
 
 #define PORTABLE_METADATA_IS_OS_RELEASE(m) (streq((m)->name, "/etc/os-release"))
-#define PORTABLE_METADATA_IS_EXTENSION_RELEASE(m) (startswith((m)->name, "/usr/lib/extension-release.d/extension-release."))
+#define PORTABLE_METADATA_IS_EXTENSION_RELEASE(m) (startswith_strv((m)->name, STRV_MAKE("/usr/lib/extension-release.d/extension-release.", "/etc/extension-release.d/extension-release.")))
 #define PORTABLE_METADATA_IS_UNIT(m) (!IN_SET((m)->name[0], 0, '/'))
 
 typedef enum PortableFlags {
-        PORTABLE_RUNTIME        = 1 << 0, /* Public API via DBUS, do not change */
-        PORTABLE_FORCE_ATTACH   = 1 << 1, /* Public API via DBUS, do not change */
-        PORTABLE_FORCE_SYSEXT   = 1 << 2, /* Public API via DBUS, do not change */
-        PORTABLE_PREFER_COPY    = 1 << 3,
-        PORTABLE_PREFER_SYMLINK = 1 << 4,
-        PORTABLE_REATTACH       = 1 << 5,
-        _PORTABLE_MASK_PUBLIC   = PORTABLE_RUNTIME | PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_SYSEXT,
+        PORTABLE_RUNTIME         = 1 << 0, /* Public API via DBUS, do not change */
+        PORTABLE_FORCE_ATTACH    = 1 << 1, /* Public API via DBUS, do not change */
+        PORTABLE_FORCE_EXTENSION = 1 << 2, /* Public API via DBUS, do not change */
+        PORTABLE_PREFER_COPY     = 1 << 3,
+        PORTABLE_PREFER_SYMLINK  = 1 << 4,
+        PORTABLE_REATTACH        = 1 << 5,
+        _PORTABLE_MASK_PUBLIC    = PORTABLE_RUNTIME | PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION,
         _PORTABLE_TYPE_MAX,
-        _PORTABLE_TYPE_INVALID  = -EINVAL,
+        _PORTABLE_TYPE_INVALID   = -EINVAL,
 } PortableFlags;
 
 /* This enum is anonymous, since we usually store it in an 'int', as we overload it with negative errno
index 3a25624b088f326aefa899f35c75cd3743f81b86..6f804e65ee51831796716df50cb63bf8ea28a663 100644 (file)
@@ -248,7 +248,7 @@ static int maybe_reload(sd_bus **bus) {
 static int get_image_metadata(sd_bus *bus, const char *image, char **matches, sd_bus_message **reply) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-        uint64_t flags = arg_force ? PORTABLE_FORCE_SYSEXT : 0;
+        uint64_t flags = arg_force ? PORTABLE_FORCE_EXTENSION : 0;
         const char *method;
         int r;
 
@@ -384,9 +384,16 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
                                 fflush(stdout);
                                 nl = true;
                         } else {
-                                _cleanup_free_ char *pretty_portable = NULL, *pretty_os = NULL, *sysext_level = NULL,
-                                        *sysext_id = NULL, *sysext_version_id = NULL, *sysext_scope = NULL, *portable_prefixes = NULL,
-                                        *id = NULL, *version_id = NULL, *image_id = NULL, *image_version = NULL, *build_id = NULL;
+                                _cleanup_free_ char *pretty_portable = NULL, *sysext_pretty_os = NULL,
+                                                    *sysext_level = NULL, *sysext_id = NULL,
+                                                    *sysext_version_id = NULL, *sysext_scope = NULL,
+                                                    *portable_prefixes = NULL, *id = NULL, *version_id = NULL,
+                                                    *sysext_image_id = NULL, *sysext_image_version = NULL,
+                                                    *sysext_build_id = NULL, *confext_pretty_os = NULL,
+                                                    *confext_level = NULL, *confext_id = NULL,
+                                                    *confext_version_id = NULL, *confext_scope = NULL,
+                                                    *confext_image_id = NULL, *confext_image_version = NULL,
+                                                    *confext_build_id = NULL;
                                 _cleanup_fclose_ FILE *f = NULL;
 
                                 f = fmemopen_unlocked((void*) data, sz, "r");
@@ -396,12 +403,20 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
                                 r = parse_env_file(f, name,
                                                    "SYSEXT_ID", &sysext_id,
                                                    "SYSEXT_VERSION_ID", &sysext_version_id,
-                                                   "SYSEXT_BUILD_ID", &build_id,
-                                                   "SYSEXT_IMAGE_ID", &image_id,
-                                                   "SYSEXT_IMAGE_VERSION", &image_version,
-                                                   "SYSEXT_PRETTY_NAME", &pretty_os,
+                                                   "SYSEXT_BUILD_ID", &sysext_build_id,
+                                                   "SYSEXT_IMAGE_ID", &sysext_image_id,
+                                                   "SYSEXT_IMAGE_VERSION", &sysext_image_version,
                                                    "SYSEXT_SCOPE", &sysext_scope,
                                                    "SYSEXT_LEVEL", &sysext_level,
+                                                   "SYSEXT_PRETTY_NAME", &sysext_pretty_os,
+                                                   "CONFEXT_ID", &confext_id,
+                                                   "CONFEXT_VERSION_ID", &confext_version_id,
+                                                   "CONFEXT_BUILD_ID", &confext_build_id,
+                                                   "CONFEXT_IMAGE_ID", &confext_image_id,
+                                                   "CONFEXT_IMAGE_VERSION", &confext_image_version,
+                                                   "CONFEXT_SCOPE", &confext_scope,
+                                                   "CONFEXT_LEVEL", &confext_level,
+                                                   "CONFEXT_PRETTY_NAME", &confext_pretty_os,
                                                    "ID", &id,
                                                    "VERSION_ID", &version_id,
                                                    "PORTABLE_PRETTY_NAME", &pretty_portable,
@@ -418,18 +433,18 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
                                        "\tPortable Prefixes:\n\t\t%s\n"
                                        "\tExtension Image:\n\t\t%s%s%s %s%s%s\n",
                                        name,
-                                       strna(sysext_scope),
-                                       strna(sysext_level),
+                                       strna(sysext_scope ?: confext_scope),
+                                       strna(sysext_level ?: confext_level),
                                        strna(id),
                                        strna(version_id),
                                        strna(pretty_portable),
                                        strna(portable_prefixes),
-                                       strempty(pretty_os),
-                                       pretty_os ? " (" : "ID: ",
-                                       strna(sysext_id ?: image_id),
-                                       pretty_os ? "" : "Version: ",
-                                       strna(sysext_version_id ?: image_version ?: build_id),
-                                       pretty_os ? ")" : "");
+                                       strempty(sysext_pretty_os ?: confext_pretty_os),
+                                       (sysext_pretty_os ?: confext_pretty_os) ? " (" : "ID: ",
+                                       strna(sysext_id ?: sysext_image_id ?: confext_id ?: confext_image_id),
+                                       (sysext_pretty_os ?: confext_pretty_os)  ? "" : "Version: ",
+                                       strna(sysext_version_id ?: sysext_image_version ?: sysext_build_id ?: confext_version_id ?: confext_image_version ?: confext_build_id),
+                                       (sysext_pretty_os ?: confext_pretty_os)  ? ")" : "");
                         }
 
                         r = sd_bus_message_exit_container(reply);
@@ -871,7 +886,7 @@ static int attach_reattach_image(int argc, char *argv[], const char *method) {
                 return bus_log_create_error(r);
 
         if (STR_IN_SET(method, "AttachImageWithExtensions", "ReattachImageWithExtensions")) {
-                uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_SYSEXT : 0);
+                uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION : 0);
 
                 r = sd_bus_message_append(m, "st", arg_copy_mode, flags);
         } else
@@ -943,7 +958,7 @@ static int detach_image(int argc, char *argv[], void *userdata) {
         if (streq(method, "DetachImage"))
                 r = sd_bus_message_append(m, "b", arg_runtime);
         else {
-                uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_SYSEXT : 0);
+                uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION : 0);
 
                 r = sd_bus_message_append(m, "t", flags);
         }
index b659c98529ccd95b391fabcd1ddbf02d17bcc1cc..e72ed57ca9efd6f0959e21b3b35ed363aed1b813 100644 (file)
@@ -814,6 +814,15 @@ EOF
         echo MARKER=1 >"$initdir/usr/lib/systemd/system/some_file"
         mksquashfs "$initdir" "$oldinitdir/usr/share/app0.raw" -noappend
 
+        export initdir="$TESTDIR/conf0"
+        mkdir -p "$initdir/etc/extension-release.d" "$initdir/etc/systemd/system" "$initdir/opt"
+        grep "^ID=" "$os_release" >"$initdir/etc/extension-release.d/extension-release.conf0"
+        echo "${version_id}" >>"$initdir/etc/extension-release.d/extension-release.conf0"
+        ( echo "${version_id}"
+          echo "CONFEXT_IMAGE_ID=app" ) >>"$initdir/etc/extension-release.d/extension-release.conf0"
+        echo MARKER_1 >"$initdir/etc/systemd/system/some_file"
+        mksquashfs "$initdir" "$oldinitdir/usr/share/conf0.raw" -noappend
+
         export initdir="$TESTDIR/app1"
         mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
         grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2"
index 4bbbd38bee3eee36afcfc839d58e60ea41bcbe7c..66256a40c8d0d93dc5587f901eaa08d0ca56d596 100755 (executable)
@@ -33,6 +33,7 @@ systemd-dissect --no-pager /usr/share/minimal_0.raw | grep -q '✓ portable serv
 systemd-dissect --no-pager /usr/share/minimal_1.raw | grep -q '✓ portable service'
 systemd-dissect --no-pager /usr/share/app0.raw | grep -q '✓ sysext for portable service'
 systemd-dissect --no-pager /usr/share/app1.raw | grep -q '✓ sysext for portable service'
+systemd-dissect --no-pager /usr/share/conf0.raw | grep -q '✓ confext for portable service'
 
 export SYSTEMD_LOG_LEVEL=debug
 mkdir -p /run/systemd/system/systemd-portabled.service.d/
@@ -187,6 +188,17 @@ portablectl inspect --force --cat --extension /tmp/app10.raw /usr/share/minimal_
 
 portablectl detach --force --now --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
 
+# portablectl also accepts confexts
+portablectl "${ARGS[@]}" attach --now --runtime --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0
+
+systemctl is-active app0.service
+status="$(portablectl is-attached --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl inspect --force --cat --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0 | grep -q -F "Extension Release: /usr/share/conf0.raw"
+
+portablectl detach --now --runtime --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0
+
 # portablectl also works with directory paths rather than images
 
 mkdir /tmp/rootdir /tmp/app0 /tmp/app1 /tmp/overlay /tmp/os-release-fix /tmp/os-release-fix/etc