Repository: osbuild/osbuild
Pull request #1553 (2024-01-25 19:02:38)

ostree: add convenience function for using default OSTree deployment
This adds a `default: true` option for all cases where OSTree information is specified in schemas and allows for the information to be picked up from the filesystem.

This is a safe operation because when building disk images there is no known case where having two deployments makes sense. In the case there ever were a case then the osname, ref, and serial options still exist and can be used.

dustymabe: This obsoletes https://github.com/osbuild/osbuild/pull/1527
dustymabe: ok over in https://github.com/osbuild/osbuild/pull/1527#issuecomment-1880334815 we came up with something like this as a proposal:

> So the concensus is something like `OneOf`:
>
>
> ```json
> "deployment": {
>   "osname": ...,
>   "ref": ...
>   "serial": ... (default 0)
> }
> ```
> or
> ```json
> "default-deployment": true (default false)
> ```

When implementing that we thought it would be more appropriate for the new option to be under `deployment` and not a new top level property.  So that is what we came up with here where we now allow the user to specify something like:

```
"deployment": {
  "default": true
}
```

Since this was a change from what we talked about before I figured it'd be worth calling out and asking if that is an acceptable path forward.

dustymabe: ok rebased and did a new push. Hopefully CI passes now. 
dustymabe: rebased and fixed the merge conflict! 
dustymabe: > I attached a small patch with my suggestions and small tests as I was playing around a bit with the code but it can be a) a new PR _or_ b) ignored as it's just tweaks.
> 
> [0001-ostree-small-tweaks-for-parse_deployment_option.patch.txt](https://github.com/osbuild/osbuild/files/14192978/0001-ostree-small-tweaks-for-parse_deployment_option.patch.txt)

I applied this patch and marked you as Co-Author of the commit! 

Patch:
diff --git a/mounts/org.osbuild.ostree.deployment b/mounts/org.osbuild.ostree.deployment
index 24df7731..9637b5b2 100755
--- a/mounts/org.osbuild.ostree.deployment
+++ b/mounts/org.osbuild.ostree.deployment
@@ -41,7 +41,26 @@ SCHEMA_2 = """
       "deployment": {
         "type": "object",
         "additionalProperties": false,
-        "required": ["osname", "ref"],
+        "oneOf": [
+          {
+            "properties": {
+              "default": {"enum": [false]}
+            },
+            "required": ["osname", "ref"]
+          },
+          {
+            "properties": {
+              "default": {"enum": [true]}
+            },
+            "not": {
+              "anyOf": [
+                {"required": ["osname"]},
+                {"required": ["ref"]},
+                {"required": ["serial"]}
+              ]
+            }
+          }
+        ],
         "properties": {
           "osname": {
             "description": "Name of the stateroot to be used in the deployment",
@@ -55,6 +74,11 @@ SCHEMA_2 = """
             "description": "The deployment serial (usually '0')",
             "type": "number",
             "default": 0
+          },
+          "default": {
+            "description": "Find and use the default ostree deployment",
+            "type": "boolean",
+            "default": false
           }
         }
       }
@@ -99,12 +123,9 @@ class OSTreeDeploymentMount(mounts.MountService):
         tree = args["tree"]
         mountroot = args["root"]
         options = args["options"]
-
         source = options.get("source", "tree")
         deployment = options["deployment"]
-        osname = deployment["osname"]
-        ref = deployment["ref"]
-        serial = deployment.get("serial", 0)
+        osname, ref, serial = ostree.parse_deployment_option(tree, deployment)
 
         # The user could specify either the tree or mountroot as the
         # place where we want the deployment to be mounted.
diff --git a/osbuild/util/ostree.py b/osbuild/util/ostree.py
index 173a9676..4c0344d1 100644
--- a/osbuild/util/ostree.py
+++ b/osbuild/util/ostree.py
@@ -1,13 +1,15 @@
 import collections
 import contextlib
+import glob
 import json
 import os
+import re
 import subprocess
 import sys
 import tempfile
 import typing
 # pylint doesn't understand the string-annotation below
-from typing import Any, List  # pylint: disable=unused-import
+from typing import Any, Dict, List, Tuple  # pylint: disable=unused-import
 
 from osbuild.util.rhsm import Subscriptions
 
