Repository: osbuild/osbuild-composer
Pull request #2881 (2022-08-05 08:11:23)

tests: Remove useless JSON file overrides
This pull request includes:

- [ ] adequate testing for the new functionality or fixed issue
- [ ] adequate documentation informing people about the change such as
  - [ ] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`

Information regarding our GitLab pipeline (Schutzbot):

CI will not be ran automatically if WIP label is applied or the PR is in DRAFT state, instead only
a link will be provided to the pipeline which can then be triggered manually if desired. To run the
CI automatically either switch the PR to ready or apply WIP+test label.

Outside contributors need manual approval from one of the osbuild-composer maintainers.

Schutzbot will only be triggered if all Tests jobs in GitHub workflow succeed.
-->


jrusz: > Uh, does this mean we were testing with older sources until now? Thanks for fixing this, totally forgot this was happening and needed update, got just one comment.

Hm, actually no, it seems that we were using the correct sources during nightly testing https://gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/-/jobs/2811520975#L3105 so idk what this override does tbh...
atodorov: > Hm, actually no, it seems that we were using the correct sources during nightly testing https://gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/-/jobs/2811520975#L3105 so idk what this override does tbh...

Could that be because the repos are overriden one more time a few lines below the current snippet ? So effectively for nightly you are not seeing the overrides coming from this `case` statement. 

Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index be76ec339..aa36365ae 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -144,7 +144,6 @@ Prepare-rhel-internal:
     paths:
       - rhel-${RHEL_MAJOR}.json
       - rhel${RHEL_MAJOR}internal.repo
-      - rhel-${RHEL_MAJOR}-beta.json
       - COMPOSE_ID
   parallel:
     matrix:
diff --git a/internal/disk/disk.go b/internal/disk/disk.go
index 6a6f0a38d..85680feb3 100644
--- a/internal/disk/disk.go
+++ b/internal/disk/disk.go
@@ -18,10 +18,12 @@ package disk
 
 import (
 	"encoding/hex"
+	"fmt"
 	"io"
 	"math/rand"
 
 	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
 )
 
 const (
@@ -50,6 +52,19 @@ const (
 	XBootLDRPartitionGUID = "BC13C2FF-59E6-4262-A352-B275FD6F7172"
 )
 
+var MountpointPolicies = NewPathPolicies(map[string]PathPolicy{
+	"/":     {Exact: true},
+	"/boot": {Exact: true},
+	"/var":  {},
+	"/opt":  {},
+	"/srv":  {},
+	"/usr":  {},
+	"/app":  {},
+	"/data": {},
+	"/home": {},
+	"/tmp":  {},
+})
+
 // Entity is the base interface for all disk-related entities.
 type Entity interface {
 	// IsContainer indicates if the implementing type can
@@ -164,3 +179,19 @@ func NewVolIDFromRand(r *rand.Rand) string {
 	}
 	return hex.EncodeToString(volid)
 }
+
+func CheckMountpoints(mountpoints []blueprint.FilesystemCustomization, mountpointAllowList *PathPolicies) error {
+	invalidMountpoints := []string{}
+	for _, m := range mountpoints {
+		err := mountpointAllowList.Check(m.Mountpoint)
+		if err != nil {
+			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
+		}
+	}
+
+	if len(invalidMountpoints) > 0 {
+		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	}
+
+	return nil
+}
diff --git a/internal/disk/partition_table.go b/internal/disk/partition_table.go
index 93677ac5f..3c12bad1b 100644
--- a/internal/disk/partition_table.go
+++ b/internal/disk/partition_table.go
@@ -21,23 +21,25 @@ type PartitionTable struct {
 
 func NewPartitionTable(basePT *PartitionTable, mountpoints []blueprint.FilesystemCustomization, imageSize uint64, lvmify bool, rng *rand.Rand) (*PartitionTable, error) {
 	newPT := basePT.Clone().(*PartitionTable)
-	for _, mnt := range mountpoints {
-		size := newPT.AlignUp(clampFSSize(mnt.Mountpoint, mnt.MinSize))
-		if path := entityPath(newPT, mnt.Mountpoint); len(path) != 0 {
-			resizeEntityBranch(path, size)
-		} else {
-			if lvmify {
-				err := newPT.ensureLVM()
-				if err != nil {
-					return nil, err
-				}
-			}
-			if err := newPT.createFilesystem(mnt.Mountpoint, size); err != nil {
-				return nil, err
-			}
+
+	// first pass: enlarge existing mountpoints and collect new ones
+	newMountpoints, _ := newPT.applyCustomization(mountpoints, false)
+
+	// if there is any new mountpoint and lvmify is enabled, ensure we have LVM layout
+	if lvmify && len(newMountpoints) > 0 {
+		err := newPT.ensureLVM()
+		if err != nil {
+			return nil, err
 		}
 	}
 
+	// second pass: deal with new mountpoints and newly created ones, after switching to
+	// the LVM layout, if requested, which might introduce new mount points, i.e. `/boot`
+	_, err := newPT.applyCustomization(newMountpoints, true)
+	if err != nil {
+		return nil, err
+	}
+
 	// TODO: make these overrideable for each image type
 	newPT.EnsureDirectorySizes(map[string]uint64{
 		"/":    1073741824,
@@ -312,6 +314,31 @@ func (pt *PartitionTable) HeaderSize() uint64 {
 	return header
 }
 
+// Apply filesystem filesystem customization to the partiton table. If create is false
+// will only apply customizations to existing partitions and return unhandled, i.e new
+// ones. An error can only occur if create is set. Conversely, it will only return non
+// empty list of new mountpoints if create is false.
+// Does not relayout the table, i.e. a call to relayout might be needed.
+func (pt *PartitionTable) applyCustomization(mountpoints []blueprint.FilesystemCustomization, create bool) ([]blueprint.FilesystemCustomization, error) {
+
+	newMountpoints := []blueprint.FilesystemCustomization{}
+
+	for _, mnt := range mountpoints {
+		size := pt.AlignUp(clampFSSize(mnt.Mountpoint, mnt.MinSize))
+		if path := entityPath(pt, mnt.Mountpoint); len(path) != 0 {
+			resizeEntityBranch(path, size)
+		} else {
+			if !create {
+				newMountpoints = append(newMountpoints, mnt)
+			} else if err := pt.createFilesystem(mnt.Mountpoint, size); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return newMountpoints, nil
+}
+
 // Dynamically calculate and update the start point for each of the existing
 // partitions. Adjusts the overall size of image to either the supplied
 // value in `size` or to the sum of all partitions if that is lager.
@@ -467,7 +494,13 @@ func (pt *PartitionTable) FindMountable(mountpoint string) Mountable {
 
 func clampFSSize(mountpoint string, size uint64) uint64 {
 	// set a minimum size of 1GB for all mountpoints
+	// with the exception for '/boot' (= 500 MB)
 	var minSize uint64 = 1073741824
+
+	if mountpoint == "/boot" {
+		minSize = 524288000
+	}
+
 	if minSize > size {
 		return minSize
 	}
diff --git a/internal/disk/path_policy.go b/internal/disk/path_policy.go
new file mode 100644
index 000000000..d1383381f
--- /dev/null
+++ b/internal/disk/path_policy.go
@@ -0,0 +1,54 @@
+package disk
+
+import (
+	"fmt"
+	"path"
+)
+
+type PathPolicy struct {
+	Deny  bool // explicitly do not allow this entry
+	Exact bool // require and exact match, no subdirs
+}
+
+type PathPolicies = PathTrie
+
+// Create a new PathPolicies trie from a map of path to PathPolicy
+func NewPathPolicies(entries map[string]PathPolicy) *PathPolicies {
+
+	noType := make(map[string]interface{}, len(entries))
+
+	for k, v := range entries {
+		noType[k] = v
+	}
+
+	return NewPathTrieFromMap(noType)
+}
+
+// Check a given path at dir against the PathPolicies
+func (pol *PathPolicies) Check(dir string) error {
+
+	// Quickly check we have a mountpoint and it is absolute
+	if dir == "" || dir[0] != '/' {
+		return fmt.Errorf("mountpoint must be absolute path")
+	}
+
+	// ensure that only clean mountpoints are valid
+	if dir != path.Clean(dir) {
+		return fmt.Errorf("mountpoint must be a canonical path")
+	}
+
+	node, left := pol.Lookup(dir)
+	policy, ok := node.Payload.(PathPolicy)
+	if !ok {
+		panic("programming error: invalid path trie payload")
+	}
+
+	// 1) path is explicitly not allowed or
+	// 2) a subpath was match but an explicit match is required
+	if policy.Deny || (policy.Exact && len(left) > 0) {
+		return fmt.Errorf("path '%s ' is not allowed", dir)
+	}
+
+	// exact match or recursive mountpoints allowed
+	return nil
+}
diff --git a/internal/disk/path_trie.go b/internal/disk/path_trie.go
new file mode 100644
index 000000000..1d43df3f7
--- /dev/null
+++ b/internal/disk/path_trie.go
@@ -0,0 +1,123 @@
+package disk
+
+import (
+	"sort"
+	"strings"
+)
+
+// splits the path into its individual components. Retruns the
+// empty list if the path is just the absolute root, i.e. "/".
+func pathTrieSplitPath(path string) []string {
+	path = strings.Trim(path, "/")
+	if path == "" {
+		return []string{}
+	}
+
+	return strings.Split(path, "/")
+}
+
+type PathTrie struct {
+	Name    []string
+	Paths   []*PathTrie
+	Payload interface{}
+}
+
+// match checks if the given trie is a prefix of path
+func (trie *PathTrie) match(path []string) bool {
+	if len(trie.Name) > len(path) {
+		return false
+	}
+
+	for i := range trie.Name {
+		if path[i] != trie.Name[i] {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (trie *PathTrie) get(path []string) (*PathTrie, []string) {
+	if len(path) < 1 {
+		panic("programming error: expected root node")
+	}
+
+	var node *PathTrie
+	for i := range trie.Paths {
+		if trie.Paths[i].match(path) {
+			node = trie.Paths[i]
+			break
+		}
+	}
+
+	// no subpath match, we are the best match
+	if node == nil {
+		return trie, path
+	}
+
+	// node, or one of its sub-nodes, is a match
+	prefix := len(node.Name)
+
+	// the node is a perfect match, return it
+	if len(path) == prefix {
+		return node, nil
+	}
+
+	// check if any sub-path's of node match
+	return node.get(path[prefix:])
+}
+
+func (trie *PathTrie) add(path []string) *PathTrie {
+	node := &PathTrie{Name: path}
+
+	if trie.Paths == nil {
+		trie.Paths = make([]*PathTrie, 0, 1)
+	}
+
+	trie.Paths = append(trie.Paths, node)
+
+	return node
+}
+
+// Construct a new trie from a map of paths to their payloads.
+// Returns the root node of the trie.
+func NewPathTrieFromMap(entries map[string]interface{}) *PathTrie {
+	root := &PathTrie{Name: []string{}}
+
+	keys := make([]string, 0, len(entries))
+	for k := range entries {
+		keys = append(keys, k)
+	}
+
+	sort.Strings(keys)
+
+	for _, k := range keys {
+		node, left := root.Lookup(k)
+
+		if len(left) > 0 {
+			node = node.add(left)
+		}
+
+		node.Payload = entries[k]
+	}
+
+	return root
+}
+
+// Lookup returns the node that is the prefix of path and
+// the unmatched path segment. Must be called on the root
+// trie node.
+func (root *PathTrie) Lookup(path string) (*PathTrie, []string) {
+
+	if len(root.Name) != 0 {
+		panic("programming error: lookup on non-root trie node")
+	}
+
+	elements := pathTrieSplitPath(path)
+
+	if len(elements) == 0 {
+		return root, elements
+	}
+
+	return root.get(elements)
+}
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 4d629450a..d27973bfe 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -3,8 +3,6 @@ package distro
 import (
 	"encoding/json"
 	"fmt"
-	"path"
-	"strings"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/container"
@@ -260,22 +258,3 @@ func MakePackageSetChains(t ImageType, packageSets map[string]rpmmd.PackageSet,
 
 	return chainedSets
 }
-
-func IsMountpointAllowed(mountpoint string, allowlist []string) bool {
-	for _, allowed := range allowlist {
-		match, _ := path.Match(allowed, mountpoint)
-		if match {
-			return true
-		}
-		// ensure that only clean mountpoints
-		// are valid
-		if strings.Contains(mountpoint, "//") {
-			return false
-		}
-		match = strings.HasPrefix(mountpoint, allowed+"/")
-		if allowed != "/" && match {
-			return true
-		}
-	}
-	return false
-}
diff --git a/internal/distro/fedora/distro.go b/internal/distro/fedora/distro.go
index 56affff1d..df1be4ba6 100644
--- a/internal/distro/fedora/distro.go
+++ b/internal/distro/fedora/distro.go
@@ -46,10 +46,6 @@ const (
 )
 
 var (
-	mountpointAllowList = []string{
-		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-	}
-
 	oscapProfileAllowList = []oscap.Profile{
 		oscap.Ospp,
 		oscap.PciDss,
@@ -700,15 +696,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
diff --git a/internal/distro/rhel7/distro.go b/internal/distro/rhel7/distro.go
index 00726e639..2bae70616 100644
--- a/internal/distro/rhel7/distro.go
+++ b/internal/distro/rhel7/distro.go
@@ -29,10 +29,6 @@ const (
 	blueprintPkgsKey = "blueprint"
 )
 
-var mountpointAllowList = []string{
-	"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-}
-
 // RHEL-based OS image configuration defaults
 var defaultDistroImageConfig = &distro.ImageConfig{
 	Timezone: "America/New_York",
@@ -437,15 +433,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 
 	mountpoints := customizations.GetFilesystems()
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
diff --git a/internal/distro/rhel8/distro.go b/internal/distro/rhel8/distro.go
index a2d54ef07..f845ab1c5 100644
--- a/internal/distro/rhel8/distro.go
+++ b/internal/distro/rhel8/distro.go
@@ -69,10 +69,6 @@ const (
 )
 
 var (
-	mountpointAllowList = []string{
-		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-	}
-
 	// rhel8 allow all
 	oscapProfileAllowList = []oscap.Profile{
 		oscap.AnssiBp28Enhanced,
@@ -667,15 +663,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
diff --git a/internal/distro/rhel90/distro.go b/internal/distro/rhel90/distro.go
index 6880e719d..0dd49ab7f 100644
--- a/internal/distro/rhel90/distro.go
+++ b/internal/distro/rhel90/distro.go
@@ -39,10 +39,6 @@ const (
 )
 
 var (
-	mountpointAllowList = []string{
-		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-	}
-
 	// rhel9 & cs9 share the same list
 	// of allowed profiles so a single
 	// allow list can be used
@@ -603,15 +599,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
diff --git a/osbuild-composer.spec b/osbuild-composer.spec
index 7e32c4194..e5f89138a 100644
--- a/osbuild-composer.spec
+++ b/osbuild-composer.spec
@@ -9,7 +9,7 @@
 
 %global goipath         github.com/osbuild/osbuild-composer
 
-Version:        59
+Version:        60
 
 %gometa
 
diff --git a/schutzbot/prepare-rhel-internal.sh b/schutzbot/prepare-rhel-internal.sh
index efd4b5489..6a29460c1 100755
--- a/schutzbot/prepare-rhel-internal.sh
+++ b/schutzbot/prepare-rhel-internal.sh
@@ -88,8 +88,6 @@ tee -a rhel-"${VERSION_ID%.*}".json << EOF
 }
 EOF
 
-cp rhel-"${VERSION_ID%.*}".json rhel-"${VERSION_ID%.*}"-beta.json
-
 # Create tests .repo file if REPO_URL is provided from ENV
 # Otherwise osbuild-composer-tests.rpm will be downloaded from
 # existing repositories
diff --git a/tools/provision.sh b/tools/provision.sh
index 4a94f5e97..6ae5d48d8 100755
--- a/tools/provision.sh
+++ b/tools/provision.sh
@@ -141,32 +141,11 @@ else # AUTH_METHOD_NONE
     sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-90.json "$REPODIR"
     sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-91.json "$REPODIR"
 
-    # RHEL nightly repos need to be overridden
-    case "${ID}-${VERSION_ID}" in
-        "rhel-8.6")
-            # Override old rhel-8.json and rhel-8-beta.json because RHEL 8.6 test needs nightly repos
-            sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-86.json "$REPODIR/rhel-8.json"
-            # If multiple tests are run and call provision.sh the symlink will need to be overridden with -f
-            sudo ln -sf /etc/osbuild-composer/repositories/rhel-8.json "$REPODIR/rhel-8-beta.json"
-            ;;
-        "rhel-9.0")
-            # Override old rhel-90.json and rhel-90-beta.json because RHEL 9.0 test needs nightly repos
-            sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-90.json "$REPODIR/rhel-90.json"
-            # If multiple tests are run and call provision.sh the symlink will need to be overridden with -f
-            sudo ln -sf /etc/osbuild-composer/repositories/rhel-90.json "$REPODIR/rhel-90-beta.json"
-            ;;
-        *) ;;
-    esac
-
     # overrides for RHEL nightly builds testing
     VERSION_SUFFIX=$(echo "${VERSION_ID}" | tr -d ".")
     if [ -f "rhel-${VERSION_ID%.*}.json" ]; then
         sudo cp rhel-"${VERSION_ID%.*}".json "$REPODIR/rhel-${VERSION_SUFFIX}.json"
     fi
-
-    if [ -f "rhel-${VERSION_ID%.*}-beta.json" ]; then
-        sudo cp rhel-"${VERSION_ID%.*}"-beta.json "$REPODIR/rhel-${VERSION_SUFFIX}-beta.json"
-    fi
 fi
 
 # start appropriate units


Test patch:
diff --git a/internal/client/compose_test.go b/internal/client/compose_test.go
index fb224eb47..dd9b5e874 100644
--- a/internal/client/compose_test.go
+++ b/internal/client/compose_test.go
@@ -553,7 +553,7 @@ func TestComposeUnsupportedMountPointV0(t *testing.T) {
 		description="TestComposeUnsupportedMountPointV0"
 		version="0.0.1"
 		[[customizations.filesystem]]
-		mountpoint = "/boot"
+		mountpoint = "/etc"
 		size = 4294967296
 		`
 	resp, err := PostTOMLBlueprintV0(testState.socket, bp)
