Repository: osbuild/osbuild
Pull request #1561 (2024-01-30 16:36:32)

stages(tar): add paths option

achilleas-k: Clicked the rebase button because it looked like the tests were stuck.
croissanne: it's ok, I need to edit the tests anyway, as of course the order isn't fixed when just supplying the whole dir


Patch:
diff --git a/stages/org.osbuild.tar b/stages/org.osbuild.tar
index f42ccd9d..4068c551 100755
--- a/stages/org.osbuild.tar
+++ b/stages/org.osbuild.tar
@@ -26,6 +26,25 @@ CAPABILITIES = ["CAP_MAC_ADMIN"]
 SCHEMA_2 = """
 "options": {
   "additionalProperties": false,
+  "anyOf": [
+    {
+      "required": ["root-node"],
+      "not": {
+        "required": ["paths"]
+      }
+    },
+    {
+      "required": ["paths"],
+      "not": {
+        "required": ["root-node"]
+      }
+    },
+    {
+      "not": {
+        "required": ["paths", "root-node"]
+      }
+    }
+  ],
   "required": ["filename"],
   "properties": {
     "filename": {
@@ -54,8 +73,15 @@ SCHEMA_2 = """
       "default": true
     },
     "root-node": {
-      "description": "How to handle the root node: include or omit",
+      "description": "How to handle the root node: include or omit, mutually exclusive with paths",
       "enum": ["include", "omit"]
+    },
+    "paths": {
+      "type": "array",
+      "items": {
+        "type": "string",
+        "description": "If specified, the archive will only contain the specified paths, in the specified order"
+      }
     }
   }
 },
@@ -78,6 +104,7 @@ def main(inputs, output_dir, options):
     filename = options["filename"].lstrip("/")
     tarfmt = options.get("format", "gnu")
     root_node = options.get("root-node", "include")
+    paths = options.get("paths", [])
 
     extra_args = []
     # Set environment variables for the tar operation.
@@ -106,13 +133,16 @@ def main(inputs, output_dir, options):
         "-C", tree,
     ]
 
-    # Should we include the root inode or not
-    # This will change the way files are listed:
-    # `./file` (include) vs `file` (omit)
-    if root_node == "include":
-        tar_cmd += ["."]
+    if len(paths) == 0:
+        # Should we include the root inode or not
+        # This will change the way files are listed:
+        # `./file` (include) vs `file` (omit)
+        if root_node == "include":
+            tar_cmd += ["."]
+        else:
+            tar_cmd += os.listdir(tree)
     else:
-        tar_cmd += os.listdir(tree)
+        tar_cmd += paths
 
     # Make a tarball of the tree.
     subprocess.run(


Test patch:
diff --git a/stages/test/test_tar.py b/stages/test/test_tar.py
new file mode 100644
index 00000000..6a988a69
--- /dev/null
+++ b/stages/test/test_tar.py
@@ -0,0 +1,100 @@
+#!/usr/bin/python3
+
+import os.path
+import subprocess
+
+import pytest
+
+from osbuild import testutil
+from osbuild.testutil import has_executable, make_fake_input_tree
+
+STAGE_NAME = "org.osbuild.tar"
+
+
+@pytest.mark.parametrize("test_data,expected_err", [
+    # bad
+    ({}, "'filename' is a required property"),
+    ({
+        "filename": "out.tar",
+        "root-node": "include",
+        "paths": ["file1"]
+    }, "{'filename': 'out.tar', 'root-node': 'include', 'paths': ['file1']} is not valid under any of the given schemas"),
+    # good
+    ({"filename": "out.tar", "root-node": "include"}, ""),
+    ({"filename": "out.tar", "paths": ["file1"]}, ""),
+    ({"filename": "out.tar"}, ""),
+])
+def test_schema_validation_tar(stage_schema, test_data, expected_err):
+    test_input = {
+        "type": STAGE_NAME,
+        "options": {
+        }
+    }
+    test_input["options"].update(test_data)
+    res = stage_schema.validate(test_input)
+
+    if expected_err == "":
+        assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}"
+    else:
+        assert res.valid is False
+        testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1)
+
+
+@pytest.fixture(name="fake_inputs")
+def make_fake_inputs(tmp_path):
+    fake_input_tree = make_fake_input_tree(tmp_path, {
+        "/file1": "A file to be tarred",
+        "/file2": "Another file to be tarred",
+    })
+    inputs = {
+        "tree": {
+            "path": fake_input_tree,
+        }
+    }
+    return inputs
+
+
+@pytest.mark.skipif(not has_executable("tar"), reason="no tar executable")
+def test_tar_filename(tmp_path, stage_module, fake_inputs):
+    options = {
+        "filename": "out.tar",
+    }
+    stage_module.main(fake_inputs, tmp_path, options)
+
+    tar_path = os.path.join(tmp_path, "out.tar")
+    assert os.path.exists(tar_path)
+    output = subprocess.check_output(["tar", "-tf", tar_path], encoding="utf-8").strip().split("\n")
+    assert sorted(["./", "./file1", "./file2"]) == sorted(output)
+
+
+@pytest.mark.skipif(not has_executable("tar"), reason="no tar executable")
+def test_tar_root_node_omit(tmp_path, stage_module, fake_inputs):
+    options = {
+        "filename": "out.tar",
+        "root-node": "omit",
+    }
+    stage_module.main(fake_inputs, tmp_path, options)
+
+    tar_path = os.path.join(tmp_path, "out.tar")
+    assert os.path.exists(tar_path)
+    output = subprocess.check_output(["tar", "-tf", tar_path], encoding="utf-8").strip().split("\n")
+    assert sorted(["file1", "file2"]) == sorted(output)
+
+
+@pytest.mark.skipif(not has_executable("tar"), reason="no tar executable")
+def test_tar_paths(tmp_path, stage_module, fake_inputs):
+    options = {
+        "filename": "out.tar",
+        "paths": [
+            "file2",
+            "file1",
+            "file1",
+            "file2",
+        ]
+    }
+    stage_module.main(fake_inputs, tmp_path, options)
+
+    tar_path = os.path.join(tmp_path, "out.tar")
+    assert os.path.exists(tar_path)
+    output = subprocess.check_output(["tar", "-tf", tar_path], encoding="utf-8").split("\n")
+    assert ["file2", "file1", "file1", "file2", ""] == output