@@ -214,7 +216,43 @@ def parse_input_commits(commits):
     return commits["path"], data["refs"]
 
 
-def deployment_path(root: PathLike, osname: str, ref: str, serial: int):
+def parse_deployment_option(root: PathLike, deployment: Dict) -> Tuple[str, str, str]:
+    """Parse the deployment option and return the osname, ref, and serial
+
+    The `deployment` arg contains the following sub fields:
+    - osname: Name of the stateroot used in the deployment (ie. fedora-coreos)
+    - ref: OStree ref to used for the deployment (ie. fedora/aarch64/coreos/next)
+    - serial: The deployment serial (ie. 0)
+    - default: Boolean to determine whether the default ostree deployment should be used
+    """
+
+    default_deployment = deployment.get("default")
+    if default_deployment:
+        filenames = glob.glob(os.path.join(root, 'ostree/deploy/*/deploy/*.0'))
+        if len(filenames) < 1:
+            raise ValueError("Could not find deployment")
+        if len(filenames) > 1:
+            raise ValueError(f"More than one deployment found: {filenames}")
+
+        # We pick up the osname, commit, and serial from the filesystem
+        # here. We'll return the detected commit as the ref in this
+        # since it's a valid substitute for all subsequent uses in
+        # the code base.
+        f = re.search("/ostree/deploy/(.*)/deploy/(.*)\\.([0-9])", filenames[0])
+        if not f:
+            raise ValueError("cannot find ostree deployment in {filenames[0]}")
+        osname = f.group(1)
+        commit = f.group(2)
+        serial = f.group(3)
+        return osname, commit, serial
+
+    osname = deployment["osname"]
+    ref = deployment["ref"]
+    serial = deployment.get("serial", 0)
+    return osname, ref, serial
+
+
+def deployment_path(root: PathLike, osname: str = "", ref: str = "", serial: int = 0):
     """Return the path to a deployment given the parameters"""
 
     base = os.path.join(root, "ostree")
diff --git a/stages/org.osbuild.bootupd b/stages/org.osbuild.bootupd
index 224bd6d4..5663ea3e 100755
--- a/stages/org.osbuild.bootupd
+++ b/stages/org.osbuild.bootupd
@@ -32,7 +32,26 @@ SCHEMA_2 = r"""
     "deployment": {
       "type": "object",
       "additionalProperties": false,
-      "required": ["osname", "ref"],
+      "oneOf": [
+        {
+          "properties": {
+          "default": {"enum": [false]}
+          },
+          "required": ["osname", "ref"]
+        },
+        {
+          "properties": {
+          "default": {"enum": [true]}
+          },
+          "not": {
+            "anyOf": [
+              {"required": ["osname"]},
+              {"required": ["ref"]},
+              {"required": ["serial"]}
+            ]
+          }
+        }
+      ],
       "properties": {
         "osname": {
           "description": "Name of the stateroot to be used in the deployment",
@@ -46,6 +65,11 @@ SCHEMA_2 = r"""
           "description": "The deployment serial (usually '0')",
           "type": "number",
           "default": 0
+        },
+        "default": {
+          "description": "Find and use the default ostree deployment",
+            "type": "boolean",
+            "default": false
         }
       }
     },
@@ -101,9 +125,7 @@ def main(args, options):
     # we'll call ostree.deployment_path() helper to find it for us.
     root = mounts
     if deployment:
-        osname = deployment["osname"]
-        ref = deployment["ref"]
-        serial = deployment.get("serial", 0)
+        osname, ref, serial = ostree.parse_deployment_option(mounts, deployment)
         root = ostree.deployment_path(mounts, osname, ref, serial)
 
     bootupd_args = []
diff --git a/stages/org.osbuild.container-deploy b/stages/org.osbuild.container-deploy
index 43c2076b..7f419da4 100755
--- a/stages/org.osbuild.container-deploy
+++ b/stages/org.osbuild.container-deploy
@@ -47,12 +47,16 @@ def mount_container(image_tag):
     try:
         result = subprocess.run(
             ["podman", "image", "mount", image_tag],
-            capture_output=True,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
             encoding="utf-8",
-            check=True,
+            check=False,
         )
