lib/deploy: Add support for overlay initrds
authorJonathan Lebon <jonathan@jlebon.com>
Mon, 17 Aug 2020 13:48:18 +0000 (09:48 -0400)
committerJonathan Lebon <jonathan@jlebon.com>
Wed, 30 Sep 2020 17:29:32 +0000 (13:29 -0400)
In FCOS and RHCOS, the need to configure software in the initramfs has
come up multiple times. Sometimes, using kernel arguments suffices.
Other times, it really must be a configuration file. Rebuilding the
initramfs on the client-side however is a costly operation. Not only
does it add complexity to the update workflow, it also erodes a lot of
the value obtained from using the baked "blessed" initramfs from the
tree itself.

One elegant way to address this is to allow specifying multiple
initramfses. This is supported by most bootloaders (notably GRUB) and
results in each initrd being overlayed on top of each other.

This patch allows libostree clients to leverage this so that they can
avoid regenerating the initramfs entirely. libostree itself is agnostic
as to what kind and how much data overlay initrds contain. It's up to
the clients to enforce such boundaries.

To implement this, we add a new ostree_sysroot_stage_overlay_initrd
which takes a file descriptor and returns a checksum. Then users can
pass these checksums when calling the deploy APIs via the new array
option `overlay_initrds`. We copy these files into `/boot` and add them
to the BLS as another `initrd` entry.

apidoc/ostree-sections.txt
src/libostree/libostree-devel.sym
src/libostree/ostree-deployment-private.h
src/libostree/ostree-deployment.c
src/libostree/ostree-sysroot-cleanup.c
src/libostree/ostree-sysroot-deploy.c
src/libostree/ostree-sysroot-private.h
src/libostree/ostree-sysroot.c
src/libostree/ostree-sysroot.h
src/ostree/ot-admin-builtin-deploy.c
tests/kolainst/destructive/overlay-initrds.sh [new file with mode: 0755]

index 4ef396e62fbc94d633082a9da31f26dfd7aba57e..c1d6b35ef924a916dfadfa317ebe4bcbb81c0a46 100644 (file)
@@ -548,6 +548,7 @@ ostree_sysroot_write_deployments_with_options
 ostree_sysroot_write_origin_file
 ostree_sysroot_stage_tree
 ostree_sysroot_stage_tree_with_options
+ostree_sysroot_stage_overlay_initrd
 ostree_sysroot_deploy_tree
 ostree_sysroot_deploy_tree_with_options
 ostree_sysroot_get_merge_deployment
index 3f59c399b1531451b5aede3543bdce33875d9449..341a22a8fe278b10b0301dc3dcd40dab23d43388 100644 (file)
@@ -28,6 +28,7 @@ global:
   ostree_bootconfig_parser_set_overlay_initrds;
   ostree_sysroot_deploy_tree_with_options;
   ostree_sysroot_stage_tree_with_options;
+  ostree_sysroot_stage_overlay_initrd;
 } LIBOSTREE_2020.4;
 
 /* Stub section for the stable release *after* this development one; don't
index ad77317d79399230607b6b8ee09a09e49d4ef38a..b339ae2618449cadd2f686e5d30c103b2be553f1 100644 (file)
@@ -37,6 +37,8 @@ G_BEGIN_DECLS
  * @origin: How to construct an upgraded version of this tree
  * @unlocked: The unlocked state
  * @staged: TRUE iff this deployment is staged
+ * @overlay_initrds: Checksums of staged additional initrds for this deployment
+ * @overlay_initrds_id: Unique ID generated from initrd checksums; used to compare deployments
  */
 struct _OstreeDeployment
 {
@@ -52,8 +54,15 @@ struct _OstreeDeployment
   GKeyFile *origin;
   OstreeDeploymentUnlockedState unlocked;
   gboolean staged;
+  char **overlay_initrds;
+  char *overlay_initrds_id;
 };
 
 void _ostree_deployment_set_bootcsum (OstreeDeployment *self, const char *bootcsum);
 
