diff --git a/man/org.freedesktop.portable1.xml b/man/org.freedesktop.portable1.xml
index c68995d952..4de3da2905 100644
--- a/man/org.freedesktop.portable1.xml
+++ b/man/org.freedesktop.portable1.xml
@@ -259,6 +259,9 @@ node /org/freedesktop/portable1 {
on the system. Note that this method returns only after all the listed operations are completed,
and due to the I/O involved it might take some time.
+
+
+
AttachImageWithExtensions() attaches a portable image to the system.
This method is a superset of AttachImage() with the addition of
a list of extensions as input parameter, which will be overlaid on top of the main
diff --git a/man/portablectl.xml b/man/portablectl.xml
index 5e88eb309d..92d8ff03aa 100644
--- a/man/portablectl.xml
+++ b/man/portablectl.xml
@@ -141,6 +141,8 @@
immediately started (blocking operation unless is passed) and/or enabled after
attaching the image.
+
+
@@ -421,6 +423,8 @@
Note that the same extensions have to be specified, in the same order, when attaching
and detaching.
+
+
diff --git a/src/portable/portable.c b/src/portable/portable.c
index 60dc98c5ba..53418c417b 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -43,6 +43,7 @@
#include "strv.h"
#include "tmpfile-util.h"
#include "user-util.h"
+#include "vpick.h"
/* Markers used in the first line of our 20-portable.conf unit file drop-in to determine, that a) the unit file was
* dropped there by the portable service logic and b) for which image it was dropped there. */
@@ -564,6 +565,7 @@ static int extract_image_and_extensions(
_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_(pick_result_done) PickResult result = PICK_RESULT_NULL;
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
_cleanup_strv_free_ char **valid_prefixes = NULL;
_cleanup_(image_unrefp) Image *image = NULL;
@@ -572,7 +574,27 @@ static int extract_image_and_extensions(
assert(name_or_path);
- r = image_find_harder(IMAGE_PORTABLE, name_or_path, NULL, &image);
+ /* If we get a path, then check if it can be resolved with vpick. We need this as we might just
+ * get a simple image name, which would make vpick error out. */
+ if (path_is_absolute(name_or_path)) {
+ r = path_pick(/* toplevel_path= */ NULL,
+ /* toplevel_fd= */ AT_FDCWD,
+ name_or_path,
+ &pick_filter_image_any,
+ PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
+ &result);
+ if (r < 0)
+ return r;
+ if (!result.path)
+ return log_debug_errno(
+ SYNTHETIC_ERRNO(ENOENT),
+ "No matching entry in .v/ directory %s found.",
+ name_or_path);
+
+ name_or_path = result.path;
+ }
+
+ r = image_find_harder(IMAGE_PORTABLE, name_or_path, /* root= */ NULL, &image);
if (r < 0)
return r;
@@ -588,9 +610,29 @@ static int extract_image_and_extensions(
}
STRV_FOREACH(p, extension_image_paths) {
+ _cleanup_(pick_result_done) PickResult ext_result = PICK_RESULT_NULL;
_cleanup_(image_unrefp) Image *new = NULL;
+ const char *path = *p;
- r = image_find_harder(IMAGE_PORTABLE, *p, NULL, &new);
+ if (path_is_absolute(*p)) {
+ r = path_pick(/* toplevel_path= */ NULL,
+ /* toplevel_fd= */ AT_FDCWD,
+ *p,
+ &pick_filter_image_any,
+ PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
+ &ext_result);
+ if (r < 0)
+ return r;
+ if (!ext_result.path)
+ return log_debug_errno(
+ SYNTHETIC_ERRNO(ENOENT),
+ "No matching entry in .v/ directory %s found.",
+ *p);
+
+ path = ext_result.path;
+ }
+
+ r = image_find_harder(IMAGE_PORTABLE, path, NULL, &new);
if (r < 0)
return r;
@@ -1691,6 +1733,7 @@ static bool marker_matches_images(const char *marker, const char *name_or_path,
while (!isempty(marker))
STRV_FOREACH(image_name_or_path, root_and_extensions) {
_cleanup_free_ char *image = NULL, *base_image = NULL, *base_image_name_or_path = NULL;
+ _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = extract_first_word(&marker, &image, ":", EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE);
if (r < 0)
@@ -1702,9 +1745,23 @@ static bool marker_matches_images(const char *marker, const char *name_or_path,
if (r < 0)
return log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", image);
- r = path_extract_image_name(*image_name_or_path, &base_image_name_or_path);
+ r = path_pick(/* toplevel_path= */ NULL,
+ /* toplevel_fd= */ AT_FDCWD,
+ *image_name_or_path,
+ &pick_filter_image_any,
+ PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
+ &result);
if (r < 0)
- return log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", *image_name_or_path);
+ return r;
+ if (!result.path)
+ return log_debug_errno(
+ SYNTHETIC_ERRNO(ENOENT),
+ "No matching entry in .v/ directory %s found.",
+ *image_name_or_path);
+
+ r = path_extract_image_name(result.path, &base_image_name_or_path);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", result.path);
if (!streq(base_image, base_image_name_or_path)) {
if (match_all)
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index b7a97f2b63..4e7a2049cf 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -639,7 +639,7 @@ int image_find(ImageClass class,
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
.basename = name,
.architecture = _ARCHITECTURE_INVALID,
- .suffix = suffix,
+ .suffix = STRV_MAKE(suffix),
};
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
@@ -807,7 +807,7 @@ int image_discover(
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
.basename = pretty,
.architecture = _ARCHITECTURE_INVALID,
- .suffix = suffix,
+ .suffix = STRV_MAKE(suffix),
};
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
diff --git a/src/shared/vpick.c b/src/shared/vpick.c
index 1870fb97ef..41bd7301a0 100644
--- a/src/shared/vpick.c
+++ b/src/shared/vpick.c
@@ -85,8 +85,13 @@ static int format_fname(
return -ENOMEM;
}
- if (filter->suffix && !strextend(&fn, filter->suffix))
- return -ENOMEM;
+ if (!strv_isempty(filter->suffix)) {
+ if (strv_length(filter->suffix) > 1)
+ return -ENOEXEC;
+
+ if (!strextend(&fn, filter->suffix[0]))
+ return -ENOMEM;
+ }
if (!filename_is_valid(fn))
return -EINVAL;
@@ -352,8 +357,8 @@ static int make_choice(
} else
e = dname;
- if (!isempty(filter->suffix)) {
- char *sfx = endswith(e, filter->suffix);
+ if (!strv_isempty(filter->suffix)) {
+ char *sfx = endswith_strv(e, filter->suffix);
if (!sfx)
continue;
@@ -493,7 +498,8 @@ int path_pick(
PickResult *ret) {
_cleanup_free_ char *filter_bname = NULL, *dir = NULL, *parent = NULL, *fname = NULL;
- const char *filter_suffix, *enumeration_path;
+ char * const *filter_suffix_strv = NULL;
+ const char *filter_suffix = NULL, *enumeration_path;
uint32_t filter_type_mask;
int r;
@@ -549,14 +555,12 @@ int path_pick(
if (!filter_bname)
return -ENOMEM;
- if (filter->suffix) {
- /* Chop off suffix, if specified */
- char *f = endswith(filter_bname, filter->suffix);
- if (f)
- *f = 0;
- }
+ /* Chop off suffix, if specified */
+ char *f = endswith_strv(filter_bname, filter->suffix);
+ if (f)
+ *f = 0;
- filter_suffix = filter->suffix;
+ filter_suffix_strv = filter->suffix;
filter_type_mask = filter->type_mask;
enumeration_path = path;
@@ -616,7 +620,7 @@ int path_pick(
.basename = filter_bname,
.version = filter->version,
.architecture = filter->architecture,
- .suffix = filter_suffix,
+ .suffix = filter_suffix_strv ?: STRV_MAKE(filter_suffix),
},
flags,
ret);
@@ -685,10 +689,16 @@ int path_pick_update_warn(
const PickFilter pick_filter_image_raw = {
.type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK),
.architecture = _ARCHITECTURE_INVALID,
- .suffix = ".raw",
+ .suffix = STRV_MAKE(".raw"),
};
const PickFilter pick_filter_image_dir = {
.type_mask = UINT32_C(1) << DT_DIR,
.architecture = _ARCHITECTURE_INVALID,
};
+
+const PickFilter pick_filter_image_any = {
+ .type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) | (UINT32_C(1) << DT_DIR),
+ .architecture = _ARCHITECTURE_INVALID,
+ .suffix = STRV_MAKE(".raw", ""),
+};
diff --git a/src/shared/vpick.h b/src/shared/vpick.h
index 21ce668404..38251c84e8 100644
--- a/src/shared/vpick.h
+++ b/src/shared/vpick.h
@@ -16,7 +16,7 @@ typedef struct PickFilter {
const char *basename; /* Can be overridden by search pattern */
const char *version;
Architecture architecture;
- const char *suffix; /* Can be overridden by search pattern */
+ char * const *suffix; /* Can be overridden by search pattern */
} PickFilter;
typedef struct PickResult {
@@ -58,3 +58,4 @@ int path_pick_update_warn(
extern const PickFilter pick_filter_image_raw;
extern const PickFilter pick_filter_image_dir;
+extern const PickFilter pick_filter_image_any;
diff --git a/src/test/test-vpick.c b/src/test/test-vpick.c
index feb16b61a4..88646ec053 100644
--- a/src/test/test-vpick.c
+++ b/src/test/test-vpick.c
@@ -47,7 +47,7 @@ TEST(path_pick) {
PickFilter filter = {
.architecture = _ARCHITECTURE_INVALID,
- .suffix = ".raw",
+ .suffix = STRV_MAKE(".raw"),
};
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
diff --git a/src/vpick/vpick-tool.c b/src/vpick/vpick-tool.c
index f3afc33a58..0504b7733b 100644
--- a/src/vpick/vpick-tool.c
+++ b/src/vpick/vpick-tool.c
@@ -241,7 +241,7 @@ static int run(int argc, char *argv[]) {
.basename = arg_filter_basename,
.version = arg_filter_version,
.architecture = arg_filter_architecture,
- .suffix = arg_filter_suffix,
+ .suffix = STRV_MAKE(arg_filter_suffix),
.type_mask = arg_filter_type_mask,
},
arg_flags,
diff --git a/test/units/testsuite-29.sh b/test/units/testsuite-29.sh
index 977d76e22f..4c0f1ba329 100755
--- a/test/units/testsuite-29.sh
+++ b/test/units/testsuite-29.sh
@@ -183,6 +183,26 @@ status="$(portablectl is-attached --extension app1 minimal_0)"
portablectl detach --now --runtime --extension /usr/share/app1.raw /usr/share/minimal_0.raw app1
+# Ensure vpick works, including reattaching to a new image
+mkdir -p /tmp/app1.v/
+cp /usr/share/app1.raw /tmp/app1.v/app1_1.0.raw
+cp /tmp/app1_2.raw /tmp/app1.v/app1_2.0.raw
+portablectl "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1_2.0.raw minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+rm -f /tmp/app1.v/app1_2.0.raw
+portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
+
+systemctl is-active app1.service
+status="$(portablectl is-attached --extension app1_1.0.raw minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl detach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_0.raw app1
+rm -f /tmp/app1.v/app1_1.0.raw
+
# Ensure that the combination of read-only images, state directory and dynamic user works, and that
# state is retained. Check after detaching, as on slow systems (eg: sanitizers) it might take a while
# after the service is attached before the file appears.