core: open more fields up to StartTransientUnit via Varlink

This commit is contained in:
Lennart Poettering
2026-09-22 16:02:44 +02:00
committed by Luca Boccassi
parent c64cb914ac
commit 214ca71682
3 changed files with 224 additions and 8 deletions
+145 -3
View File
@@ -664,6 +664,29 @@ static void transient_exec_command_item_done(TransientExecCommandItem *i) {
static JSON_DISPATCH_ENUM_DEFINE(dispatch_service_type, ServiceType, service_type_from_string);
static JSON_DISPATCH_ENUM_DEFINE(dispatch_job_mode, JobMode, job_mode_from_string);
static JSON_DISPATCH_ENUM_DEFINE(dispatch_collect_mode, CollectMode, collect_mode_from_string);
static JSON_DISPATCH_ENUM_DEFINE(dispatch_exec_input, ExecInput, exec_input_from_string);
static ExecOutput exec_output_from_string_harder(const char *s) {
ExecOutput o;
/* Like exec_output_from_string(), but also accepts the spelling of the combined types used in the
* Varlink IDL (and hence in our own JSON output), i.e. with "_" instead of "+", so that what we emit
* can be fed back to us. */
o = exec_output_from_string(s);
if (o >= 0)
return o;
if (streq_ptr(s, "kmsg_console"))
return EXEC_OUTPUT_KMSG_AND_CONSOLE;
if (streq_ptr(s, "journal_console"))
return EXEC_OUTPUT_JOURNAL_AND_CONSOLE;
return _EXEC_OUTPUT_INVALID;
}
static JSON_DISPATCH_ENUM_DEFINE(dispatch_exec_output, ExecOutput, exec_output_from_string_harder);
static JSON_DISPATCH_ENUM_DEFINE(dispatch_kill_mode, KillMode, kill_mode_from_string);
typedef struct TransientWorkingDirectory {
const char *path;
@@ -703,6 +726,7 @@ typedef struct TransientExecContextParameters {
const char *root_image;
const char *root_mstack;
const char *root_verity;
const char *tty_path;
const char *user_namespace_path;
bool nice_set;
int nice;
@@ -720,6 +744,10 @@ typedef struct TransientExecContextParameters {
int restrict_realtime;
int restrict_suid_sgid;
int root_ephemeral;
/* Enums: parsed via JSON_DISPATCH_ENUM_DEFINE dispatchers, negative = absent. */
ExecInput std_input;
ExecOutput std_output;
ExecOutput std_error;
} TransientExecContextParameters;
static void transient_set_credential_array_free(TransientSetCredential *items, size_t n) {
@@ -736,6 +764,20 @@ static void transient_exec_context_parameters_done(TransientExecContextParameter
transient_set_credential_array_free(p->set_credentials_encrypted, p->n_set_credentials_encrypted);
}
typedef struct TransientKillContextParameters {
bool present;
KillMode kill_mode; /* negative = absent */
int send_sighup; /* tristate: -1 = absent */
} TransientKillContextParameters;
static void transient_kill_context_parameters_init(TransientKillContextParameters *p) {
assert(p);
*p = (TransientKillContextParameters) {
.kill_mode = _KILL_MODE_INVALID,
.send_sighup = -1,
};
}
typedef struct TransientServiceParameters {
bool present;
ServiceType type;
@@ -814,8 +856,10 @@ typedef struct StartTransientContextParameters {
const char *description;
CollectMode collect_mode;
TransientExecContextParameters exec;
TransientKillContextParameters kill;
TransientServiceParameters service;
const char *bad_exec_field; /* Set by inner Exec dispatcher to the unknown sub-property name */
const char *bad_kill_field;
const char *bad_service_field;
} StartTransientContextParameters;
@@ -833,6 +877,7 @@ static void start_transient_context_parameters_init(StartTransientContextParamet
.collect_mode = _COLLECT_MODE_INVALID,
};
transient_exec_context_parameters_init(&p->exec);
transient_kill_context_parameters_init(&p->kill);
transient_service_parameters_init(&p->service);
}
@@ -959,6 +1004,18 @@ static int dispatch_transient_set_credential_encrypted(const char *name, sd_json
static int dispatch_transient_exec_context(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
static int dispatch_transient_kill(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
static const sd_json_dispatch_field kill_dispatch[] = {
{ "KillMode", SD_JSON_VARIANT_STRING, dispatch_kill_mode, offsetof(TransientKillContextParameters, kill_mode), 0 },
{ "SendSIGHUP", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(TransientKillContextParameters, send_sighup), 0 },
{}
};
StartTransientContextParameters *p = ASSERT_PTR(userdata);
p->kill.present = true;
return sd_json_dispatch_full(variant, kill_dispatch, /* bad= */ NULL, /* flags= */ 0, &p->kill, &p->bad_kill_field);
}
static int dispatch_transient_service(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
static const sd_json_dispatch_field service_dispatch[] = {
{ "Type", SD_JSON_VARIANT_STRING, dispatch_service_type, offsetof(TransientServiceParameters, type), 0 },
@@ -981,6 +1038,7 @@ static int dispatch_transient_context(const char *name, sd_json_variant *variant
{ "Description", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(StartTransientContextParameters, description), 0 },
{ "CollectMode", SD_JSON_VARIANT_STRING, dispatch_collect_mode, offsetof(StartTransientContextParameters, collect_mode), 0 },
{ "Exec", SD_JSON_VARIANT_OBJECT, dispatch_transient_exec_context, 0, 0 },
{ "Kill", SD_JSON_VARIANT_OBJECT, dispatch_transient_kill, 0, 0 },
{ "Service", SD_JSON_VARIANT_OBJECT, dispatch_transient_service, 0, 0 },
{}
};
@@ -999,6 +1057,8 @@ static int dispatch_transient_context(const char *name, sd_json_variant *variant
* actual sub-property. */
if (streq(bad_field, "Exec") && !isempty(p->context.bad_exec_field))
p->unsupported_property = strjoin("Exec.", p->context.bad_exec_field);
else if (streq(bad_field, "Kill") && !isempty(p->context.bad_kill_field))
p->unsupported_property = strjoin("Kill.", p->context.bad_kill_field);
else if (streq(bad_field, "Service") && !isempty(p->context.bad_service_field))
p->unsupported_property = strjoin("Service.", p->context.bad_service_field);
else
@@ -1030,6 +1090,24 @@ static int transient_unit_apply_properties(Unit *u, StartTransientContextParamet
return 0;
}
static int transient_kill_context_apply_properties(Unit *u, KillContext *k, TransientKillContextParameters *p) {
assert(u);
assert(k);
assert(p);
if (p->kill_mode >= 0) {
k->kill_mode = p->kill_mode;
unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "KillMode", "KillMode=%s", kill_mode_to_string(p->kill_mode));
}
if (p->send_sighup >= 0) {
k->send_sighup = p->send_sighup;
unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "SendSIGHUP", "SendSIGHUP=%s", yes_no(p->send_sighup));
}
return 0;
}
static int transient_apply_set_credentials(
Unit *u,
ExecContext *c,
@@ -1252,6 +1330,38 @@ DEFINE_APPLY_EXEC_TRISTATE_BOOL(restrict_realtime, "RestrictRealtime");
DEFINE_APPLY_EXEC_TRISTATE_BOOL(restrict_suid_sgid, "RestrictSUIDSGID");
DEFINE_APPLY_EXEC_TRISTATE_BOOL(root_ephemeral, "RootEphemeral");
/* The stdio types that require a companion setting: the file based ones need a path (StandardInputFile=,
* StandardOutputFile=, …) and StandardInput=data needs the payload (StandardInputData=). The Varlink
* interface has no way to pass either yet, and without them the executor cannot set them up (it would assert
* on the missing path, and silently fall back to /dev/null on the missing payload), hence refuse them for
* now. For the output types exec_output_is_file() expresses the same. */
static bool exec_input_needs_companion(ExecInput i) {
return IN_SET(i, EXEC_INPUT_FILE, EXEC_INPUT_DATA);
}
/* Generate an apply fn for an exec-context enum field: copy the parsed value to ExecContext and
* write the corresponding "Name=value" line. A negative parsed value means the field was absent, values
* for which refused_fn() returns true are rejected as invalid.
* Mirrors D-Bus BUS_DEFINE_SET_TRANSIENT_PARSE. */
#define DEFINE_APPLY_EXEC_ENUM(field, JsonName, to_string_fn, refused_fn) \
static int apply_exec_##field(Unit *u, ExecContext *c, TransientExecContextParameters *p) { \
assert(c); \
assert(p); \
if (p->field < 0) \
return 0; \
if (refused_fn(p->field)) \
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), \
JsonName "=%s is not supported via Varlink.", to_string_fn(p->field)); \
c->field = p->field; \
unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, JsonName, \
JsonName "=%s", to_string_fn(p->field)); \
return 0; \
}
DEFINE_APPLY_EXEC_ENUM(std_error, "StandardError", exec_output_to_string, exec_output_is_file);
DEFINE_APPLY_EXEC_ENUM(std_input, "StandardInput", exec_input_to_string, exec_input_needs_companion);
DEFINE_APPLY_EXEC_ENUM(std_output, "StandardOutput", exec_output_to_string, exec_output_is_file);
/* Generate an apply fn for an exec-context absolute-path string: validate via path_is_absolute(),
* copy into ExecContext, write "Name=<path>". Mirrors D-Bus bus_set_transient_path. */
#define DEFINE_APPLY_EXEC_ABSOLUTE_PATH(field, JsonName) \
@@ -1278,6 +1388,7 @@ DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_directory, "RootDirectory");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_image, "RootImage");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_mstack, "RootMStack");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(root_verity, "RootVerity");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(tty_path, "TTYPath");
DEFINE_APPLY_EXEC_ABSOLUTE_PATH(user_namespace_path, "UserNamespacePath");
/* RootHashPath and RootHashSignaturePath need custom apply fns: the corresponding unit-file
@@ -1341,7 +1452,8 @@ static int apply_exec_root_hash_sig_path(Unit *u, ExecContext *c, TransientExecC
* the property was set), <0 on error. -EINVAL is mapped to err_field by the
* central loop.
* tristate - If true, the int at parse_offset is initialized to -1 (absent sentinel) by
* transient_exec_context_parameters_init(). Defaults to false.
* transient_exec_context_parameters_init(). Used for tristate bools and enum
* fields (both treat negative as absent). Defaults to false.
*/
typedef struct TransientExecProperty {
const char *json_name;
@@ -1378,6 +1490,13 @@ typedef struct TransientExecProperty {
{ json, "Exec." json, SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, \
offsetof(TransientExecContextParameters, field), 0, apply_exec_##field }
/* Enum property: parsed via a JSON_DISPATCH_ENUM_DEFINE dispatcher directly into the enum field at
* parse_offset. Sets .tristate=true so the field is seeded to -1 (= absent) before dispatch; the
* matching DEFINE_APPLY_EXEC_ENUM fn treats any negative value as unset. */
#define EXEC_PROPERTY_ENUM(json, field, dispatch_fn) \
{ json, "Exec." json, SD_JSON_VARIANT_STRING, dispatch_fn, \
offsetof(TransientExecContextParameters, field), 0, apply_exec_##field, true }
static const TransientExecProperty exec_properties[] = {
EXEC_PROPERTY ("WorkingDirectory", SD_JSON_VARIANT_OBJECT, dispatch_transient_working_directory, 0, 0, apply_exec_working_directory),
EXEC_PROPERTY_ABSOLUTE_PATH("RootDirectory", root_directory),
@@ -1405,6 +1524,10 @@ static const TransientExecProperty exec_properties[] = {
EXEC_PROPERTY_TRISTATE_BOOL("RestrictSUIDSGID", restrict_suid_sgid),
EXEC_PROPERTY_TRISTATE_BOOL("RemoveIPC", remove_ipc),
EXEC_PROPERTY ("Environment", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_transient_environment, 0, 0, apply_exec_environment),
EXEC_PROPERTY_ENUM ("StandardInput", std_input, dispatch_exec_input),
EXEC_PROPERTY_ENUM ("StandardOutput", std_output, dispatch_exec_output),
EXEC_PROPERTY_ENUM ("StandardError", std_error, dispatch_exec_output),
EXEC_PROPERTY_ABSOLUTE_PATH("TTYPath", tty_path),
EXEC_PROPERTY ("SetCredential", SD_JSON_VARIANT_ARRAY, dispatch_transient_set_credential, 0, 0, apply_exec_set_credential),
EXEC_PROPERTY ("SetCredentialEncrypted", SD_JSON_VARIANT_ARRAY, dispatch_transient_set_credential_encrypted, 0, 0, apply_exec_set_credential_encrypted),
};
@@ -1412,14 +1535,21 @@ static const TransientExecProperty exec_properties[] = {
#undef EXEC_PROPERTY_TRISTATE_BOOL
#undef EXEC_PROPERTY_STRING
#undef EXEC_PROPERTY_ABSOLUTE_PATH
#undef EXEC_PROPERTY_ENUM
/* Tristate-bool fields default to -1 (= absent). Default-zeroed slots would look "explicitly false"
* to the apply path. Initialize all tristate fields here so adding a new tristate property only
/* Tristate-bool and enum fields default to -1 (= absent). Default-zeroed slots would look
* "explicitly set" to the apply path. Initialize all tristate fields here so adding a new such property only
* requires a new exec_properties[] row, never a fresh designated initializer at the call site.
* The .tristate descriptor flag drives the loop -- mirrors the .cleanup hook used on the Service
* side, and avoids fragile dispatch-function-pointer equality checks. */
static void transient_exec_context_parameters_init(TransientExecContextParameters *p) {
assert(p);
/* The enum fields are seeded through an int pointer below, hence make sure they really are that
* wide (their dispatchers write them through their own type) */
assert_cc(sizeof(ExecInput) == sizeof(int));
assert_cc(sizeof(ExecOutput) == sizeof(int));
FOREACH_ELEMENT(prop, exec_properties)
if (prop->tristate)
*(int*) ((uint8_t*) p + prop->parse_offset) = -1;
@@ -1725,6 +1855,18 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters
return sd_varlink_error_errno(link, r);
}
/* Apply context.Kill only when a Kill object was actually sent, on unit types that have a kill
* context. */
if (p.context.kill.present) {
KillContext *k = unit_get_kill_context(u);
if (!k)
return sd_varlink_error(link, VARLINK_ERROR_UNIT_TYPE_NOT_SUPPORTED, NULL);
r = transient_kill_context_apply_properties(u, k, &p.context.kill);
if (r < 0)
return sd_varlink_error_errno(link, r);
}
/* Apply service-specific properties from context.Service */
Service *s = SERVICE(u);
if (s) {
+5 -5
View File
@@ -1569,11 +1569,11 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_FIELD_COMMENT("The parameter passed to the condition"),
SD_VARLINK_DEFINE_FIELD(parameter, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
/* UnitContext is used both as input to StartTransient (subset settable at creation time: ID,
* Description, CollectMode, Service, and the Exec subset {WorkingDirectory, Environment, SetCredential,
* SetCredentialEncrypted}) and as output from List/StartTransient (full unit configuration). Fields
* that are not settable at creation time are rejected with PropertyNotSupported when supplied as
* input. */
/* UnitContext is used both as input to StartTransient (subset settable at creation time: ID, Description,
* CollectMode, Service, Kill (currently KillMode and SendSIGHUP only), and the Exec subset listed in
* exec_properties[] in src/core/varlink-unit.c) and as output from List/StartTransient (full unit
* configuration). Fields that are not settable at creation time are rejected with PropertyNotSupported when
* supplied as input. */
static SD_VARLINK_DEFINE_STRUCT_TYPE(
UnitContext,
SD_VARLINK_FIELD_COMMENT("The unit type"),
@@ -211,6 +211,44 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-home.service; do sleep 0.5; done'
systemctl show -P WorkingDirectory varlink-transient-wd-home.service | grep '^~$' >/dev/null
# Exec.StandardInput/StandardOutput/StandardError enum properties and Exec.TTYPath
defer_transient_cleanup varlink-transient-stdio.service
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-stdio.service","Exec":{"StandardInput":"null","StandardOutput":"null","StandardError":"journal","TTYPath":"/dev/tty2"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
echo "$result" | jq -e '.context.Exec.StandardInput == "null"'
echo "$result" | jq -e '.context.Exec.StandardOutput == "null"'
echo "$result" | jq -e '.context.Exec.StandardError == "journal"'
echo "$result" | jq -e '.context.Exec.TTYPath == "/dev/tty2"'
timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio.service; do sleep 0.5; done'
systemctl show -P StandardInput varlink-transient-stdio.service | grep '^null$' >/dev/null
systemctl show -P StandardOutput varlink-transient-stdio.service | grep '^null$' >/dev/null
systemctl show -P StandardError varlink-transient-stdio.service | grep '^journal$' >/dev/null
systemctl show -P TTYPath varlink-transient-stdio.service | grep '^/dev/tty2$' >/dev/null
# "null" is also the default for StandardInput=, hence additionally verify it was written explicitly
fragment=$(systemctl show -P FragmentPath varlink-transient-stdio.service)
grep '^StandardInput=null$' "$fragment" >/dev/null
# The combined output types are spelled with a "+" in unit files, but with a "_" in the Varlink IDL (and
# hence in our own replies). The latter spelling must be accepted, so that replies can be fed back in.
defer_transient_cleanup varlink-transient-stdio-combined.service
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-stdio-combined.service","Exec":{"StandardOutput":"kmsg_console","StandardError":"journal_console"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
echo "$result" | jq -e '.context.Exec.StandardOutput == "kmsg_console"'
echo "$result" | jq -e '.context.Exec.StandardError == "journal_console"'
timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio-combined.service; do sleep 0.5; done'
systemctl show -P StandardOutput varlink-transient-stdio-combined.service | grep '^kmsg+console$' >/dev/null
systemctl show -P StandardError varlink-transient-stdio-combined.service | grep '^journal+console$' >/dev/null
# Kill.KillMode/Kill.SendSIGHUP
defer_transient_cleanup varlink-transient-kill.service
result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-kill.service","Kill":{"KillMode":"process","SendSIGHUP":true},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
echo "$result" | jq -e '.context.Kill.KillMode == "process"'
echo "$result" | jq -e '.context.Kill.SendSIGHUP == true'
timeout 30 bash -c 'until systemctl is-active varlink-transient-kill.service; do sleep 0.5; done'
systemctl show -P KillMode varlink-transient-kill.service | grep '^process$' >/dev/null
systemctl show -P SendSIGHUP varlink-transient-kill.service | grep '^yes$' >/dev/null
# Exec.SetCredential: pass a credential and verify the running process can read it
defer_transient_cleanup varlink-transient-cred.service
CRED_VALUE_B64=$(printf 'secret-value' | base64 -w0)
@@ -340,6 +378,33 @@ defer_transient_cleanup varlink-transient-bad-rd.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-rd.service","Exec":{"RootDirectory":"relative/path"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
"Exec.RootDirectory"
# Note that an unparsable enum value (e.g. StandardOutput=bogus) is rejected by the Varlink IDL
# validation layer before the JSON dispatch even runs, hence there's no point in testing pid1's
# dispatch-level enum handling here — and doing so would trip post.sh's "didn't pass validation"
# journal check, since the IDL layer logs about the rejection.
# Relative TTYPath is rejected
defer_transient_cleanup varlink-transient-bad-ttypath.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-ttypath.service","Exec":{"TTYPath":"relative/tty"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
"Exec.TTYPath"
# The file based stdio types need a companion path, and StandardInput=data a companion payload, neither of
# which can be passed yet, hence they are rejected
for t in file append truncate; do
defer_transient_cleanup "varlink-transient-bad-stdout-$t.service"
expect_invalid_parameter \
"{\"context\":{\"ID\":\"varlink-transient-bad-stdout-$t.service\",\"Exec\":{\"StandardOutput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \
"Exec.StandardOutput"
done
defer_transient_cleanup varlink-transient-bad-stderr.service
expect_invalid_parameter \
'{"context":{"ID":"varlink-transient-bad-stderr.service","Exec":{"StandardError":"file"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
"Exec.StandardError"
for t in file data; do
defer_transient_cleanup "varlink-transient-bad-stdin-$t.service"
expect_invalid_parameter \
"{\"context\":{\"ID\":\"varlink-transient-bad-stdin-$t.service\",\"Exec\":{\"StandardInput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \
"Exec.StandardInput"
done
# Invalid credential ID
defer_transient_cleanup varlink-transient-bad-cred-id.service
expect_invalid_parameter \
@@ -353,6 +418,9 @@ expect_invalid_parameter \
# Exec on a unit type without an exec context (.slice) is rejected
varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
# Same for Kill on a unit type without a kill context
varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-kill.slice","Kill":{"KillMode":"process"}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
# Unknown field in Exec is rejected as PropertyNotSupported
defer_transient_cleanup varlink-transient-unknown-exec.service
unsupported_exec=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
@@ -366,6 +434,12 @@ unsupported_service=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTra
'{"context":{"ID":"varlink-transient-unknown-service.service","Service":{"Type":"oneshot","Restart":"always","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
echo "$unsupported_service" | grep "io.systemd.Unit.PropertyNotSupported"
echo "$unsupported_service" | grep "Service.Restart"
# Same for a Kill field
defer_transient_cleanup varlink-transient-unknown-kill.service
unsupported_kill=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
'{"context":{"ID":"varlink-transient-unknown-kill.service","Kill":{"KillSignal":"SIGINT"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
echo "$unsupported_kill" | grep "io.systemd.Unit.PropertyNotSupported"
echo "$unsupported_kill" | grep "Kill.KillSignal"
set -o pipefail
transient_cleanup