+void _ostree_deployment_set_overlay_initrds (OstreeDeployment *self,
+                                             char **overlay_initrds);
+
+char** _ostree_deployment_get_overlay_initrds (OstreeDeployment *self);
+
 G_END_DECLS
index 70e1bc4928e29eda3c7b71b925430bd458047370..182bceea9fdb1f0c86da0c9df7f30af9670b5514 100644 (file)
@@ -158,6 +158,34 @@ _ostree_deployment_set_bootcsum (OstreeDeployment *self,
   self->bootcsum = g_strdup (bootcsum);
 }
 
+void
+_ostree_deployment_set_overlay_initrds (OstreeDeployment *self,
+                                        char            **overlay_initrds)
+{
+  g_clear_pointer (&self->overlay_initrds, g_strfreev);
+  g_clear_pointer (&self->overlay_initrds_id, g_free);
+
+  if (!overlay_initrds || g_strv_length (overlay_initrds) == 0)
+    return;
+
+  /* Generate a unique ID representing this combination of overlay initrds. This is so that
+   * ostree_sysroot_write_deployments_with_options() can easily compare initrds when
+   * comparing deployments for whether a bootswap is necessary. We could be fancier here but
+   * meh... this works. */
+  g_autoptr(GString) id = g_string_new (NULL);
+  for (char **it = overlay_initrds; it && *it; it++)
+    g_string_append (id, *it);
+
+  self->overlay_initrds = g_strdupv (overlay_initrds);
+  self->overlay_initrds_id = g_string_free (g_steal_pointer (&id), FALSE);
+}
+
+char**
+_ostree_deployment_get_overlay_initrds (OstreeDeployment *self)
+{
+  return self->overlay_initrds;
+}
+
 /**
  * ostree_deployment_clone:
  * @self: Deployment
@@ -175,6 +203,8 @@ ostree_deployment_clone (OstreeDeployment *self)
   new_bootconfig = ostree_bootconfig_parser_clone (self->bootconfig);
   ostree_deployment_set_bootconfig (ret, new_bootconfig);
 
+  _ostree_deployment_set_overlay_initrds (ret, self->overlay_initrds);
+
   if (self->origin)
     {
       g_autoptr(GKeyFile) new_origin = NULL;
@@ -238,6 +268,8 @@ ostree_deployment_finalize (GObject *object)
   g_free (self->bootcsum);
   g_clear_object (&self->bootconfig);
   g_clear_pointer (&self->origin, g_key_file_unref);
+  g_strfreev (self->overlay_initrds);
+  g_free (self->overlay_initrds_id);
 
   G_OBJECT_CLASS (ostree_deployment_parent_class)->finalize (object);
 }
index ffad4130809609ead25c1a97f0e4bcc1857a8bb8..27122834054b8b493d8aa85b3bc63d44aa02feec 100644 (file)
@@ -298,6 +298,8 @@ cleanup_old_deployments (OstreeSysroot       *self,
     g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
   g_autoptr(GHashTable) active_boot_checksums =
     g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+  g_autoptr(GHashTable) active_overlay_initrds =
+    g_hash_table_new (g_str_hash, g_str_equal); /* borrows from deployment's bootconfig */
   for (guint i = 0; i < self->deployments->len; i++)
     {
       OstreeDeployment *deployment = self->deployments->pdata[i];
@@ -306,6 +308,11 @@ cleanup_old_deployments (OstreeSysroot       *self,
       /* Transfer ownership */
       g_hash_table_replace (active_deployment_dirs, deployment_path, deployment_path);
       g_hash_table_replace (active_boot_checksums, bootcsum, bootcsum);
+
+      OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment);
+      char **initrds = ostree_bootconfig_parser_get_overlay_initrds (bootconfig);
+      for (char **it = initrds; it && *it; it++)
+        g_hash_table_add (active_overlay_initrds, (char*)glnx_basename (*it));
     }
 
   /* Find all deployment directories, both active and inactive */
