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.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/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/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"