+        if result.returncode != 0:
+            code = result.returncode
+            msg = result.stderr.strip()
+            raise RuntimeError(f"Failed to mount image ({code}): {msg}")
         yield result.stdout.strip()
-
     finally:
         subprocess.run(
             ["podman", "image", "umount", image_tag],
diff --git a/stages/org.osbuild.copy b/stages/org.osbuild.copy
index 3c137af5..5bc9067f 100755
--- a/stages/org.osbuild.copy
+++ b/stages/org.osbuild.copy
@@ -45,9 +45,23 @@ SCHEMA_2 = r"""
         "required": ["from", "to"],
         "properties": {
           "from": {
-            "type": "string",
-            "description": "The source",
-            "pattern": "^input:\/\/[^\/]+\/"
+            "oneOf": [
+              {
+                "type": "string",
+                "description": "The source, if an input",
+                "pattern": "^input:\/\/[^\/]+\/"
+              },
+              {
+                "type": "string",
+                "description": "The source, if a mount",
+                "pattern": "^mount:\/\/[^\/]+\/"
+              },
+              {
+                "type": "string",
+                "description": "The source, if the tree",
+                "pattern": "^tree:\/\/\/"
+              }
+            ]
           },
           "to": {
             "oneOf": [
@@ -58,7 +72,7 @@ SCHEMA_2 = r"""
               },
               {
                 "type": "string",
-                "description": "The destination, if a tree",
+                "description": "The destination, if the tree",
                 "pattern": "^tree:\/\/\/"
               }
             ]
diff --git a/stages/org.osbuild.fstab b/stages/org.osbuild.fstab
index eb5ac2b0..a9af5ffe 100755
--- a/stages/org.osbuild.fstab
+++ b/stages/org.osbuild.fstab
@@ -29,7 +29,26 @@ SCHEMA = """
       "deployment": {
         "type": "object",
         "additionalProperties": false,
-        "required": ["osname","ref"],
+        "oneOf": [
+          {
+            "properties": {
+              "default": {"enum": [false]}
+            },
+            "required": ["osname", "ref"]
+          },
+          {
+            "properties": {
+              "default": {"enum": [true]}
+            },
+            "not": {
+              "anyOf": [
+                {"required": ["osname"]},
+                {"required": ["ref"]},
+                {"required": ["serial"]}
+              ]
+            }
+          }
+        ],
         "properties": {
           "osname": {
             "description": "Name of the stateroot to be used in the deployment",
@@ -43,6 +62,11 @@ SCHEMA = """
             "description": "The deployment serial (usually '0')",
             "type": "number",
             "default": 0
+          },
+          "default": {
+            "description": "Find and use the default ostree deployment",
+            "type": "boolean",
+            "default": false
           }
         }
       }
@@ -118,9 +142,7 @@ def main(tree, options):
 
     if ostree_options:
         deployment = ostree_options["deployment"]
-        osname = deployment["osname"]
-        ref = deployment["ref"]
-        serial = deployment.get("serial", 0)
+        osname, ref, serial = ostree.parse_deployment_option(tree, deployment)
 
         root = ostree.deployment_path(tree, osname, ref, serial)
 