@@ -349,6 +356,42 @@ cleanup_old_deployments (OstreeSysroot       *self,
         return FALSE;
     }
 
+  /* Clean up overlay initrds */
+  glnx_autofd int overlays_dfd =
+    glnx_opendirat_with_errno (self->sysroot_fd, _OSTREE_SYSROOT_INITRAMFS_OVERLAYS, FALSE);
+  if (overlays_dfd < 0)
+    {
+      if (errno != ENOENT)
+        return glnx_throw_errno_prefix (error, "open(initrd_overlays)");
+    }
+  else
+    {
+      g_autoptr(GPtrArray) initrds_to_delete = g_ptr_array_new_with_free_func (g_free);
+      g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
+      if (!glnx_dirfd_iterator_init_at (overlays_dfd, ".", TRUE, &dfd_iter, error))
+        return FALSE;
+      while (TRUE)
+        {
+          struct dirent *dent;
+          if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&dfd_iter, &dent, cancellable, error))
+            return FALSE;
+          if (dent == NULL)
+            break;
+
+          /* there shouldn't be other file types there, but let's be conservative */
+          if (dent->d_type != DT_REG)
+            continue;
+
+          if (!g_hash_table_lookup (active_overlay_initrds, dent->d_name))
+            g_ptr_array_add (initrds_to_delete, g_strdup (dent->d_name));
+        }
+      for (guint i = 0; i < initrds_to_delete->len; i++)
+        {
+          if (!ot_ensure_unlinked_at (overlays_dfd, initrds_to_delete->pdata[i], error))
+            return FALSE;
+        }
+    }
+
   return TRUE;
 }
 
index 9425316f8fa903de900cbd52b7f059fe08095323..1c4fb5dc8b964e74837c26aeea57605249c941de 100644 (file)
@@ -1859,6 +1859,47 @@ install_deployment_kernel (OstreeSysroot   *sysroot,
         }
     }
 
+  g_autoptr(GPtrArray) overlay_initrds = NULL;
+  for (char **it = _ostree_deployment_get_overlay_initrds (deployment); it && *it; it++)
+    {
+      char *checksum = *it;
+
+      /* Overlay initrds are not part of the bootcsum dir; they're not part of the tree
+       * proper. Instead they're in /boot/ostree/initramfs-overlays/ named by their csum.
+       * Doing it this way allows sharing the same bootcsum dir for multiple deployments
+       * with the only change being in overlay initrds (or conversely, the same overlay
+       * across different boocsums). Eventually, it'd be nice to have an OSTree repo in
+       * /boot itself and drop the boocsum dir concept entirely. */
+
+      g_autofree char *destpath =
+        g_strdup_printf ("/" _OSTREE_SYSROOT_BOOT_INITRAMFS_OVERLAYS "/%s.img", checksum);
+      const char *rel_destpath = destpath + 1;
+
+      /* lazily allocate array and create dir so we don't pollute /boot if not needed */
+      if (overlay_initrds == NULL)
+        {
+          overlay_initrds = g_ptr_array_new_with_free_func (g_free);
+
+          if (!glnx_shutil_mkdir_p_at (boot_dfd, _OSTREE_SYSROOT_BOOT_INITRAMFS_OVERLAYS,
+                                       0755, cancellable, error))
+            return FALSE;
+        }
+
+      if (!glnx_fstatat_allow_noent (boot_dfd, rel_destpath, NULL, 0, error))
+        return FALSE;
+      if (errno == ENOENT)
+        {
+          g_autofree char *srcpath =
+            g_strdup_printf (_OSTREE_SYSROOT_RUNSTATE_STAGED_INITRDS_DIR "/%s", checksum);
+          if (!install_into_boot (repo, sepolicy, AT_FDCWD, srcpath, boot_dfd, rel_destpath,
+                                  cancellable, error))
+            return FALSE;
+        }
+
+      /* these are used lower down to populate the bootconfig */
+      g_ptr_array_add (overlay_initrds, g_steal_pointer (&destpath));
+    }
+
   g_autofree char *contents = NULL;
   if (!glnx_fstatat_allow_noent (deployment_dfd, "usr/lib/os-release", &stbuf, 0, error))
     return FALSE;