diff --git a/internal/disk/disk_test.go b/internal/disk/disk_test.go
index 6f9b38d36..7e98ced62 100644
--- a/internal/disk/disk_test.go
+++ b/internal/disk/disk_test.go
@@ -617,6 +617,36 @@ func TestMinimumSizes(t *testing.T) {
 	}
 }
 
+func TestNewBootWithSizeLVMify(t *testing.T) {
+	pt := testPartitionTables["plain-noboot"]
+	assert := assert.New(t)
+
+	// math/rand is good enough in this case
+	/* #nosec G404 */
+	rng := rand.New(rand.NewSource(13))
+
+	custom := []blueprint.FilesystemCustomization{
+		{
+			Mountpoint: "/boot",
+			MinSize:    700 * MiB,
+		},
+	}
+
+	mpt, err := NewPartitionTable(&pt, custom, uint64(3*GiB), true, rng)
+	assert.NoError(err)
+
+	for idx, c := range custom {
+		mnt, minSize := c.Mountpoint, c.MinSize
+		path := entityPath(mpt, mnt)
+		assert.NotNil(path, "[%d] mountpoint %q not found", idx, mnt)
+		parent := path[1]
+		part, ok := parent.(*Partition)
+		assert.True(ok, "%q parent (%v) is not a partition", mnt, parent)
+		assert.GreaterOrEqual(part.GetSize(), minSize,
+			"[%d] %q size %d should be greater or equal to %d", idx, mnt, part.GetSize(), minSize)
+	}
+}
+
 func collectEntities(pt *PartitionTable) []Entity {
 	entities := make([]Entity, 0)
 	collector := func(ent Entity, path []Entity) error {
diff --git a/internal/disk/path_policy_test.go b/internal/disk/path_policy_test.go
new file mode 100644
index 000000000..2d30c1c90
--- /dev/null
+++ b/internal/disk/path_policy_test.go
@@ -0,0 +1,50 @@
+package disk
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPathPolicyCheck(t *testing.T) {
+	assert := assert.New(t)
+
+	entires := map[string]PathPolicy{
+		"/":          {Exact: true},
+		"/boot":      {Exact: true},
+		"/boot/efi":  {Exact: true},
+		"/var":       {},
+		"/var/empty": {Deny: true},
+		"/srv":       {},
+		"/home":      {},
+	}
+
+	policies := NewPathPolicies(entires)
+	assert.NotNil(policies)
+
+	tests := map[string]bool{
+		"/":                true,
+		"/custom":          false,
+		"/boot":            true,
+		"/boot/grub2":      false,
+		"/boot/efi":        true,
+		"/boot/efi/redora": false,
+		"/srv":             true,
+		"/srv/www":         true,
+		"/srv/www/data":    true,
+		"/var":             true,
+		"/var/log":         true,
+		"/var/empty":       false,
+		"/var/empty/dir":   false,
+	}
+
+	for k, v := range tests {
+		err := policies.Check(k)
+		if v {
+			assert.NoError(err)
+		} else {
+			assert.Errorf(err, "unexpected error for path '%s'", k)
+
+		}
+	}
+}
diff --git a/internal/disk/path_tree_test.go b/internal/disk/path_tree_test.go
new file mode 100644
index 000000000..076a451fc
--- /dev/null
+++ b/internal/disk/path_tree_test.go
@@ -0,0 +1,134 @@
+package disk
+
+import (
+	"testing"
+
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewPathTrieFromMap(t *testing.T) {
+	assert := assert.New(t)
+
+	type testCase struct {
+		entries map[string]interface{}
+		trie    *PathTrie
+	}
+
+	tests := []testCase{
+		{
+			entries: map[string]interface{}{},
+			trie: &PathTrie{
+				Name: []string{},
+			},
+		},
+		{
+			entries: map[string]interface{}{
+				"/": common.IntToPtr(1),
+			},
+			trie: &PathTrie{
+				Name:    []string{},
+				Payload: common.IntToPtr(1),
+			},
+		},
+		{
+			entries: map[string]interface{}{
+				"/":                            common.IntToPtr(1),
+				"/var":                         common.IntToPtr(2),
+				"/var/lib/chrony":              common.IntToPtr(3),
+				"/var/lib/chrony/logs":         common.IntToPtr(4),
+				"/var/lib/osbuild":             common.IntToPtr(5),
+				"/var/lib/osbuild/store/cache": common.IntToPtr(6),
+				"/boot":                        common.IntToPtr(7),
+				"/boot/efi":                    common.IntToPtr(8),
+			},
+			trie: &PathTrie{
+				Name:    []string{},
+				Payload: common.IntToPtr(1),
+				Paths: []*PathTrie{
+					{
+						Name:    []string{"boot"},
+						Payload: common.IntToPtr(7),
+						Paths: []*PathTrie{
+							{
+								Name:    []string{"efi"},
+								Payload: common.IntToPtr(8),
+							},
+						},
+					},
+					{
+						Name:    []string{"var"},
+						Payload: common.IntToPtr(2),
+						Paths: []*PathTrie{
+							{
+								Name:    []string{"lib", "chrony"},
+								Payload: common.IntToPtr(3),
+								Paths: []*PathTrie{
+									{
+										Name:    []string{"logs"},
+										Payload: common.IntToPtr(4),
+									},
+								},
+							},
+							{
+								Name:    []string{"lib", "osbuild"},
+								Payload: common.IntToPtr(5),
+								Paths: []*PathTrie{
+									{
+										Name:    []string{"store", "cache"},
+										Payload: common.IntToPtr(6),
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		have := NewPathTrieFromMap(tc.entries)
+		assert.NotNil(have)
+		assert.Equal(tc.trie, have)
+	}
+}
+
+func TestPathTrieLookup(t *testing.T) {
+	assert := assert.New(t)
+
+	entries := map[string]interface{}{
+		"/":                            "/",
+		"/boot":                        "/boot",
+		"/boot/efi":                    "/boot/efi",
+		"/var":                         "/var",
+		"/var/lib/osbuild":             "/var/lib/osbuild",
+		"/var/lib/osbuild/store/cache": "/var/lib/osbuild/store/cache",
+		"/var/lib/chrony":              "/var/lib/chrony",
+		"/var/lib/chrony/logs":         "/var/lib/chrony/logs",
+	}
+
+	trie := NewPathTrieFromMap(entries)
+
+	testCases := map[string]string{
+		"/":                         "/",
+		"/srv":                      "/",
+		"/srv/data":                 "/",
+		"/boot":                     "/boot",
+		"/boot/efi":                 "/boot/efi",
+		"/boot/grub2":               "/boot",
+		"/boot/efi/fedora":          "/boot/efi",
+		"/var/lib/osbuild":          "/var/lib/osbuild",
+		"/var/lib/osbuild/test":     "/var/lib/osbuild",
+		"/var/lib/chrony":           "/var/lib/chrony",
+		"/var/lib/chrony/test":      "/var/lib/chrony",
+		"/var/lib/chrony/logs":      "/var/lib/chrony/logs",
+		"/var/lib/chrony/logs/data": "/var/lib/chrony/logs",
+	}
+
+	for k, v := range testCases {
+		node, _ := trie.Lookup(k)
+		assert.NotNil(node)
+		assert.Equal(v, node.Payload, "Lookup path: '%s' (%+v)", k, node.Name)
+	}
+}
diff --git a/internal/distro/fedora/distro_test.go b/internal/distro/fedora/distro_test.go
index 4dca8ae4f..5ebd87a38 100644
--- a/internal/distro/fedora/distro_test.go
+++ b/internal/distro/fedora/distro_test.go
@@ -529,7 +529,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -544,7 +544,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			} else if imgTypeName == "fedora-iot-installer" {
 				continue
 			} else {
-				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 			}
 		}
 	}
diff --git a/internal/distro/rhel7/distro_test.go b/internal/distro/rhel7/distro_test.go
index b727f941a..24bc87f8e 100644
--- a/internal/distro/rhel7/distro_test.go
+++ b/internal/distro/rhel7/distro_test.go
@@ -275,7 +275,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -285,7 +285,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
-			assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+			assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 		}
 	}
 }
diff --git a/internal/distro/rhel8/distro_test.go b/internal/distro/rhel8/distro_test.go
index ac7827ad9..645adb311 100644
--- a/internal/distro/rhel8/distro_test.go
+++ b/internal/distro/rhel8/distro_test.go
@@ -614,7 +614,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -629,7 +629,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
 				continue
 			} else {
-				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 			}
 		}
 	}
diff --git a/internal/distro/rhel90/distro_test.go b/internal/distro/rhel90/distro_test.go
index c1b586469..56f3aea97 100644
--- a/internal/distro/rhel90/distro_test.go
+++ b/internal/distro/rhel90/distro_test.go
@@ -606,7 +606,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -621,7 +621,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
 				continue
 			} else {
-				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 			}
 		}
 	}
