deploy: Support an empty `/etc` and populated `/usr/etc`
authorColin Walters <walters@verbum.org>
Sat, 29 Jul 2023 12:47:47 +0000 (08:47 -0400)
committerColin Walters <walters@verbum.org>
Sat, 29 Jul 2023 12:47:54 +0000 (08:47 -0400)
In preparation for support for a transient `/etc`:
https://github.com/ostreedev/ostree/issues/2868
particularly in combination with composefs.

Basically it's just much more elegant if we can directly mount
an overlayfs on the *empty* `etc` directory, using `usr/etc` as
the lower.

In the composefs case, we'd have to mount the composefs overlayfs
itself writable (and call `mkdir`) *just* so we can make that
empty `etc` directory which is ugly.

Makefile-tests.am
src/libostree/ostree-sysroot-deploy.c
tests/test-admin-deploy-emptyetc.sh [new file with mode: 0755]

index a9b866bf8b4bda6d7d42af1571cb6b20ab6d85fb..5544a6bf05c88e67c1133acb7aa04bfefa77c9cd 100644 (file)
@@ -114,6 +114,7 @@ _installed_or_uninstalled_test_scripts = \
        tests/test-admin-deploy-none.sh \
        tests/test-admin-deploy-bootid-gc.sh \
        tests/test-admin-deploy-whiteouts.sh \
+       tests/test-admin-deploy-emptyetc.sh \
        tests/test-osupdate-dtb.sh \
        tests/test-admin-instutil-set-kargs.sh \
        tests/test-admin-upgrade-not-backwards.sh \
index 974d23362f64116d8f482a975da167d2517b0ca7..72d729c3daa050e057c473af3ff4b0d1a3a693fb 100644 (file)
@@ -872,26 +872,80 @@ prepare_deployment_etc (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployme
 {
   GLNX_AUTO_PREFIX_ERROR ("Preparing /etc", error);
 
+  enum DirectoryState
+  {
+    DIRSTATE_NONEXISTENT,
+    DIRSTATE_EMPTY,
+    DIRSTATE_POPULATED,
+  };
+
+  enum DirectoryState etc_state;
+  {
+    gboolean exists = FALSE;
+    g_auto (GLnxDirFdIterator) dfd_iter = {
+      0,
+    };
+    if (!ot_dfd_iter_init_allow_noent (deployment_dfd, "etc", &dfd_iter, &exists, error))
+      return glnx_prefix_error (error, "Failed to stat etc in deployment");
+    if (!exists)
+      {
+        etc_state = DIRSTATE_NONEXISTENT;
+      }
+    else
+      {
+        struct dirent *dent;
+        if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, NULL, error))
+          return FALSE;
+        if (dent)
+          etc_state = DIRSTATE_POPULATED;
+        else
+          etc_state = DIRSTATE_EMPTY;
+      }
+  }
   struct stat stbuf;
-  if (!glnx_fstatat_allow_noent (deployment_dfd, "etc", &stbuf, AT_SYMLINK_NOFOLLOW, error))
-    return FALSE;
-  gboolean etc_exists = (errno == 0);
   if (!glnx_fstatat_allow_noent (deployment_dfd, "usr/etc", &stbuf, AT_SYMLINK_NOFOLLOW, error))
     return FALSE;
   gboolean usretc_exists = (errno == 0);
 
-  if (etc_exists)
+  switch (etc_state)
     {
-      if (usretc_exists)
-        return glnx_throw (error, "Tree contains both /etc and /usr/etc");
-      /* Compatibility hack */
-      if (!glnx_renameat (deployment_dfd, "etc", deployment_dfd, "usr/etc", error))
-        return FALSE;
-      usretc_exists = TRUE;
+    case DIRSTATE_NONEXISTENT:
+      break;
+    case DIRSTATE_EMPTY:
+      {
+        if (usretc_exists)
+          {
+            /* For now it's actually simpler to just remove the empty directory
+             * and have a symmetrical code path.
+             */
+            if (unlinkat (deployment_dfd, "etc", AT_REMOVEDIR) < 0)
+              return glnx_throw_errno_prefix (error, "Failed to remove empty etc");
+            etc_state = DIRSTATE_NONEXISTENT;
+          }
+        /* Otherwise, there's no /etc or /usr/etc, we'll assume they know what they're doing... */
+      }
+      break;
+    case DIRSTATE_POPULATED:
+      {
+        if (usretc_exists)
+          {
+            return glnx_throw (error, "Tree contains both /etc and /usr/etc");
+          }
+        else
+          {
+            /* Compatibility hack */
+            if (!glnx_renameat (deployment_dfd, "etc", deployment_dfd, "usr/etc", error))
+              return FALSE;
+            etc_state = DIRSTATE_NONEXISTENT;
+            usretc_exists = TRUE;
+          }
+      }
+      break;
     }
 
   if (usretc_exists)
     {
+      g_assert (etc_state == DIRSTATE_NONEXISTENT);
       /* We need copies of /etc from /usr/etc (so admins can use vi), and if
        * SELinux is enabled, we need to relabel.
        */
diff --git a/tests/test-admin-deploy-emptyetc.sh b/tests/test-admin-deploy-emptyetc.sh
new file mode 100755 (executable)
index 0000000..8c96207
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.0+
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <https://www.gnu.org/licenses/>.
+
+set -euo pipefail
+
+. $(dirname $0)/libtest.sh
+
+setup_os_repository "archive" "syslinux"
+
+echo "1..1"
+cd ${test_tmpdir}/osdata
+mkdir etc
+${CMD_PREFIX} ostree --repo=${test_tmpdir}/testos-repo commit  --add-metadata-string "version=42.etc" -b testos/buildmain/x86_64-runtime
+cd -
+${CMD_PREFIX} ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmain/x86_64-runtime
+${CMD_PREFIX} ostree admin deploy --os=testos testos:testos/buildmain/x86_64-runtime
+origdeployment=$(${CMD_PREFIX} ostree admin --sysroot=sysroot --print-current-dir)
+assert_file_has_content ${origdeployment}/etc/NetworkManager/nm.conf "a default daemon file"
+echo "ok empty etc"