@@ -1938,6 +1979,12 @@ install_deployment_kernel (OstreeSysroot   *sysroot,
       g_autofree char * initrd_boot_relpath =
         g_strconcat ("/", bootcsumdir, "/", kernel_layout->initramfs_namever, NULL);
       ostree_bootconfig_parser_set (bootconfig, "initrd", initrd_boot_relpath);
+
+      if (overlay_initrds)
+        {
+          g_ptr_array_add (overlay_initrds, NULL);
+          ostree_bootconfig_parser_set_overlay_initrds (bootconfig, (char**)overlay_initrds->pdata);
+        }
     }
   else
     {
@@ -2135,6 +2182,10 @@ deployment_bootconfigs_equal (OstreeRepo       *repo,
   if (strcmp (a_bootcsum, b_bootcsum) != 0)
     return FALSE;
 
+  /* same initrd overlays? */
+  if (g_strcmp0 (a->overlay_initrds_id, b->overlay_initrds_id) != 0)
+    return FALSE;
+
   /* same kargs? */
   g_autofree char *a_boot_options_without_ostree = get_deployment_nonostree_kargs (a);
   g_autofree char *b_boot_options_without_ostree = get_deployment_nonostree_kargs (b);
@@ -2722,6 +2773,7 @@ sysroot_initialize_deployment (OstreeSysroot     *self,
 
   _ostree_deployment_set_bootcsum (new_deployment, kernel_layout->bootcsum);
   _ostree_deployment_set_bootconfig_from_kargs (new_deployment, opts ? opts->override_kernel_argv : NULL);
+  _ostree_deployment_set_overlay_initrds (new_deployment, opts ? opts->overlay_initrds : NULL);
 
   if (!prepare_deployment_etc (self, repo, new_deployment, deployment_dfd,
                                cancellable, error))
@@ -2991,6 +3043,63 @@ _ostree_sysroot_deserialize_deployment_from_variant (GVariant *v,
 }
 
 
+/**
+ * ostree_sysroot_stage_overlay_initrd:
+ * @self: Sysroot
+ * @fd: (transfer none): File descriptor to overlay initrd
+ * @out_checksum: (out) (transfer full): Overlay initrd checksum
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * Stage an overlay initrd to be used in an upcoming deployment. Returns a checksum which
+ * can be passed to ostree_sysroot_deploy_tree_with_options() or
+ * ostree_sysroot_stage_tree_with_options() via the `overlay_initrds` array option.
+ *
+ * Since: 2020.7
+ */
+gboolean
+ostree_sysroot_stage_overlay_initrd (OstreeSysroot  *self,
+                                     int             fd,
+                                     char          **out_checksum,
+                                     GCancellable   *cancellable,
+                                     GError        **error)
+{
+  g_return_val_if_fail (fd != -1, FALSE);
+  g_return_val_if_fail (out_checksum != NULL, FALSE);
+
+  if (!glnx_shutil_mkdir_p_at (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED_INITRDS_DIR,
+                               0755, cancellable, error))
+    return FALSE;
+
+  glnx_autofd int staged_initrds_dfd = -1;
+  if (!glnx_opendirat (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED_INITRDS_DIR, FALSE,
+                       &staged_initrds_dfd, error))
+    return FALSE;
+
+  g_auto(GLnxTmpfile) overlay_initrd = { 0, };
+  if (!glnx_open_tmpfile_linkable_at (staged_initrds_dfd, ".", O_WRONLY | O_CLOEXEC,
+                                      &overlay_initrd, error))
+    return FALSE;
+
+  char checksum[_OSTREE_SHA256_STRING_LEN+1];
+  {
+    g_autoptr(GOutputStream) output = g_unix_output_stream_new (overlay_initrd.fd, FALSE);
+    g_autoptr(GInputStream) input = g_unix_input_stream_new (fd, FALSE);
+    g_autofree guchar *digest = NULL;
+    if (!ot_gio_splice_get_checksum (output, input, &digest, cancellable, error))
+      return FALSE;
+    ot_bin2hex (checksum, (guint8*)digest, _OSTREE_SHA256_DIGEST_LEN);
+  }
+
+  if (!glnx_link_tmpfile_at (&overlay_initrd, GLNX_LINK_TMPFILE_REPLACE,
+                             staged_initrds_dfd, checksum, error))
+    return FALSE;
+
+  *out_checksum = g_strdup (checksum);
+  return TRUE;
+}
+
+
 /**
  * ostree_sysroot_stage_tree:
  * @self: Sysroot
@@ -3122,6 +3231,9 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot     *self,
   if (opts && opts->override_kernel_argv)
     g_variant_builder_add (builder, "{sv}", "kargs",
                            g_variant_new_strv ((const char *const*)opts->override_kernel_argv, -1));
+  if (opts && opts->overlay_initrds)
+    g_variant_builder_add (builder, "{sv}", "overlay-initrds",
+                           g_variant_new_strv ((const char *const*)opts->overlay_initrds, -1));
 
   const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED));
   if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error))
index 1af2fd27caf21bf60ac9856655030fd5254c657a..318b0b199ef7769397d5f441a8ad20d9a776bed1 100644 (file)
@@ -84,10 +84,14 @@ struct OstreeSysroot {
 /* We keep some transient state in /run */
 #define _OSTREE_SYSROOT_RUNSTATE_STAGED "/run/ostree/staged-deployment"
 #define _OSTREE_SYSROOT_RUNSTATE_STAGED_LOCKED "/run/ostree/staged-deployment-locked"
+#define _OSTREE_SYSROOT_RUNSTATE_STAGED_INITRDS_DIR "/run/ostree/staged-initrds/"
 #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/"
 #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development"
 #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_TRANSIENT "unlocked-transient"
 
+#define _OSTREE_SYSROOT_BOOT_INITRAMFS_OVERLAYS "ostree/initramfs-overlays"
+#define _OSTREE_SYSROOT_INITRAMFS_OVERLAYS "boot/" _OSTREE_SYSROOT_BOOT_INITRAMFS_OVERLAYS
+
 gboolean
 _ostree_sysroot_ensure_writable (OstreeSysroot      *self,
                                  GError            **error);
index e412ea4da73c774e866c91596eba0176bc9030e2..e0813b554432b1443bc740c44e825f1def6e0c60 100644 (file)
@@ -815,6 +815,24 @@ list_deployments_process_one_boot_entry (OstreeSysroot               *self,
     return FALSE;
 
   ostree_deployment_set_bootconfig (deployment, config);
+  char **overlay_initrds = ostree_bootconfig_parser_get_overlay_initrds (config);
+  g_autoptr(GPtrArray) initrds_chksums = NULL;
+  for (char **it = overlay_initrds; it && *it; it++)
+    {
+      const char *basename = glnx_basename (*it);
+      if (strlen (basename) != (_OSTREE_SHA256_STRING_LEN + strlen (".img")))
+        return glnx_throw (error, "Malformed overlay initrd filename: %s", basename);
+
+      if (!initrds_chksums) /* lazy init */
+        initrds_chksums = g_ptr_array_new_full (g_strv_length (overlay_initrds), g_free);
+      g_ptr_array_add (initrds_chksums, g_strndup (basename, _OSTREE_SHA256_STRING_LEN));
+    }
+
+  if (initrds_chksums)
+    {
+      g_ptr_array_add (initrds_chksums, NULL);
+      _ostree_deployment_set_overlay_initrds (deployment, (char**)initrds_chksums->pdata);
+    }
 
   g_ptr_array_add (inout_deployments, g_object_ref (deployment));
   return TRUE;
@@ -967,8 +985,10 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self,
       /* Parse it */
       g_autoptr(GVariant) target = NULL;
       g_autofree char **kargs = NULL;
+      g_autofree char **overlay_initrds = NULL;
       g_variant_dict_lookup (staged_deployment_dict, "target", "@a{sv}", &target);
       g_variant_dict_lookup (staged_deployment_dict, "kargs", "^a&s", &kargs);
+      g_variant_dict_lookup (staged_deployment_dict, "overlay-initrds", "^a&s", &overlay_initrds);
       if (target)
         {
           g_autoptr(OstreeDeployment) staged =
@@ -980,6 +1000,8 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self,
           if (!load_origin (self, staged, NULL, error))
             return FALSE;
 
+          _ostree_deployment_set_overlay_initrds (staged, overlay_initrds);
+
           self->staged_deployment = g_steal_pointer (&staged);
           self->staged_deployment_data = g_steal_pointer (&staged_deployment_data);
           /* We set this flag for ostree_deployment_is_staged() because that API
index 45d6d63cc4573c85f393c73c3245569cfd45d953..3a3b6a774a55be2479329c0315abbe113096420d 100644 (file)
@@ -186,11 +186,19 @@ gboolean ostree_sysroot_write_deployments_with_options (OstreeSysroot     *self,
                                                         GCancellable      *cancellable,
                                                         GError           **error);
 
+_OSTREE_PUBLIC
+gboolean ostree_sysroot_stage_overlay_initrd (OstreeSysroot  *self,
+                                              int             fd,
+                                              char          **out_checksum,
+                                              GCancellable   *cancellable,
+                                              GError        **error);
+
 typedef struct {
   gboolean unused_bools[8];
   int unused_ints[8];
   char **override_kernel_argv;
-  gpointer unused_ptrs[7];
+  char **overlay_initrds;
+  gpointer unused_ptrs[6];
 } OstreeSysrootDeployTreeOpts;
 
 _OSTREE_PUBLIC
index bcece3f6559e4e7fcab0839eaa5129c474144a39..8156cc153c8c46a68eab5f7b15ea2c523ccc1489 100644 (file)
@@ -44,6 +44,7 @@ static gboolean opt_kernel_proc_cmdline;
 static char *opt_osname;
 static char *opt_origin_path;
 static gboolean opt_kernel_arg_none;
+static char **opt_overlay_initrds;
 
 static GOptionEntry options[] = {
   { "os", 0, 0, G_OPTION_ARG_STRING, &opt_osname, "Use a different operating system root than the current one", "OSNAME" },
@@ -59,6 +60,7 @@ static GOptionEntry options[] = {
   { "karg", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_kernel_argv, "Set kernel argument, like root=/dev/sda1; this overrides any earlier argument with the same name", "NAME=VALUE" },
   { "karg-append", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_kernel_argv_append, "Append kernel argument; useful with e.g. console= that can be used multiple times", "NAME=VALUE" },
   { "karg-none", 0, 0, G_OPTION_ARG_NONE, &opt_kernel_arg_none, "Do not import kernel arguments", NULL },
+  { "overlay-initrd", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_overlay_initrds, "Overlay iniramfs file", "FILE" },
   { NULL }
 };
 
@@ -167,24 +169,76 @@ ot_admin_builtin_deploy (int argc, char **argv, OstreeCommandInvocation *invocat
       ostree_kernel_args_append_argv (kargs, opt_kernel_argv_append);
     }
 
-  g_autoptr(OstreeDeployment) new_deployment = NULL;
+  g_autoptr(GPtrArray) overlay_initrd_chksums = NULL;
+  for (char **it = opt_overlay_initrds; it && *it; it++)
+    {
+      const char *path = *it;
+
+      glnx_autofd int fd = -1;
+      if (!glnx_openat_rdonly (AT_FDCWD, path, TRUE, &fd, error))
+        return FALSE;
+
+      g_autofree char *chksum = NULL;
+      if (!ostree_sysroot_stage_overlay_initrd (sysroot, fd, &chksum, cancellable, error))
+        return FALSE;
+
+      if (!overlay_initrd_chksums)
+        overlay_initrd_chksums = g_ptr_array_new_full (g_strv_length (opt_overlay_initrds), g_free);
+      g_ptr_array_add (overlay_initrd_chksums, g_steal_pointer (&chksum));
+    }
+
+  if (overlay_initrd_chksums)
+    g_ptr_array_add (overlay_initrd_chksums, NULL);
+
   g_auto(GStrv) kargs_strv = kargs ? ostree_kernel_args_to_strv (kargs) : NULL;
+
+  OstreeSysrootDeployTreeOpts opts = {
+    .override_kernel_argv = kargs_strv,
+    .overlay_initrds = overlay_initrd_chksums ? (char**)overlay_initrd_chksums->pdata : NULL,
+  };
+
+  g_autoptr(OstreeDeployment) new_deployment = NULL;
   if (opt_stage)
     {
       if (opt_retain_pending || opt_retain_rollback)
         return glnx_throw (error, "--stage cannot currently be combined with --retain arguments");
       if (opt_not_as_default)
         return glnx_throw (error, "--stage cannot currently be combined with --not-as-default");
-      if (!ostree_sysroot_stage_tree (sysroot, opt_osname, revision, origin, merge_deployment,
-                                      kargs_strv, &new_deployment, cancellable, error))
-        return FALSE;
+      /* use old API if we can to exercise it in CI */
+      if (!overlay_initrd_chksums)
+        {
+          if (!ostree_sysroot_stage_tree (sysroot, opt_osname, revision, origin,
+                                          merge_deployment, kargs_strv, &new_deployment,
+                                          cancellable, error))
+            return FALSE;
+        }
+      else
+        {
+          if (!ostree_sysroot_stage_tree_with_options (sysroot, opt_osname, revision,
+                                                       origin, merge_deployment, &opts,
+                                                       &new_deployment, cancellable, error))
+            return FALSE;
+        }
       g_assert (new_deployment);
     }
   else
     {
-      if (!ostree_sysroot_deploy_tree (sysroot, opt_osname, revision, origin, merge_deployment,
-                                       kargs_strv, &new_deployment, cancellable, error))
-        return FALSE;
+      /* use old API if we can to exercise it in CI */
+      if (!overlay_initrd_chksums)
+        {
+          if (!ostree_sysroot_deploy_tree (sysroot, opt_osname, revision, origin,
+                                           merge_deployment, kargs_strv, &new_deployment,
+                                           cancellable, error))
+            return FALSE;
+        }
+      else
+        {
+          if (!ostree_sysroot_deploy_tree_with_options (sysroot, opt_osname, revision,
+                                                        origin, merge_deployment, &opts,
+                                                        &new_deployment, cancellable,
+                                                        error))
+            return FALSE;
+        }
       g_assert (new_deployment);
 
       OstreeSysrootSimpleWriteDeploymentFlags flags = OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NO_CLEAN;
diff --git a/tests/kolainst/destructive/overlay-initrds.sh b/tests/kolainst/destructive/overlay-initrds.sh
new file mode 100755 (executable)
index 0000000..b24d2d0
--- /dev/null
@@ -0,0 +1,96 @@
+#!/bin/bash
+# https://github.com/ostreedev/ostree/issues/1667
+set -xeuo pipefail
+
+. ${KOLA_EXT_DATA}/libinsttest.sh
+
+# we don't just use `rpm-ostree initramfs-etc` here because we want to be able
+# to test more things
+
+# dracut prints all the cmdline args, including those from /etc/cmdline.d, so
+# the way we test that an initrd was included is to just add kargs there and
+# grep for it
+create_initrd_with_dracut_karg() {
+  local karg=$1; shift
+  local d
+  d=$(mktemp -dp /var/tmp)
+  mkdir -p "${d}/etc/cmdline.d"
+  echo "${karg}" > "${d}/etc/cmdline.d/${karg}.conf"
+  echo "etc/cmdline.d/${karg}.conf" | \
+    cpio -D "${d}" -o -H newc --reproducible > "/var/tmp/${karg}.img"
+}
+
+check_for_dracut_karg() {
+  local karg=$1; shift
+  # https://github.com/dracutdevs/dracut/blob/38ea7e821b/modules.d/98dracut-systemd/dracut-cmdline.sh#L17
+  journalctl -b 0 -t dracut-cmdline \
+    --grep "Using kernel command line parameters:.* ${karg} "
+}
+
+case "${AUTOPKGTEST_REBOOT_MARK:-}" in
+  "")
+    create_initrd_with_dracut_karg ostree.test1
+    # let's use the deploy API first
+    ostree admin deploy "${host_refspec}" \
+      --overlay-initrd /var/tmp/ostree.test1.img
+    /tmp/autopkgtest-reboot "2"
+    ;;
+  2)
+    # verify that ostree.test1 is here
+    check_for_dracut_karg ostree.test1
+    img_sha=$(sha256sum < /var/tmp/ostree.test1.img | cut -f 1 -d ' ')
+    test -f "/boot/ostree/initramfs-overlays/${img_sha}.img"
+
+    # now let's change to ostree.test2
+    create_initrd_with_dracut_karg ostree.test2
+
+    # let's use the staging API this time
+    ostree admin deploy "${host_refspec}" --stage \
+      --overlay-initrd /var/tmp/ostree.test2.img
+    /tmp/autopkgtest-reboot "3"
+    ;;
+  3)
+    # verify that ostree.test1 is gone, but ostree.test2 is here
+    if check_for_dracut_karg ostree.test1; then
+      assert_not_reached "Unexpected ostree.test1 karg found"
+    fi
+    check_for_dracut_karg ostree.test2
+
+    # both the new and old initrds should still be there since they're
+    # referenced in the BLS
+    test1_sha=$(sha256sum < /var/tmp/ostree.test1.img | cut -f 1 -d ' ')
+    test2_sha=$(sha256sum < /var/tmp/ostree.test2.img | cut -f 1 -d ' ')
+    test -f "/boot/ostree/initramfs-overlays/${test1_sha}.img"
+    test -f "/boot/ostree/initramfs-overlays/${test2_sha}.img"
+
+    # OK, now let's deploy an identical copy of this test
+    ostree admin deploy "${host_refspec}" \
+      --overlay-initrd /var/tmp/ostree.test2.img
+
+    # Now the deployment with ostree.test1 should've been GC'ed; check that its
+    # initrd was cleaned up
+    test ! -f "/boot/ostree/initramfs-overlays/${test1_sha}.img"
+    test -f "/boot/ostree/initramfs-overlays/${test2_sha}.img"
+
+    # deploy again to check that no bootconfig swap was needed; this verifies
+    # that deployment overlay initrds can be successfully compared
+    ostree admin deploy "${host_refspec}" \
+      --overlay-initrd /var/tmp/ostree.test2.img |& tee /tmp/out.txt
+    assert_file_has_content /tmp/out.txt 'bootconfig swap: no'
+
+    # finally, let's check that we can overlay multiple initrds
+    ostree admin deploy "${host_refspec}" --stage \
+      --overlay-initrd /var/tmp/ostree.test1.img \
+      --overlay-initrd /var/tmp/ostree.test2.img
+    /tmp/autopkgtest-reboot "4"
+    ;;
+  4)
+    check_for_dracut_karg ostree.test1
+    check_for_dracut_karg ostree.test2
+    test1_sha=$(sha256sum < /var/tmp/ostree.test1.img | cut -f 1 -d ' ')
+    test2_sha=$(sha256sum < /var/tmp/ostree.test2.img | cut -f 1 -d ' ')
+    test -f "/boot/ostree/initramfs-overlays/${test1_sha}.img"
+    test -f "/boot/ostree/initramfs-overlays/${test2_sha}.img"
+    ;;
+  *) fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}" ;;
+esac