diff --git a/test/cases/filesystem.sh b/test/cases/filesystem.sh
index 1cb86529f..8eb22b865 100644
--- a/test/cases/filesystem.sh
+++ b/test/cases/filesystem.sh
@@ -242,7 +242,7 @@ mountpoint = "/etc"
 size = 131072000
 
 [[customizations.filesystem]]
-mountpoint = "/boot"
+mountpoint = "/lost+found"
 size = 131072000
 
 EOF
@@ -254,7 +254,7 @@ build_image "$BLUEPRINT_FILE" rhel85-custom-filesystem-fail qcow2 true
 FAILED_MOUNTPOINTS=()
 
 greenprint "💬 Checking expected failures"
-for MOUNTPOINT in '/etc' '/boot' ; do
+for MOUNTPOINT in '/etc' '/lost+found' ; do
   if ! [[ $ERROR_MSG == *"$MOUNTPOINT"* ]]; then
     FAILED_MOUNTPOINTS+=("$MOUNTPOINT")
   fi
diff --git a/test/cases/regression-composer-works-behind-satellite-fallback.sh b/test/cases/regression-composer-works-behind-satellite-fallback.sh
index 51f880f58..8fdd541a6 100644
--- a/test/cases/regression-composer-works-behind-satellite-fallback.sh
+++ b/test/cases/regression-composer-works-behind-satellite-fallback.sh
@@ -61,37 +61,12 @@ case "${ID}" in
     "rhel")
         echo "Running on RHEL"
         case "${VERSION_ID%.*}" in
