]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
machine-tags: optionally support key/value pairs as machine tags
authorLennart Poettering <lennart@amutable.com>
Tue, 16 Jun 2026 13:53:56 +0000 (15:53 +0200)
committerLennart Poettering <lennart@amutable.com>
Fri, 19 Jun 2026 21:05:36 +0000 (23:05 +0200)
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=*

man/hostnamectl.xml
man/machine-info.xml
man/systemd.unit.xml
src/basic/hostname-util.c

index c120b938c937cfc986ed8d13aec13ca0928aa8d7..4c1d3cf245ec41cb49a7fe17c852f9493515693f 100644 (file)
         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,
-        <literal>-</literal> and <literal>.</literal>. The tags are stored in the <varname>TAGS=</varname>
-        field of <filename>/etc/machine-info</filename>; see
+        <literal>-</literal>, <literal>.</literal> and <literal>=</literal>. A tag may optionally be
+        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
         details. They may also be matched against with the
         <varname>ConditionMachineTag=</varname>/<varname>AssertMachineTag=</varname> unit settings, see
index 252f341bba655f0527c7d562b96c7fae434366a3..6b2d92e7e5a09c2d5886c707af518c5f3e55c92f 100644 (file)
           <literal>TAGS=webserver:frontend:berlin</literal>.</para>
 
           <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
           <varname>ConditionMachineTag=</varname> and <varname>AssertMachineTag=</varname> unit settings, see
 ICON_NAME=computer-tablet
 CHASSIS=tablet
 DEPLOYMENT=production
-TAGS=demo:berlin</programlisting>
+TAGS=demo:berlin:role=webserver</programlisting>
   </refsect1>
 
   <refsect1>
index 5d2f32dfcefac1bc03b8ab05a3b7fdb347bee9c4..53de1791d2fc5430f8be1501e3a3c3b31d91553e 100644 (file)
           negated by prepending an exclamation mark, in which case it is satisfied if none of the configured
           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"/>
           </listitem>
         </varlistentry>
index e12ec01af21c34b239320e07945d780f8bf639f1..444c4e1af06eddda1d1aac38819a9a189ab6b6ad 100644 (file)
@@ -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;