diff --git a/stages/org.osbuild.ostree.aleph b/stages/org.osbuild.ostree.aleph
index 6baedeb0..bcef1fe3 100755
--- a/stages/org.osbuild.ostree.aleph
+++ b/stages/org.osbuild.ostree.aleph
@@ -19,6 +19,7 @@ COREOS_ALEPH_FILENAME = ".coreos-aleph-version.json"
 SCHEMA_2 = """
 "options": {
   "additionalProperties": false,
+  "required": ["deployment"],
   "properties": {
     "coreos_compat": {
       "description": "boolean to allow for CoreOS aleph version backwards compatibility",
@@ -26,7 +27,26 @@ SCHEMA_2 = """
     },
     "deployment": {
       "additionalProperties": false,
-      "required": ["osname", "ref"],
+      "oneOf": [
+        {
+          "properties": {
+            "default": {"enum": [false]}
+          },
+          "required": ["osname", "ref"]
+        },
+        {
+          "properties": {
+            "default": {"enum": [true]}
+          },
+          "not": {
+            "anyOf": [
+              {"required": ["osname"]},
+              {"required": ["ref"]},
+              {"required": ["serial"]}
+            ]
+          }
+        }
+      ],
       "properties": {
         "osname": {
           "description": "Name of the stateroot to be used in the deployment",
@@ -40,6 +60,11 @@ SCHEMA_2 = """
           "description": "The deployment serial (usually '0')",
           "type": "number",
           "default": 0
+        },
+        "default": {
+          "description": "Find and use the default ostree deployment",
+          "type": "boolean",
+          "default": false
         }
       }
     }
@@ -131,9 +156,7 @@ def construct_aleph_json(tree, origin):
 def main(tree, options):
     coreos_compat = options.get("coreos_compat", False)
     dep = options["deployment"]
-    osname = dep["osname"]
-    ref = dep["ref"]
-    serial = dep.get("serial", 0)
+    osname, ref, serial = ostree.parse_deployment_option(tree, dep)
 
     origin = ostree.deployment_path(tree, osname, ref, serial) + ".origin"
     data = construct_aleph_json(tree, origin)
diff --git a/stages/org.osbuild.ostree.fillvar b/stages/org.osbuild.ostree.fillvar
index e5b5eacd..dfb706e8 100755
--- a/stages/org.osbuild.ostree.fillvar
+++ b/stages/org.osbuild.ostree.fillvar
@@ -18,7 +18,26 @@ SCHEMA = """
 "properties": {
   "deployment": {
     "additionalProperties": false,
-    "required": ["osname", "ref"],
+    "oneOf": [
+      {
+        "properties": {
+          "default": {"enum": [false]}
+        },
+        "required": ["osname", "ref"]
+      },
+      {
+        "properties": {
+          "default": {"enum": [true]}
+        },
+        "not": {
+          "anyOf": [
+            {"required": ["osname"]},
+            {"required": ["ref"]},
+            {"required": ["serial"]}
+          ]
+        }
+      }
+    ],
     "properties": {
       "osname": {
         "description": "Name of the stateroot to be used in the deployment",
@@ -32,6 +51,11 @@ SCHEMA = """
         "description": "The deployment serial (usually '0')",
         "type": "number",
         "default": 0
+      },
+      "default": {
+        "description": "Find and use the default ostree deployment",
+        "type": "boolean",
+        "default": false
       }
     }
   }
@@ -71,9 +95,7 @@ def populate_var(sysroot):
 
 def main(tree, options):
     dep = options["deployment"]
-    osname = dep["osname"]
-    ref = dep["ref"]
-    serial = dep.get("serial", 0)
+    osname, ref, serial = ostree.parse_deployment_option(tree, dep)
 
     deployment = ostree.deployment_path(tree, osname, ref, serial)
     var = os.path.join(tree, "ostree", "deploy", osname, "var")
diff --git a/stages/org.osbuild.ostree.selinux b/stages/org.osbuild.ostree.selinux
index d990a5e5..58663e34 100755
--- a/stages/org.osbuild.ostree.selinux
+++ b/stages/org.osbuild.ostree.selinux
@@ -21,7 +21,26 @@ SCHEMA = """
 "properties": {
   "deployment": {
     "additionalProperties": false,
-    "required": ["osname", "ref"],
+    "oneOf": [
+      {
+        "properties": {
+          "default": {"enum": [false]}
+        },
+        "required": ["osname", "ref"]
+      },
+      {
+        "properties": {
+          "default": {"enum": [true]}
+        },
+        "not": {
+          "anyOf": [
+            {"required": ["osname"]},
+            {"required": ["ref"]},
+            {"required": ["serial"]}
+          ]
+        }
+      }
+    ],
     "properties": {
       "osname": {
         "description": "Name of the stateroot to be used in the deployment",
@@ -35,6 +54,11 @@ SCHEMA = """
         "description": "The deployment serial (usually '0')",
         "type": "number",
         "default": 0
+      },
+      "default": {
+        "description": "Find and use the default ostree deployment",
+        "type": "boolean",
+        "default": false
       }
     }
   }
@@ -44,9 +68,7 @@ SCHEMA = """
 
 def main(tree, options):
     dep = options["deployment"]
-    osname = dep["osname"]
-    ref = dep["ref"]
-    serial = dep.get("serial", 0)
+    osname, ref, serial = ostree.parse_deployment_option(tree, dep)
 
     # this created a state root at `osname`
     stateroot = f"{tree}/ostree/deploy/{osname}"
diff --git a/stages/org.osbuild.zipl.inst b/stages/org.osbuild.zipl.inst
index a9bea045..caca9f82 100755
--- a/stages/org.osbuild.zipl.inst
+++ b/stages/org.osbuild.zipl.inst
@@ -28,6 +28,14 @@ SCHEMA_2 = r"""
     "location": {
       "type": "number"
     },
+    "kernel_opts_append": {
+      "description": "Additional kernel options to add to the discovered options",
+      "type": "array",
+      "items": {
+        "description": "A single kernel command line option",
+        "type": "string"
+      }
+    },
     "blocksize": {
       "type": "number",
       "default": 512
@@ -85,6 +93,7 @@ def find_kernel(root, kernel: str):
 def main(paths, devices, options):
     kver = options["kernel"]
     location = options["location"]
+    append_kopts = options.get("kernel_opts_append", [])
     blocksize = options.get("blocksize", 512)
 
     root = paths["mounts"]
@@ -100,7 +109,7 @@ def main(paths, devices, options):
                     "--target", f"{root}/boot",
                     "--image", kernel,
                     "--ramdisk", initrd,
-                    "--parameters", kopts,
+                    "--parameters", f"{kopts} {' '.join(append_kopts)}" if append_kopts else kopts,
                     "--targetbase", device,
                     "--targettype", "SCSI",
                     "--targetblocksize", str(blocksize),
diff --git a/tools/osbuild-depsolve-dnf b/tools/osbuild-depsolve-dnf
index a54473b8..fdc34624 100755
--- a/tools/osbuild-depsolve-dnf
+++ b/tools/osbuild-depsolve-dnf
@@ -121,6 +121,11 @@ class Solver():
         # we set the expiration to a short time period, rather than 0.
         repo.metadata_expire = desc.get("metadata_expire", "20s")
 
+        # This option if True disables modularization filtering. Effectively
+        # disabling modularity for given repository.
+        if "module_hotfixes" in desc:
+            repo.module_hotfixes = desc["module_hotfixes"]
+
         return repo
 
     @staticmethod
@@ -201,11 +206,13 @@ class Solver():
     def depsolve(self, transactions):
         last_transaction = []
 
-        for transaction in transactions:
+        for idx, transaction in enumerate(transactions):
             self.base.reset(goal=True)
             self.base.sack.reset_excludes()
 
-            self.base.conf.install_weak_deps = transaction.get("install_weak_deps", False)
+            # don't install weak-deps for transactions after the 1st transaction
+            if idx > 0:
+                self.base.conf.install_weak_deps = False
 
             # set the packages from the last transaction as installed
             for installed_pkg in last_transaction:


Test patch:
diff --git a/stages/test/test_bootupd.py b/stages/test/test_bootupd.py
index 06cc485b..aafd6b33 100644
--- a/stages/test/test_bootupd.py
+++ b/stages/test/test_bootupd.py
@@ -12,10 +12,17 @@ STAGE_NAME = "org.osbuild.bootupd"
 @pytest.mark.parametrize("test_data,expected_err", [
     # bad
     ({"deployment": "must-be-object"}, "'must-be-object' is not of type 'object'"),
-    ({"deployment": {"osname": "some-os"}}, "'ref' is a required property"),
-    ({"deployment": {"ref": "some-ref"}}, "'osname' is a required property"),
+    ({"deployment": {"osname": "some-os"}}, "{'osname': 'some-os'} is not valid under any of the given schemas"),
+    ({"deployment": {"ref": "some-ref"}}, "{'ref': 'some-ref'} is not valid under any of the given schemas"),
     ({"deployment": {"osname": "some-os", "ref": "some-ref", "serial": "must-be-number"}},
      "'must-be-number' is not of type 'number'"),
+    ({"deployment": {"default": False}}, "{'default': False} is not valid under any of the given schemas"),
+    ({"deployment": {
+        "osname": "some-os",
+        "ref": "some-ref",
+        "serial": 0,
+        "default": True}
+      }, "{'osname': 'some-os', 'ref': 'some-ref', 'serial': 0, 'default': True} is not valid under any of the given schemas"),
     ({"random": "property"}, "Additional properties are not allowed"),
     ({"bios": {}}, "'device' is a required property"),
     ({"bios": "must-be-object"}, "'must-be-object' is not of type 'object'"),
@@ -33,6 +40,17 @@ STAGE_NAME = "org.osbuild.bootupd"
         {
             "device": "/dev/sda",
         },
+    }, ""),
+    ({
+        "deployment":
+        {
+            "default": True,
+        },
+        "static-configs": True,
+        "bios":
+        {
+            "device": "/dev/sda",
+        },
     }, "")
 
 ])
diff --git a/stages/test/test_zipl_inst.py b/stages/test/test_zipl_inst.py
new file mode 100644
index 00000000..8e867fbb
--- /dev/null
+++ b/stages/test/test_zipl_inst.py
@@ -0,0 +1,82 @@
+#!/usr/bin/python3
+
+import os
+import textwrap
+from unittest.mock import patch
+
+from osbuild.testutil import make_fake_tree
+
+STAGE_NAME = "org.osbuild.zipl.inst"
+
+
+@patch("subprocess.run")
+def test_zipl_inst_default(mocked_run, tmp_path, stage_module):
+    make_fake_tree(tmp_path, {
+        "/boot/loader/entries/ostree-1-fedora-coreos.conf": textwrap.dedent("""\
+        title Fedora CoreOS 39.20240206.dev.0 (ostree:0)
+        version 1
+        options ignition.platform.id=qemu ostree=/ostree/boot.1/fedora-coreos/abcd/0
+        linux /vmlinuz-6.6.14-200.fc39.s390x
+        initrd /initramfs-6.6.14-200.fc39.s390x.img
+        """),
+        "/vmlinuz-6.6.14-200.fc39.s390x": "fake-vmlinuz",
+        "/initramfs-6.6.14-200.fc39.s390x.img": "fake-initramfs.img",
+    })
+    fake_paths = {
+        "mounts": os.fspath(tmp_path),
+        "devices": "/run/osbuild/dev",
+    }
+    fake_devices = {
+        "disk": {
+            "path": "some-device-path",
+        }
+    }
+    fake_options = {
+        "kernel": "1",
+        "location": "some-location",
+    }
+
+    stage_module.main(fake_paths, fake_devices, fake_options)
+    assert len(mocked_run.call_args_list) == 1
+    args, kwargs = mocked_run.call_args_list[0]
+    assert len(args) == 1
+    run_argv = args[0]
+    assert "ignition.platform.id=qemu ostree=/ostree/boot.1/fedora-coreos/abcd/0" in run_argv
+    assert kwargs["check"]
+
+
+@patch("subprocess.run")
+def test_zipl_inst_kernel_opts_append(mocked_run, tmp_path, stage_module):
+    make_fake_tree(tmp_path, {
+        "/boot/loader/entries/00_linux.conf": textwrap.dedent("""\
+        title fake-linux
+        linux /vmlinuz
+        initrd /initrd.img
+        options root=/dev/sda1 ro
+        version 6.8
+        """),
+        "/vmlinuz": "fake-vmlinuz",
+        "/initrd.img": "fake-initrd.img",
+    })
+    fake_paths = {
+        "mounts": os.fspath(tmp_path),
+        "devices": "/run/osbuild/dev",
+    }
+    fake_devices = {
+        "disk": {
+            "path": "some-device-path",
+        }
+    }
+    fake_options = {
+        "kernel": "6.8",
+        "location": "some-location",
+        "kernel_opts_append": ["some=more", "opts"],
+    }
+
+    stage_module.main(fake_paths, fake_devices, fake_options)
+    assert len(mocked_run.call_args_list) == 1
+    args, kwargs = mocked_run.call_args_list[0]
+    assert len(args) == 1
+    run_argv = args[0]
+    assert "root=/dev/sda1 ro some=more opts" in run_argv
+    assert kwargs["check"]
diff --git a/test/data/manifests/fedora-coreos-container.json b/test/data/manifests/fedora-coreos-container.json
index bc9303d4..18cf87da 100644
--- a/test/data/manifests/fedora-coreos-container.json
+++ b/test/data/manifests/fedora-coreos-container.json
@@ -518,8 +518,7 @@
           "options": {
             "coreos_compat": true,
             "deployment": {
-              "ref": "ostree/1/1/0",
-              "osname": "fedora-coreos"
+              "default": true
             }
           }
         },
@@ -527,8 +526,7 @@
           "type": "org.osbuild.ostree.selinux",
           "options": {
             "deployment": {
-              "ref": "ostree/1/1/0",
-              "osname": "fedora-coreos"
+              "default": true
             }
           }
         }
@@ -705,8 +703,7 @@
             },
             "static-configs": true,
             "deployment": {
-              "ref": "ostree/1/1/0",
-              "osname": "fedora-coreos"
+              "default": true
             }
           },
           "devices": {
@@ -956,8 +953,7 @@
           "options": {
             "static-configs": true,
             "deployment": {
-              "ref": "ostree/1/1/0",
-              "osname": "fedora-coreos"
+              "default": true
             }
           },
           "devices": {
diff --git a/test/data/manifests/fedora-coreos-container.mpp.yaml b/test/data/manifests/fedora-coreos-container.mpp.yaml
index 59fdf552..60ebe72f 100644
--- a/test/data/manifests/fedora-coreos-container.mpp.yaml
+++ b/test/data/manifests/fedora-coreos-container.mpp.yaml
@@ -129,15 +129,11 @@ pipelines:
         options:
           coreos_compat: true
           deployment:
-            ref: ostree/1/1/0
-            osname:
-              mpp-format-string: '{osname}'
+            default: true
       - type: org.osbuild.ostree.selinux
         options:
           deployment:
-            ref: ostree/1/1/0
-            osname:
-              mpp-format-string: '{osname}'
+            default: true
   - name: raw-image
     build: name:build
     stages:
@@ -242,9 +238,7 @@ pipelines:
             device: disk
           static-configs: true
           deployment:
-            ref: ostree/1/1/0
-            osname:
-              mpp-format-string: '{osname}'
+            default: true
         devices:
           disk:
             type: org.osbuild.loopback
@@ -408,9 +402,7 @@ pipelines:
         options:
           static-configs: true
           deployment:
-            ref: ostree/1/1/0
-            osname:
-              mpp-format-string: '{osname}'
+            default: true
         devices:
           disk:
             type: org.osbuild.loopback
diff --git a/test/mod/test_util_ostree.py b/test/mod/test_util_ostree.py
index 6a934a58..2f87f0af 100644
--- a/test/mod/test_util_ostree.py
+++ b/test/mod/test_util_ostree.py
@@ -11,6 +11,7 @@ import unittest
 
 import pytest
 
+from osbuild.testutil import make_fake_tree
 from osbuild.util import ostree
 
 from .. import test
@@ -208,3 +209,41 @@ class TestPasswdLike(unittest.TestCase):
             check.read_from(file)
 
             assert subids.db == check.db
+
+
+def test_parse_default_deployment_happy(tmp_path):
+    deployment = {
+        "default": True,
+    }
+    make_fake_tree(tmp_path, {
+        "ostree/deploy/fedora-coreos/deploy/72f807.0": "",
+    })
+    osname, ref, serial = ostree.parse_deployment_option(tmp_path, deployment)
+    assert osname == "fedora-coreos"
+    assert ref == "72f807"
+    assert serial == "0"
+
+
+def test_parse_default_deployment_sad(tmp_path):
+    deployment = {
+        "default": True,
+    }
+    make_fake_tree(tmp_path, {
+        "ostree/deploy/fedora-coreos/deploy/72f807.0": "",
+        "ostree/deploy/fedora-coreos/deploy/123456.0": "",
+    })
+    with pytest.raises(ValueError) as exp:
+        ostree.parse_deployment_option(tmp_path, deployment)
+    assert "More than one deployment found: [" in str(exp.value)
+
+
+def test_parse_deployment_happy(tmp_path):
+    deployment = {
+        "osname": "fedora-coreos",
+        "ref": "some-ref",
+        "serial": "0",
+    }
+    osname, ref, serial = ostree.parse_deployment_option(tmp_path, deployment)
+    assert osname == "fedora-coreos"
+    assert ref == "some-ref"
+    assert serial == "0"