-            "8" )
+            "8" | "9")
                 echo "Running on RHEL ${VERSION_ID}"
                 # starting in 8.5 the override file contains minor version number as well
                 VERSION_SUFFIX=$(echo "${VERSION_ID}" | tr -d ".")
-                if grep beta /etc/os-release;
-                then
-                    DISTRO_NAME="rhel-8-beta"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}-beta.json"
-                else
-                    DISTRO_NAME="rhel-8"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}.json"
-                fi
-                REPO1_NAME="baseos"
-                REPO2_NAME="appstream"
-                if [ -n "${NIGHTLY:-}" ]; then
-                    REPO1_NAME="baseos-${ARCH}"
-                    REPO2_NAME="appstream-${ARCH}"
-                fi
-                ;;
-            "9" )
-                echo "Running on RHEL ${VERSION_ID}"
-                # in 9.0 the override file contains minor version number as well
-                VERSION_SUFFIX=$(echo "${VERSION_ID}" | tr -d ".")
-                if grep beta /etc/os-release;
-                then
-                    DISTRO_NAME="rhel-90-beta"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}-beta.json"
-                else
-                    DISTRO_NAME="rhel-90"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}.json"
-                fi
+                DISTRO_NAME="rhel-${VERSION_SUFFIX}"
+                REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}.json"
                 REPO1_NAME="baseos"
                 REPO2_NAME="appstream"
                 if [ -n "${NIGHTLY:-}" ]; then
