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=*
This commit is contained in:
Lennart Poettering
2026-06-19 23:05:36 +02:00
parent 4552dce3b5
commit 9afeae5e8a
4 changed files with 83 additions and 9 deletions
+5 -2
View File
@@ -189,8 +189,11 @@
purposes, for example to identify the role a machine plays in a deployment, the fleet or 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 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, must be 1…255 characters long and consist only of ASCII alphanumeric characters,
<literal>-</literal> and <literal>.</literal>. The tags are stored in the <varname>TAGS=</varname> <literal>-</literal>, <literal>.</literal> and <literal>=</literal>. A tag may optionally be
field of <filename>/etc/machine-info</filename>; see parameterized with a value, in the form
<literal><replaceable>key</replaceable>=<replaceable>value</replaceable></literal>, in which case the
same key may not be assigned more than one distinct value. The tags are stored in the
<varname>TAGS=</varname> field of <filename>/etc/machine-info</filename>; see
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for <citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details. They may also be matched against with the details. They may also be matched against with the
<varname>ConditionMachineTag=</varname>/<varname>AssertMachineTag=</varname> unit settings, see <varname>ConditionMachineTag=</varname>/<varname>AssertMachineTag=</varname> unit settings, see
+17 -2
View File
@@ -149,7 +149,22 @@
<literal>TAGS=webserver:frontend:berlin</literal>.</para> <literal>TAGS=webserver:frontend:berlin</literal>.</para>
<para>Each individual tag must be 1…255 characters long and may consist only of the ASCII <para>Each individual tag must be 1…255 characters long and may consist only of the ASCII
alphanumeric characters, <literal>-</literal> and <literal>.</literal>.</para> alphanumeric characters, <literal>-</literal>, <literal>.</literal> and <literal>=</literal>. The
first character may not be <literal>-</literal>, <literal>.</literal> or <literal>=</literal>, and
the last character may not be <literal>-</literal> or <literal>.</literal> (unless it takes the
parameterized form, see below).</para>
<para>A tag may optionally be parameterized with a value, in the form
<literal><replaceable>key</replaceable>=<replaceable>value</replaceable></literal>. The first
<literal>=</literal> separates the key from the value; any further <literal>=</literal> characters
are part of the value. The key (the part before the first <literal>=</literal>) follows the same
restrictions as an unparameterized tag, in particular it may not be empty and may not end in
<literal>-</literal> or <literal>.</literal>. The value (the part after the first
<literal>=</literal>) may be empty and is otherwise unrestricted within the allowed character set.
Example: <literal>TAGS=role=webserver:env=production:berlin</literal>. The same key may not be
assigned more than one distinct value: <literal>role=webserver:role=database</literal> is refused
(but a key may coexist with the corresponding unparameterized tag, e.g.
<literal>role:role=webserver</literal>).</para>
<para>The configured tags may be matched against with the <para>The configured tags may be matched against with the
<varname>ConditionMachineTag=</varname> and <varname>AssertMachineTag=</varname> unit settings, see <varname>ConditionMachineTag=</varname> and <varname>AssertMachineTag=</varname> unit settings, see
@@ -217,7 +232,7 @@
ICON_NAME=computer-tablet ICON_NAME=computer-tablet
CHASSIS=tablet CHASSIS=tablet
DEPLOYMENT=production DEPLOYMENT=production
TAGS=demo:berlin</programlisting> TAGS=demo:berlin:role=webserver</programlisting>
</refsect1> </refsect1>
<refsect1> <refsect1>
+11
View File
@@ -2083,6 +2083,17 @@
negated by prepending an exclamation mark, in which case it is satisfied if none of the configured negated by prepending an exclamation mark, in which case it is satisfied if none of the configured
tags matches.</para> tags matches.</para>
<para>Tags may be parameterized with a value in the form
<literal><replaceable>key</replaceable>=<replaceable>value</replaceable></literal>; the
<literal>=</literal> and the value are part of the tag and thus part of the string the pattern is
matched against. Hence <literal>ConditionMachineTag=role=webserver</literal> matches the tag
<literal>role=webserver</literal> exactly, <literal>ConditionMachineTag=role=*</literal> matches any
value assigned to the <literal>role</literal> key, and <literal>ConditionMachineTag=role</literal>
(without <literal>=</literal>) does <emphasis>not</emphasis> match <literal>role=webserver</literal>.
See
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for the precise syntax of machine tags.</para>
<xi:include href="version-info.xml" xpointer="v261"/> <xi:include href="version-info.xml" xpointer="v261"/>
</listitem> </listitem>
</varlistentry> </varlistentry>
+50 -5
View File
@@ -259,13 +259,26 @@ bool machine_tag_is_valid(const char *s) {
if (n <= 0 || n >= 256) if (n <= 0 || n >= 256)
return false; return false;
/* Don't allow "-" and "." as first or last char. (This is load-bearing, we want that "+"/"-" can be /* Don't allow "-" and "." as first char. (This is load-bearing, we want that "+"/"-" can be used as
* used as prefix for adding/removing tags from the list). */ * prefix for adding/removing tags from the list). */
if (strchr("-.", s[0]) || if (strchr("-.=", s[0]))
strchr("-.", s[n-1]))
return false; 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) { 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)) if (!machine_tag_is_valid(*i))
return false; 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; return true;
@@ -320,6 +350,21 @@ int machine_tags_from_string(const char *s, bool graceful, char ***ret) {
if (n > MACHINE_TAGS_MAX) if (n > MACHINE_TAGS_MAX)
return -E2BIG; 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); r = strv_extend(&cleaned, *i);
if (r < 0) if (r < 0)
return r; return r;