sysupdate: load a per-component *.component file (#42935)

This is split out of #42651, because for some reason Claude refuses to
review that PR, probably because it's too large. Hence let's try this
piecemeal.

This has integration tests in #42651 (which passed). And docs too.
This commit is contained in:
Lennart Poettering
2026-07-09 12:21:28 +02:00
committed by GitHub
3 changed files with 183 additions and 18 deletions
+3
View File
@@ -37,6 +37,9 @@ bool component_name_valid(const char *c) {
if (!string_is_safe(c, STRING_FILENAME_PART))
return false;
if (endswith(c, ".component")) /* don't allow ambiguity around sysupdate.<foobar>.component.d/ */
return false;
/* Stack allocation is safe, since STRING_FILENAME_PART includes a length check */
const char *j = strjoina("sysupdate.", c, ".d");
+176 -18
View File
@@ -10,6 +10,7 @@
#include "build.h"
#include "bus-polkit.h"
#include "conf-files.h"
#include "conf-parser.h"
#include "constants.h"
#include "discover-image.h"
#include "dissect-image.h"
@@ -38,6 +39,7 @@
#include "strv.h"
#include "sysupdate.h"
#include "sysupdate-cleanup.h"
#include "sysupdate-config.h"
#include "sysupdate-feature.h"
#include "sysupdate-instance.h"
#include "sysupdate-target.h"
@@ -75,6 +77,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
#define CONTEXT_NULL \
(Context) { \
.component_enabled = true, \
.sync = true, \
.instances_max = UINT64_MAX, \
.verify = -1, \
@@ -114,6 +117,8 @@ void context_done(Context *c) {
c->root = mfree(c->root);
c->image = mfree(c->image);
c->component = mfree(c->component);
c->component_description = mfree(c->component_description);
c->component_documentation = strv_free(c->component_documentation);
c->image_policy = image_policy_free(c->image_policy);
c->transfer_source = mfree(c->transfer_source);
@@ -158,6 +163,50 @@ static int context_from_cmdline(Context *ret) {
return 0;
}
static int context_from_base_with_component(const Context *base, const char *component, Context *ret) {
assert(base);
assert(component);
assert(ret);
/* Copies the specified context, but changes the component to the specified one */
_cleanup_(context_done) Context context = CONTEXT_NULL;
context.instances_max = base->instances_max;
context.sync = base->sync;
context.reboot = base->reboot;
context.verify = base->verify;
context.offline = base->offline;
context.cleanup = base->cleanup;
if (strdup_to(&context.root, base->root) < 0)
return log_oom();
if (strdup_to(&context.image, base->image) < 0)
return log_oom();
if (strdup_to(&context.transfer_source, base->transfer_source) < 0)
return log_oom();
if (base->image_policy) {
context.image_policy = image_policy_copy(base->image_policy);
if (!context.image_policy)
return log_oom();
}
if (strdup_to(&context.component, component) < 0)
return log_oom();
/* NB: we do not copy .loop_device/.mounted_dir here, since that's for lifetime tracking only, and
* not needed for access (we only need to copy .root/.image) for that. Under the assumption that the
* contexts initialized via context_from_base_with_component() have a shorter lifetime than the
* contexts they are copied from we don't bother with lifetime tracking of the loopback device/mount
* point here. */
*ret = TAKE_GENERIC(context, Context, CONTEXT_NULL);
return 0;
}
/* Stores any long-running server state which needs to persist between varlink calls, such as state for
* pending polkit requests */
typedef struct Server {
@@ -280,9 +329,46 @@ static int read_transfers(
return 0;
}
static int read_component(Context *c) {
int r;
assert(c);
/* Read a component description file, but only if we actually operate on a component */
if (c->definitions || !c->component)
return 0;
_cleanup_free_ char *j = strjoin("sysupdate.", c->component, ".component");
if (!j)
return log_oom();
ConfigTableItem table[] = {
{ "Component", "Description", config_parse_string, 0, &c->component_description },
{ "Component", "Documentation", config_parse_url_specifiers_many, 0, &c->component_documentation },
{ "Component", "Enabled", config_parse_bool, 0, &c->component_enabled },
{}
};
r = config_parse_standard_file_with_dropins_full(
c->root,
/* root_fd= */ -EBADF,
j,
"Component\0",
config_item_table_lookup, table,
CONFIG_PARSE_WARN,
/* userdata= */ (void *) c->root,
/* ret_stats_by_path= */ NULL,
/* ret_dropin_files= */ NULL);
if (r < 0)
return r;
return 0;
}
typedef enum ReadDefinitionsFlags {
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0,
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS = 1 << 1,
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0, /* fail unless there's at least one enabled transfer */
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS = 1 << 1, /* fail unless there's at least one transfer */
READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT = 1 << 2, /* fail if component is disabled */
} ReadDefinitionsFlags;
static int context_read_definitions(Context *c, const char* node, ReadDefinitionsFlags flags) {
@@ -315,6 +401,10 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition
if (!dirs)
return log_oom();
r = read_component(c);
if (r < 0)
return r;
r = read_features(c, (const char**) dirs);
if (r < 0)
return r;
@@ -344,6 +434,9 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition
"No transfer definitions found.");
}
if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT) && !c->component_enabled)
return log_error_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Component is disabled.");
return 0;
}
@@ -1029,10 +1122,9 @@ static int context_process_image(
assert(c);
if (!c->image)
if (c->root || !c->image) /* Idempotent or nothing to do */
return 0;
assert(!c->root);
assert(!c->mounted_dir);
assert(!c->loop_device);
@@ -1094,7 +1186,9 @@ static int context_load_offline(
static int context_load_online(
Context *context,
ProcessImageFlags process_image_flags) {
ProcessImageFlags process_image_flags,
ReadDefinitionsFlags read_definitions_flags) {
int r;
assert(context);
@@ -1105,7 +1199,7 @@ static int context_load_online(
r = context_load_offline(
context,
process_image_flags,
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
read_definitions_flags);
if (r < 0)
return r;
@@ -1156,7 +1250,10 @@ static int context_load_paths_from_image(Context *context, Image *image) {
* syntactically validated by dispatch_target_identifier(), but might still point to components which don’t
* exist, images which the user isn’t privileged to access, etc. This function validates the TargetIdentifier
* against an enumerated list of known targets, which are safe to update without additional permissions. */
static int context_load_online_from_target(Context *context, ProcessImageFlags process_image_flags) {
static int context_load_online_from_target(
Context *context,
ProcessImageFlags process_image_flags,
ReadDefinitionsFlags read_definitions_flags) {
int r;
assert(context);
@@ -1247,7 +1344,7 @@ static int context_load_online_from_target(Context *context, ProcessImageFlags p
assert_not_reached();
}
return context_load_online(context, process_image_flags);
return context_load_online(context, process_image_flags, read_definitions_flags);
}
static int context_on_acquire_progress(const Transfer *t, const Instance *inst, unsigned percentage) {
@@ -1650,7 +1747,11 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
if (context.component_all)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
r = context_load_online(&context, PROCESS_IMAGE_READ_ONLY);
r = context_load_online(
&context,
PROCESS_IMAGE_READ_ONLY,
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
if (r < 0)
return r;
@@ -1860,7 +1961,9 @@ static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdat
r = context_load_online(
&context,
PROCESS_IMAGE_READ_ONLY);
PROCESS_IMAGE_READ_ONLY,
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
if (r < 0)
return r;
@@ -1919,7 +2022,11 @@ static int vl_method_check_new(sd_varlink *link, sd_json_variant *parameters, sd
/* CheckNew is always online */
context.offline = false;
r = context_load_online_from_target(&context, PROCESS_IMAGE_READ_ONLY);
r = context_load_online_from_target(
&context,
PROCESS_IMAGE_READ_ONLY,
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
if (r == -ENOENT)
return sd_varlink_error(link, "io.systemd.SysUpdate.NoSuchTarget", NULL);
if (r < 0)
@@ -1976,7 +2083,10 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag
r = context_load_online(
&context,
/* process_image_flags= */ 0);
/* process_image_flags= */ 0,
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS|
READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT);
if (r < 0) {
if (r != -ENOENT)
return r;
@@ -2108,7 +2218,9 @@ static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void
r = context_load_offline(
&context,
/* process_image_flags= */ 0,
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS|
READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT);
if (r < 0)
return r;
@@ -2210,12 +2322,58 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda
return 0;
}
if (has_default_component)
printf("%s<default>%s\n",
ansi_highlight(), ansi_normal());
_cleanup_(table_unrefp) Table *t = table_new("", "component", "description", "documentation");
if (!t)
return log_oom();
STRV_FOREACH(i, component_names)
puts(*i);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
if (has_default_component) {
r = table_add_many(
t,
TABLE_EMPTY,
TABLE_STRING, "<default>",
TABLE_SET_COLOR, ansi_highlight(),
TABLE_STRING, "Default Component",
TABLE_EMPTY);
if (r < 0)
return table_log_add_error(r);
}
STRV_FOREACH(i, component_names) {
_cleanup_(context_done) Context cc = CONTEXT_NULL;
r = context_from_base_with_component(
&context,
*i,
&cc);
if (r < 0)
return r;
r = context_load_offline(
&cc,
/* process_image_flags= */ 0,
/* read_definitions_flags= */ 0);
if (r == -ENOMEM)
return r;
if (r < 0)
continue;
const char *doc = cc.component_documentation ? cc.component_documentation[0] : NULL;
r = table_add_many(
t,
TABLE_BOOLEAN_CHECKMARK, cc.component_enabled,
TABLE_SET_COLOR, ansi_highlight_green_red(cc.component_enabled),
TABLE_STRING, *i,
TABLE_STRING, cc.component_description,
TABLE_STRING, doc,
TABLE_SET_URL, doc);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
} else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
+4
View File
@@ -24,6 +24,10 @@ typedef struct Context {
LoopDevice *loop_device;
char *mounted_dir;
char *component_description;
char **component_documentation;
bool component_enabled;
Transfer **transfers;
size_t n_transfers;