diff --git a/test/cases/regression-composer-works-behind-satellite.sh b/test/cases/regression-composer-works-behind-satellite.sh
index 7930a0ab1..beba30480 100644
--- a/test/cases/regression-composer-works-behind-satellite.sh
+++ b/test/cases/regression-composer-works-behind-satellite.sh
@@ -90,37 +90,12 @@ case "${ID}" in
     "rhel")
         echo "Running on RHEL"
         case "${VERSION_ID%.*}" in
-            "8" )
+            "8" | "9")
                 echo "Running on RHEL ${VERSION_ID}"
                 # starting in 8.5 the override file contains minor version number as well
                 VERSION_SUFFIX=$(echo "${VERSION_ID}" | tr -d ".")
-                if grep beta /etc/os-release;
-                then
-                    DISTRO_NAME="rhel-8-beta"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}-beta.json"
-                else
-                    DISTRO_NAME="rhel-8"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}.json"
-                fi
-                REPO1_NAME="baseos"
-                REPO2_NAME="appstream"
-                if [ -n "${NIGHTLY:-}" ]; then
-                    REPO1_NAME="baseos-${ARCH}"
-                    REPO2_NAME="appstream-${ARCH}"
-                fi
-                ;;
-            "9" )
-                echo "Running on RHEL ${VERSION_ID}"
-                # in 9.0 the override file contains minor version number as well
-                VERSION_SUFFIX=$(echo "${VERSION_ID}" | tr -d ".")
-                if grep beta /etc/os-release;
-                then
-                    DISTRO_NAME="rhel-90-beta"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}-beta.json"
-                else
-                    DISTRO_NAME="rhel-90"
-                    REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}.json"
-                fi
+                DISTRO_NAME="rhel-${VERSION_SUFFIX}"
+                REPOSITORY_OVERRIDE="/etc/osbuild-composer/repositories/rhel-${VERSION_SUFFIX}.json"
                 REPO1_NAME="baseos"
                 REPO2_NAME="appstream"
                 if [ -n "${NIGHTLY:-}" ]; then