From: Lennart Poettering Date: Tue, 16 Jun 2026 13:53:56 +0000 (+0200) Subject: machine-tags: optionally support key/value pairs as machine tags X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9afeae5e8a8fd5deb75ea6e861e2fe02b247deb9;p=thirdparty%2Fsystemd.git machine-tags: optionally support key/value pairs as machine tags Other systems (kubernetes…) allow tagging machines with key/value pairs. Let's extend our allowed syntax slightly to allow that too. Thankfully, we enforced a pretty strict ruleset on machine tags, hence we can introduce this without breaking compatibility. This basically allows tags to contain "=". If so, then the left-hand side of it must be unique among machine tags. When matching against a machine tag, we apply the same rules as before. This means, that if people want to check if a tag with value applies they can do: ConditionMachineTag=foo=bar If they just want to check if "foo=" is set to anything, they can use the usual glob matching: ConditionMachineTag=foo=* --- diff --git a/man/hostnamectl.xml b/man/hostnamectl.xml index c120b938c93..4c1d3cf245e 100644 --- a/man/hostnamectl.xml +++ b/man/hostnamectl.xml @@ -189,8 +189,11 @@ purposes, for example to identify the role a machine plays in a deployment, the fleet or organizational unit it belongs to, or any other administrator-defined attribute. Each individual tag must be 1…255 characters long and consist only of ASCII alphanumeric characters, - - and .. The tags are stored in the TAGS= - field of /etc/machine-info; see + -, . and =. A tag may optionally be + parameterized with a value, in the form + key=value, in which case the + same key may not be assigned more than one distinct value. The tags are stored in the + TAGS= field of /etc/machine-info; see machine-info5 for details. They may also be matched against with the ConditionMachineTag=/AssertMachineTag= unit settings, see diff --git a/man/machine-info.xml b/man/machine-info.xml index 252f341bba6..6b2d92e7e5a 100644 --- a/man/machine-info.xml +++ b/man/machine-info.xml @@ -149,7 +149,22 @@ TAGS=webserver:frontend:berlin. Each individual tag must be 1…255 characters long and may consist only of the ASCII - alphanumeric characters, - and .. + alphanumeric characters, -, . and =. The + first character may not be -, . or =, and + the last character may not be - or . (unless it takes the + parameterized form, see below). + + A tag may optionally be parameterized with a value, in the form + key=value. The first + = separates the key from the value; any further = characters + are part of the value. The key (the part before the first =) follows the same + restrictions as an unparameterized tag, in particular it may not be empty and may not end in + - or .. The value (the part after the first + =) may be empty and is otherwise unrestricted within the allowed character set. + Example: TAGS=role=webserver:env=production:berlin. The same key may not be + assigned more than one distinct value: role=webserver:role=database is refused + (but a key may coexist with the corresponding unparameterized tag, e.g. + role:role=webserver). The configured tags may be matched against with the ConditionMachineTag= and AssertMachineTag= unit settings, see @@ -217,7 +232,7 @@ ICON_NAME=computer-tablet CHASSIS=tablet DEPLOYMENT=production -TAGS=demo:berlin +TAGS=demo:berlin:role=webserver diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 5d2f32dfcef..53de1791d2f 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -2083,6 +2083,17 @@ negated by prepending an exclamation mark, in which case it is satisfied if none of the configured tags matches. + Tags may be parameterized with a value in the form + key=value; the + = and the value are part of the tag and thus part of the string the pattern is + matched against. Hence ConditionMachineTag=role=webserver matches the tag + role=webserver exactly, ConditionMachineTag=role=* matches any + value assigned to the role key, and ConditionMachineTag=role + (without =) does not match role=webserver. + See + machine-info5 + for the precise syntax of machine tags. + diff --git a/src/basic/hostname-util.c b/src/basic/hostname-util.c index e12ec01af21..444c4e1af06 100644 --- a/src/basic/hostname-util.c +++ b/src/basic/hostname-util.c @@ -259,13 +259,26 @@ bool machine_tag_is_valid(const char *s) { if (n <= 0 || n >= 256) return false; - /* Don't allow "-" and "." as first or last char. (This is load-bearing, we want that "+"/"-" can be - * used as prefix for adding/removing tags from the list). */ - if (strchr("-.", s[0]) || - strchr("-.", s[n-1])) + /* Don't allow "-" and "." as first char. (This is load-bearing, we want that "+"/"-" can be used as + * prefix for adding/removing tags from the list). */ + if (strchr("-.=", s[0])) return false; - return in_charset(s, ALPHANUMERICAL "-."); + /* We allow parameterization of tags, with a "=" as separator */ + const char *eq = strchr(s, '='); + if (eq) { + assert(eq > s); + + /* If there is an '=', then make the same restrictions as for the first char on the last char before it */ + if (strchr("-.", eq[-1])) + return false; + } else { + /* If there's no '=', then make the restriction on the very last character */ + if (strchr("-.", s[n-1])) + return false; + } + + return in_charset(s, ALPHANUMERICAL "-.="); } bool machine_tag_list_is_valid(char **l) { @@ -277,6 +290,23 @@ bool machine_tag_list_is_valid(char **l) { if (!machine_tag_is_valid(*i)) return false; + + const char *eq = strchr(*i, '='); + if (!eq) + continue; + + /* Refuse tags with a common part before the '=', that do no also carry the same value. */ + size_t np = eq - *i + 1; + STRV_FOREACH(j, l) { + if (j == i) + break; + + if (streq(*i, *j)) /* Fully identical is OK */ + continue; + + if (strneq(*i, *j, np)) /* Not identical, but same key: refuse */ + return false; + } } return true; @@ -320,6 +350,21 @@ int machine_tags_from_string(const char *s, bool graceful, char ***ret) { if (n > MACHINE_TAGS_MAX) return -E2BIG; + const char *eq = strchr(*i, '='); + if (eq) { + /* Suppress duplicate assignments */ + bool skip = false; + size_t np = eq - *i + 1; + STRV_FOREACH(j, cleaned) + if (strneq(*i, *j, np)) { + skip = true; + break; + } + + if (skip) + continue; + } + r = strv_extend(&cleaned, *i); if (r < 0) return r;