Repository: osbuild/osbuild-composer
Pull request #3319 (2023-03-02 11:37:06)

weldr+distro: allow to send Manifest warnings on ComposeReply
This adds a new field 'Warnings' to the `ComposeReply` struct, allowing to send back any warnings (e.g. deprecation notices, see #3285) when a compose build is requested.

This also changes the `Manifest` function on the `ImageType` interface so that any warnings detected during the `checkOptions` step of the manifest initialization are propagated back to the Welder API.

See also https://github.com/osbuild/weldr-client/pull/99 which handles the welder-client side of things.


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.
-->


7flying: Taking care of the tests that I've missed to update
7flying: @achilleas-k can you review this while I re-work the `welr-client` side of things with Brian?
7flying: Hopefully nils everywhere now :crossed_fingers: 
bcl: I see that these changes are necessary to get the values to percolate back up to the top, but it seems like quite a bit of change. Looking at the number of places where warning is ignored/nil, I wonder if it would be cleaner to switch to a custom error+warning return value? The drawback there being that it would make error checking harder than ` err != nil`. But if it returned an empty `ErrWarn{}` struct instead of nil the check could be something like `ret.IsError()` and `ret.IsWarning()`

7flying: > I see that these changes are necessary to get the values to percolate back up to the top, but it seems like quite a bit of change. Looking at the number of places where warning is ignored/nil, I wonder if it would be cleaner to switch to a custom error+warning return value? The drawback there being that it would make error checking harder than ` err != nil`. But if it returned an empty `ErrWarn{}` struct instead of nil the check could be something like `ret.IsError()` and `ret.IsWarning()`

@achilleas-k @thozza I'll be happy to rewrite the error handling as Brian suggests if that makes the change a more "permanent" solution. What do you think?
supakeen: > > Since all the codechanges have been gone through I won't get too much into that.
> > I just have a single question: does it make sense to return `nil, nil, err` even in cases where `warnings` _is_ available, e.g. when `InstantiateManifest` fails `warnings` are available but not returned.
> 
> The reasoning that we made in all those instances is that when we have an `err`, we only care about returning the error since we are just going to handle that, and not the warnings. Warnings are returned when everything else is OK and we simply need to bring something to the attention of the user.

Makes sense.
7flying: rebased on main, fixed file conflicts
runcom: > Still looks good

restarted the test failing
thozza: > rebased on main and fixed conflicts
> 
> @thozza can I ping you once all the tests are green so that you can push the merge button?

Sure. Just ping me once CI is green and I'll review, approve and merge...
7flying: @thozza this is the

> > rebased on main and fixed conflicts
> > @thozza can I ping you once all the tests are green so that you can push the merge button?
> 
> Sure. Just ping me once CI is green and I'll review, approve and merge...

@thozza this is the all-green ping

Patch:
diff --git a/cmd/gen-manifests/main.go b/cmd/gen-manifests/main.go
index 22ebc2508..47caf9027 100644
--- a/cmd/gen-manifests/main.go
+++ b/cmd/gen-manifests/main.go
@@ -164,7 +164,7 @@ func makeManifestJob(name string, imgType distro.ImageType, cr composeRequest, d
 			err = fmt.Errorf("[%s] nil package specs", filename)
 			return
 		}
-		manifest, err := imgType.Manifest(cr.Blueprint.Customizations, options, repos, packageSpecs, containerSpecs, seedArg)
+		manifest, _, err := imgType.Manifest(cr.Blueprint.Customizations, options, repos, packageSpecs, containerSpecs, seedArg)
 		if err != nil {
 			err = fmt.Errorf("[%s] failed: %s", filename, err)
 			return
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index ed4c1e459..bc434bd5a 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -214,7 +214,7 @@ func main() {
 			composeRequest.OSTree.Ref = imageType.OSTreeRef()
 		}
 
-		manifest, err := imageType.Manifest(composeRequest.Blueprint.Customizations,
+		manifest, _, err := imageType.Manifest(composeRequest.Blueprint.Customizations,
 			options,
 			repos,
 			depsolvedSets,
diff --git a/cmd/osbuild-store-dump/main.go b/cmd/osbuild-store-dump/main.go
index df71afb40..c57357f27 100644
--- a/cmd/osbuild-store-dump/main.go
+++ b/cmd/osbuild-store-dump/main.go
@@ -30,7 +30,7 @@ func getManifest(bp blueprint.Blueprint, t distro.ImageType, a distro.Arch, d di
 		}
 		pkgSpecSets[name] = res
 	}
-	manifest, err := t.Manifest(bp.Customizations, distro.ImageOptions{}, repos, pkgSpecSets, nil, 0)
+	manifest, _, err := t.Manifest(bp.Customizations, distro.ImageOptions{}, repos, pkgSpecSets, nil, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/internal/cloudapi/v2/server.go b/internal/cloudapi/v2/server.go
index fdae5eeae..7c863c36d 100644
--- a/internal/cloudapi/v2/server.go
+++ b/internal/cloudapi/v2/server.go
@@ -458,7 +458,7 @@ func generateManifest(ctx context.Context, workers *worker.Server, depsolveJobID
 		options.OSTree.URL = result.Specs[0].URL
 	}
 
-	manifest, err := imageType.Manifest(b, options, repos, depsolveResults.PackageSpecs, containerSpecs, seed)
+	manifest, _, err := imageType.Manifest(b, options, repos, depsolveResults.PackageSpecs, containerSpecs, seed)
 	if err != nil {
 		reason := "Error generating manifest"
 		jobResult.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestGeneration, reason, nil)
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index c40c55186..e81489c99 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -118,9 +118,10 @@ type ImageType interface {
 
 	// Returns an osbuild manifest, containing the sources and pipeline necessary
 	// to build an image, given output format with all packages and customizations
-	// specified in the given blueprint. The packageSpecSets must be labelled in
-	// the same way as the originating PackageSets.
-	Manifest(b *blueprint.Customizations, options ImageOptions, repos []rpmmd.RepoConfig, packageSpecSets map[string][]rpmmd.PackageSpec, containers []container.Spec, seed int64) (Manifest, error)
+	// specified in the given blueprint; it also returns any warnings (e.g.
+	// deprecation notices) generated by the manifest.
+	// The packageSpecSets must be labelled in the same way as the originating PackageSets.
+	Manifest(b *blueprint.Customizations, options ImageOptions, repos []rpmmd.RepoConfig, packageSpecSets map[string][]rpmmd.PackageSpec, containers []container.Spec, seed int64) (Manifest, []string, error)
 }
 
 // The ImageOptions specify options for a specific image build
diff --git a/internal/distro/fedora/distro.go b/internal/distro/fedora/distro.go
index ccc083418..d6c1b5d61 100644
--- a/internal/distro/fedora/distro.go
+++ b/internal/distro/fedora/distro.go
@@ -612,7 +612,7 @@ func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOpti
 	}
 
 	// create a manifest object and instantiate it with the computed packageSetChains
-	manifest, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
+	manifest, _, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
 	if err != nil {
 		// TODO: handle manifest initialization errors more gracefully, we
 		// refuse to initialize manifests with invalid config.
@@ -687,10 +687,11 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string]rpmmd.PackageSet,
 	containers []container.Spec,
-	seed int64) (*manifest.Manifest, error) {
+	seed int64) (*manifest.Manifest, []string, error) {
 
-	if err := t.checkOptions(bp.Customizations, options, containers); err != nil {
-		return nil, err
+	warnings, err := t.checkOptions(bp.Customizations, options, containers)
+	if err != nil {
+		return nil, nil, err
 	}
 
 	// TODO: let image types specify valid workloads, rather than
@@ -713,14 +714,14 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 
 	img, err := t.image(w, t, bp.Customizations, options, packageSets, containers, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	manifest := manifest.New()
 	_, err = img.InstantiateManifest(&manifest, repos, t.arch.distro.runner, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	return &manifest, err
+	return &manifest, warnings, err
 }
 
 func (t *imageType) Manifest(customizations *blueprint.Customizations,
@@ -728,7 +729,7 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string][]rpmmd.PackageSpec,
 	containers []container.Spec,
-	seed int64) (distro.Manifest, error) {
+	seed int64) (distro.Manifest, []string, error) {
 
 	bp := &blueprint.Blueprint{Name: "empty blueprint"}
 	err := bp.Initialize()
@@ -755,26 +756,31 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 		mergedRepos = append(mergedRepos, repo)
 	}
 
-	manifest, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
+	manifest, warnings, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
 	if err != nil {
-		return distro.Manifest{}, err
+		return distro.Manifest{}, nil, err
 	}
 
-	return manifest.Serialize(packageSets)
+	ret, err := manifest.Serialize(packageSets)
+	if err != nil {
+		return ret, nil, err
+	}
+	return ret, warnings, err
 }
 
 // checkOptions checks the validity and compatibility of options and customizations for the image type.
-func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) error {
+// Returns ([]string, error) where []string, if non-nil, will hold any generated warnings (e.g. deprecation notices).
+func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) ([]string, error) {
 
 	// we do not support embedding containers on ostree-derived images, only on commits themselves
 	if len(containers) > 0 && t.rpmOstree && (t.name != "iot-commit" && t.name != "iot-container") {
-		return fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
+		return nil, fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
 	}
 
 	if t.bootISO && t.rpmOstree {
 		// check the checksum instead of the URL, because the URL should have been used to resolve the checksum and we need both
 		if options.OSTree.FetchChecksum == "" {
-			return fmt.Errorf("boot ISO image type %q requires specifying a URL from which to retrieve the OSTree commit", t.name)
+			return nil, fmt.Errorf("boot ISO image type %q requires specifying a URL from which to retrieve the OSTree commit", t.name)
 		}
 	}
 
@@ -784,39 +790,39 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		if t.name == "iot-installer" || t.name == "image-installer" {
 			allowed := []string{"User", "Group"}
 			if err := customizations.CheckAllowed(allowed...); err != nil {
-				return fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
+				return nil, fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
 			}
 		}
 	}
 
 	if kernelOpts := customizations.GetKernel(); kernelOpts.Append != "" && t.rpmOstree {
-		return fmt.Errorf("kernel boot parameter customizations are not supported for ostree types")
+		return nil, fmt.Errorf("kernel boot parameter customizations are not supported for ostree types")
 	}
 
 	mountpoints := customizations.GetFilesystems()
 
 	if mountpoints != nil && t.rpmOstree {
-		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
+		return nil, fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
 	err := blueprint.CheckMountpointsPolicy(mountpoints, pathpolicy.MountpointPolicies)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
 		supported := oscap.IsProfileAllowed(osc.ProfileID, oscapProfileAllowList)
 		if !supported {
-			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
+			return nil, fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
 		}
 		if t.rpmOstree {
-			return fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
+			return nil, fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
 		}
 		if osc.DataStream == "" {
-			return fmt.Errorf("OpenSCAP datastream cannot be empty")
+			return nil, fmt.Errorf("OpenSCAP datastream cannot be empty")
 		}
 		if osc.ProfileID == "" {
-			return fmt.Errorf("OpenSCAP profile cannot be empty")
+			return nil, fmt.Errorf("OpenSCAP profile cannot be empty")
 		}
 	}
 
@@ -826,20 +832,20 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 
 	err = blueprint.ValidateDirFileCustomizations(dc, fc)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	err = blueprint.CheckDirectoryCustomizationsPolicy(dc, pathpolicy.CustomDirectoriesPolicies)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	err = blueprint.CheckFileCustomizationsPolicy(fc, pathpolicy.CustomFilesPolicies)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	return nil
+	return nil, nil
 }
 
 // New creates a new distro object, defining the supported architectures and image types
diff --git a/internal/distro/rhel7/distro.go b/internal/distro/rhel7/distro.go
index 969e38ae9..8208536af 100644
--- a/internal/distro/rhel7/distro.go
+++ b/internal/distro/rhel7/distro.go
@@ -341,10 +341,11 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string]rpmmd.PackageSet,
 	containers []container.Spec,
-	seed int64) (*manifest.Manifest, error) {
+	seed int64) (*manifest.Manifest, []string, error) {
 
-	if err := t.checkOptions(bp.Customizations, options, containers); err != nil {
-		return nil, err
+	warnings, err := t.checkOptions(bp.Customizations, options, containers)
+	if err != nil {
+		return nil, nil, err
 	}
 
 	w := t.workload
@@ -368,18 +369,18 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 	rng := rand.New(source)
 
 	if t.image == nil {
-		return nil, nil
+		return nil, nil, nil
 	}
 	img, err := t.image(w, t, bp.Customizations, options, packageSets, containers, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	manifest := manifest.New()
 	_, err = img.InstantiateManifest(&manifest, repos, t.arch.distro.runner, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	return &manifest, err
+	return &manifest, warnings, err
 }
 
 func (t *imageType) Manifest(customizations *blueprint.Customizations,
@@ -387,7 +388,7 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string][]rpmmd.PackageSpec,
 	containers []container.Spec,
-	seed int64) (distro.Manifest, error) {
+	seed int64) (distro.Manifest, []string, error) {
 
 	bp := &blueprint.Blueprint{Name: "empty blueprint"}
 	err := bp.Initialize()
@@ -414,12 +415,16 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 		mergedRepos = append(mergedRepos, repo)
 	}
 
-	manifest, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
+	manifest, warnings, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
 	if err != nil {
-		return distro.Manifest{}, err
+		return distro.Manifest{}, nil, err
 	}
 
-	return manifest.Serialize(packageSets)
+	ret, err := manifest.Serialize(packageSets)
+	if err != nil {
+		return ret, nil, err
+	}
+	return ret, warnings, err
 }
 
 func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOptions, repos []rpmmd.RepoConfig) map[string][]rpmmd.PackageSet {
@@ -460,7 +465,7 @@ func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOpti
 	}
 
 	// create a manifest object and instantiate it with the computed packageSetChains
-	manifest, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
+	manifest, _, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
 	if err != nil {
 		// TODO: handle manifest initialization errors more gracefully, we
 		// refuse to initialize manifests with invalid config.
@@ -504,8 +509,10 @@ func overridePackageNames(packages []string) []string {
 }
 
 // checkOptions checks the validity and compatibility of options and customizations for the image type.
-func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) error {
-
+// Returns ([]string, error) where []string, if non-nil, will hold any generated warnings (e.g. deprecation notices).
+func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) ([]string, error) {
+	// holds warnings (e.g. deprecation notices)
+	var warnings []string
 	if t.workload != nil {
 		// For now, if an image type defines its own workload, don't allow any
 		// user customizations.
@@ -513,23 +520,23 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		// set of customizations.  The current set of customizations defined in
 		// the blueprint spec corresponds to the Custom workflow.
 		if customizations != nil {
-			return fmt.Errorf("image type %q does not support customizations", t.name)
+			return warnings, fmt.Errorf("image type %q does not support customizations", t.name)
 		}
 	}
 
 	if len(containers) > 0 {
-		return fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
+		return warnings, fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
 	}
 
 	mountpoints := customizations.GetFilesystems()
 
 	err := blueprint.CheckMountpointsPolicy(mountpoints, pathpolicy.MountpointPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
-		return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
+		return warnings, fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
 	}
 
 	// Check Directory/File Customizations are valid
@@ -538,20 +545,20 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 
 	err = blueprint.ValidateDirFileCustomizations(dc, fc)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	err = blueprint.CheckDirectoryCustomizationsPolicy(dc, pathpolicy.CustomDirectoriesPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	err = blueprint.CheckFileCustomizationsPolicy(fc, pathpolicy.CustomFilesPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
-	return nil
+	return warnings, nil
 }
 
 // New creates a new distro object, defining the supported architectures and image types
diff --git a/internal/distro/rhel8/imagetype.go b/internal/distro/rhel8/imagetype.go
index 59431698d..8f1ad8e8f 100644
--- a/internal/distro/rhel8/imagetype.go
+++ b/internal/distro/rhel8/imagetype.go
@@ -188,10 +188,11 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string]rpmmd.PackageSet,
 	containers []container.Spec,
-	seed int64) (*manifest.Manifest, error) {
+	seed int64) (*manifest.Manifest, []string, error) {
 
-	if err := t.checkOptions(bp.Customizations, options, containers); err != nil {
-		return nil, err
+	warnings, err := t.checkOptions(bp.Customizations, options, containers)
+	if err != nil {
+		return nil, nil, err
 	}
 
 	w := t.workload
@@ -215,18 +216,18 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 	rng := rand.New(source)
 
 	if t.image == nil {
-		return nil, nil
+		return nil, nil, nil
 	}
 	img, err := t.image(w, t, bp.Customizations, options, packageSets, containers, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	manifest := manifest.New()
 	_, err = img.InstantiateManifest(&manifest, repos, t.arch.distro.runner, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	return &manifest, err
+	return &manifest, warnings, err
 }
 
 func (t *imageType) Manifest(customizations *blueprint.Customizations,
@@ -234,7 +235,7 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string][]rpmmd.PackageSpec,
 	containers []container.Spec,
-	seed int64) (distro.Manifest, error) {
+	seed int64) (distro.Manifest, []string, error) {
 
 	bp := &blueprint.Blueprint{Name: "empty blueprint"}
 	err := bp.Initialize()
@@ -261,12 +262,16 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 		mergedRepos = append(mergedRepos, repo)
 	}
 
-	manifest, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
+	manifest, warnings, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
 	if err != nil {
-		return distro.Manifest{}, err
+		return distro.Manifest{}, nil, err
 	}
 
-	return manifest.Serialize(packageSets)
+	ret, err := manifest.Serialize(packageSets)
+	if err != nil {
+		return ret, nil, err
+	}
+	return ret, warnings, err
 }
 
 func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOptions, repos []rpmmd.RepoConfig) map[string][]rpmmd.PackageSet {
@@ -325,7 +330,7 @@ func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOpti
 	}
 
 	// create a manifest object and instantiate it with the computed packageSetChains
-	manifest, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
+	manifest, _, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
 	if err != nil {
 		// TODO: handle manifest initialization errors more gracefully, we
 		// refuse to initialize manifests with invalid config.
@@ -369,8 +374,10 @@ func overridePackageNames(packages []string) []string {
 }
 
 // checkOptions checks the validity and compatibility of options and customizations for the image type.
-func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) error {
-
+// Returns ([]string, error) where []string, if non-nil, will hold any generated warnings (e.g. deprecation notices).
+func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) ([]string, error) {
+	// holds warnings (e.g. deprecation notices)
+	var warnings []string
 	if t.workload != nil {
 		// For now, if an image type defines its own workload, don't allow any
 		// user customizations.
@@ -378,33 +385,32 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		// set of customizations.  The current set of customizations defined in
 		// the blueprint spec corresponds to the Custom workflow.
 		if customizations != nil {
-			return fmt.Errorf("image type %q does not support customizations", t.name)
+			return warnings, fmt.Errorf("image type %q does not support customizations", t.name)
 		}
 	}
-
 	// we do not support embedding containers on ostree-derived images, only on commits themselves
 	if len(containers) > 0 && t.rpmOstree && (t.name != "edge-commit" && t.name != "edge-container") {
-		return fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
+		return warnings, fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
 	}
 
 	if t.bootISO && t.rpmOstree {
 		// check the checksum instead of the URL, because the URL should have been used to resolve the checksum and we need both
 		if options.OSTree.FetchChecksum == "" {
-			return fmt.Errorf("boot ISO image type %q requires specifying a URL from which to retrieve the OSTree commit", t.name)
+			return warnings, fmt.Errorf("boot ISO image type %q requires specifying a URL from which to retrieve the OSTree commit", t.name)
 		}
 
 		if t.name == "edge-simplified-installer" {
 			allowed := []string{"InstallationDevice", "FDO", "User", "Group"}
 			if err := customizations.CheckAllowed(allowed...); err != nil {
-				return fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
+				return warnings, fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
 			}
 			if customizations.GetInstallationDevice() == "" {
-				return fmt.Errorf("boot ISO image type %q requires specifying an installation device to install to", t.name)
+				return warnings, fmt.Errorf("boot ISO image type %q requires specifying an installation device to install to", t.name)
 			}
 			//making fdo optional so that simplified installer can be composed w/o the FDO section in the blueprint
 			if customizations.GetFDO() != nil {
 				if customizations.GetFDO().ManufacturingServerURL == "" {
-					return fmt.Errorf("boot ISO image type %q requires specifying FDO.ManufacturingServerURL configuration to install to", t.name)
+					return warnings, fmt.Errorf("boot ISO image type %q requires specifying FDO.ManufacturingServerURL configuration to install to", t.name)
 				}
 				var diunSet int
 				if customizations.GetFDO().DiunPubKeyHash != "" {
@@ -417,65 +423,69 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 					diunSet++
 				}
 				if diunSet != 1 {
-					return fmt.Errorf("boot ISO image type %q requires specifying one of [FDO.DiunPubKeyHash,FDO.DiunPubKeyInsecure,FDO.DiunPubKeyRootCerts] configuration to install to", t.name)
+					return warnings, fmt.Errorf("boot ISO image type %q requires specifying one of [FDO.DiunPubKeyHash,FDO.DiunPubKeyInsecure,FDO.DiunPubKeyRootCerts] configuration to install to", t.name)
 				}
 			}
 		} else if t.name == "edge-installer" {
 			allowed := []string{"User", "Group"}
 			if err := customizations.CheckAllowed(allowed...); err != nil {
-				return fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
+				return warnings, fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
 			}
 		}
 	}
 
 	// check the checksum instead of the URL, because the URL should have been used to resolve the checksum and we need both
 	if t.name == "edge-raw-image" && options.OSTree.FetchChecksum == "" {
-		return fmt.Errorf("edge raw images require specifying a URL from which to retrieve the OSTree commit")
+		return warnings, fmt.Errorf("edge raw images require specifying a URL from which to retrieve the OSTree commit")
 	}
 
 	// warn that user & group customizations on edge-commit, edge-container are deprecated
 	// TODO(edge): directly error if these options are provided when rhel-9.5's time arrives
 	if t.name == "edge-commit" || t.name == "edge-container" {
 		if customizations.GetUsers() != nil {
-			log.Printf("Please note that user customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			w := fmt.Sprintf("Please note that user customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			log.Print(w)
+			warnings = append(warnings, w)
 		}
 		if customizations.GetGroups() != nil {
-			log.Printf("Please note that group customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			w := fmt.Sprintf("Please note that group customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			log.Print(w)
+			warnings = append(warnings, w)
 		}
 	}
 
 	if kernelOpts := customizations.GetKernel(); kernelOpts.Append != "" && t.rpmOstree && (!t.bootable || t.bootISO) {
-		return fmt.Errorf("kernel boot parameter customizations are not supported for ostree types")
+		return warnings, fmt.Errorf("kernel boot parameter customizations are not supported for ostree types")
 	}
 
 	mountpoints := customizations.GetFilesystems()
 
 	if mountpoints != nil && t.rpmOstree {
-		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
+		return warnings, fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
 	err := blueprint.CheckMountpointsPolicy(mountpoints, pathpolicy.MountpointPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
 		// only add support for RHEL 8.7 and above. centos not supported.
 		if !t.arch.distro.isRHEL() || common.VersionLessThan(t.arch.distro.osVersion, "8.7") {
-			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
+			return warnings, fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
 		}
 		supported := oscap.IsProfileAllowed(osc.ProfileID, oscapProfileAllowList)
 		if !supported {
-			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
+			return warnings, fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
 		}
 		if t.rpmOstree {
-			return fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
+			return warnings, fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
 		}
 		if osc.DataStream == "" {
-			return fmt.Errorf("OpenSCAP datastream cannot be empty")
+			return warnings, fmt.Errorf("OpenSCAP datastream cannot be empty")
 		}
 		if osc.ProfileID == "" {
-			return fmt.Errorf("OpenSCAP profile cannot be empty")
+			return warnings, fmt.Errorf("OpenSCAP profile cannot be empty")
 		}
 	}
 
@@ -485,18 +495,18 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 
 	err = blueprint.ValidateDirFileCustomizations(dc, fc)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	err = blueprint.CheckDirectoryCustomizationsPolicy(dc, pathpolicy.CustomDirectoriesPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	err = blueprint.CheckFileCustomizationsPolicy(fc, pathpolicy.CustomFilesPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
-	return nil
+	return warnings, nil
 }
diff --git a/internal/distro/rhel9/imagetype.go b/internal/distro/rhel9/imagetype.go
index cef75f1b2..9339e5c89 100644
--- a/internal/distro/rhel9/imagetype.go
+++ b/internal/distro/rhel9/imagetype.go
@@ -191,10 +191,11 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string]rpmmd.PackageSet,
 	containers []container.Spec,
-	seed int64) (*manifest.Manifest, error) {
+	seed int64) (*manifest.Manifest, []string, error) {
 
-	if err := t.checkOptions(bp.Customizations, options, containers); err != nil {
-		return nil, err
+	warnings, err := t.checkOptions(bp.Customizations, options, containers)
+	if err != nil {
+		return nil, nil, err
 	}
 
 	w := t.workload
@@ -219,14 +220,14 @@ func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
 
 	img, err := t.image(w, t, bp.Customizations, options, packageSets, containers, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	manifest := manifest.New()
 	_, err = img.InstantiateManifest(&manifest, repos, t.arch.distro.runner, rng)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	return &manifest, err
+	return &manifest, warnings, err
 }
 
 func (t *imageType) Manifest(customizations *blueprint.Customizations,
@@ -234,7 +235,7 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 	repos []rpmmd.RepoConfig,
 	packageSets map[string][]rpmmd.PackageSpec,
 	containers []container.Spec,
-	seed int64) (distro.Manifest, error) {
+	seed int64) (distro.Manifest, []string, error) {
 
 	bp := &blueprint.Blueprint{Name: "empty blueprint"}
 	err := bp.Initialize()
@@ -261,12 +262,16 @@ func (t *imageType) Manifest(customizations *blueprint.Customizations,
 		mergedRepos = append(mergedRepos, repo)
 	}
 
-	manifest, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
+	manifest, warnings, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
 	if err != nil {
-		return distro.Manifest{}, err
+		return distro.Manifest{}, nil, err
 	}
 
-	return manifest.Serialize(packageSets)
+	ret, err := manifest.Serialize(packageSets)
+	if err != nil {
+		return ret, nil, err
+	}
+	return ret, warnings, err
 }
 
 func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOptions, repos []rpmmd.RepoConfig) map[string][]rpmmd.PackageSet {
@@ -325,7 +330,7 @@ func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOpti
 	}
 
 	// create a manifest object and instantiate it with the computed packageSetChains
-	manifest, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
+	manifest, _, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
 	if err != nil {
 		// TODO: handle manifest initialization errors more gracefully, we
 		// refuse to initialize manifests with invalid config.
@@ -337,8 +342,10 @@ func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOpti
 }
 
 // checkOptions checks the validity and compatibility of options and customizations for the image type.
-func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) error {
-
+// Returns ([]string, error) where []string, if non-nil, will hold any generated warnings (e.g. deprecation notices).
+func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) ([]string, error) {
+	// holds warnings (e.g. deprecation notices)
+	var warnings []string
 	if t.workload != nil {
 		// For now, if an image type defines its own workload, don't allow any
 		// user customizations.
@@ -346,34 +353,34 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		// set of customizations.  The current set of customizations defined in
 		// the blueprint spec corresponds to the Custom workflow.
 		if customizations != nil {
-			return fmt.Errorf("image type %q does not support customizations", t.name)
+			return warnings, fmt.Errorf("image type %q does not support customizations", t.name)
 		}
 	}
 
 	// we do not support embedding containers on ostree-derived images, only on commits themselves
 	if len(containers) > 0 && t.rpmOstree && (t.name != "edge-commit" && t.name != "edge-container") {
-		return fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
+		return warnings, fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
 	}
 
 	if t.bootISO && t.rpmOstree {
 		// check the checksum instead of the URL, because the URL should have been used to resolve the checksum and we need both
 		if options.OSTree.FetchChecksum == "" {
-			return fmt.Errorf("boot ISO image type %q requires specifying a URL from which to retrieve the OSTree commit", t.name)
+			return warnings, fmt.Errorf("boot ISO image type %q requires specifying a URL from which to retrieve the OSTree commit", t.name)
 		}
 
 		if t.name == "edge-simplified-installer" {
 			allowed := []string{"InstallationDevice", "FDO", "Ignition", "Kernel", "User", "Group"}
 			if err := customizations.CheckAllowed(allowed...); err != nil {
-				return fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
+				return warnings, fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
 			}
 			if customizations.GetInstallationDevice() == "" {
-				return fmt.Errorf("boot ISO image type %q requires specifying an installation device to install to", t.name)
+				return warnings, fmt.Errorf("boot ISO image type %q requires specifying an installation device to install to", t.name)
 			}
 
 			// FDO is optional, but when specified has some restrictions
 			if customizations.GetFDO() != nil {
 				if customizations.GetFDO().ManufacturingServerURL == "" {
-					return fmt.Errorf("boot ISO image type %q requires specifying FDO.ManufacturingServerURL configuration to install to when using FDO", t.name)
+					return warnings, fmt.Errorf("boot ISO image type %q requires specifying FDO.ManufacturingServerURL configuration to install to when using FDO", t.name)
 				}
 				var diunSet int
 				if customizations.GetFDO().DiunPubKeyHash != "" {
@@ -386,73 +393,77 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 					diunSet++
 				}
 				if diunSet != 1 {
-					return fmt.Errorf("boot ISO image type %q requires specifying one of [FDO.DiunPubKeyHash,FDO.DiunPubKeyInsecure,FDO.DiunPubKeyRootCerts] configuration to install to when using FDO", t.name)
+					return warnings, fmt.Errorf("boot ISO image type %q requires specifying one of [FDO.DiunPubKeyHash,FDO.DiunPubKeyInsecure,FDO.DiunPubKeyRootCerts] configuration to install to when using FDO", t.name)
 				}
 			}
 
 			// ignition is optional, we might be using FDO
 			if customizations.GetIgnition() != nil {
 				if customizations.GetIgnition().Embedded != nil && customizations.GetIgnition().FirstBoot != nil {
-					return fmt.Errorf("both ignition embedded and firstboot configurations found")
+					return warnings, fmt.Errorf("both ignition embedded and firstboot configurations found")
 				}
 				if customizations.GetIgnition().FirstBoot != nil && customizations.GetIgnition().FirstBoot.ProvisioningURL == "" {
-					return fmt.Errorf("ignition.firstboot requires a provisioning url")
+					return warnings, fmt.Errorf("ignition.firstboot requires a provisioning url")
 				}
 			}
 		} else if t.name == "edge-installer" {
 			allowed := []string{"User", "Group"}
 			if err := customizations.CheckAllowed(allowed...); err != nil {
-				return fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
+				return warnings, fmt.Errorf("unsupported blueprint customizations found for boot ISO image type %q: (allowed: %s)", t.name, strings.Join(allowed, ", "))
 			}
 		}
 	}
 
 	// check the checksum instead of the URL, because the URL should have been used to resolve the checksum and we need both
 	if t.name == "edge-raw-image" && options.OSTree.FetchChecksum == "" {
-		return fmt.Errorf("edge raw images require specifying a URL from which to retrieve the OSTree commit")
+		return warnings, fmt.Errorf("edge raw images require specifying a URL from which to retrieve the OSTree commit")
 	}
 
 	// warn that user & group customizations on edge-commit, edge-container are deprecated
 	// TODO(edge): directly error if these options are provided when rhel-9.5's time arrives
 	if t.name == "edge-commit" || t.name == "edge-container" {
 		if customizations.GetUsers() != nil {
-			log.Printf("Please note that user customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			w := fmt.Sprintf("Please note that user customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			log.Print(w)
+			warnings = append(warnings, w)
 		}
 		if customizations.GetGroups() != nil {
-			log.Printf("Please note that group customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			w := fmt.Sprintf("Please note that group customizations on %q image type are deprecated and will be removed in the near future\n", t.name)
+			log.Print(w)
+			warnings = append(warnings, w)
 		}
 	}
 
 	if kernelOpts := customizations.GetKernel(); kernelOpts.Append != "" && t.rpmOstree && t.name != "edge-raw-image" && t.name != "edge-simplified-installer" {
-		return fmt.Errorf("kernel boot parameter customizations are not supported for ostree types")
+		return warnings, fmt.Errorf("kernel boot parameter customizations are not supported for ostree types")
 	}
 
 	mountpoints := customizations.GetFilesystems()
 
 	if mountpoints != nil && t.rpmOstree {
-		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
+		return warnings, fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
 	err := blueprint.CheckMountpointsPolicy(mountpoints, pathpolicy.MountpointPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
 		if t.arch.distro.osVersion == "9.0" {
-			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
+			return warnings, fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
 		}
 		if !oscap.IsProfileAllowed(osc.ProfileID, oscapProfileAllowList) {
-			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
+			return warnings, fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
 		}
 		if t.rpmOstree {
-			return fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
+			return warnings, fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
 		}
 		if osc.DataStream == "" {
-			return fmt.Errorf("OpenSCAP datastream cannot be empty")
+			return warnings, fmt.Errorf("OpenSCAP datastream cannot be empty")
 		}
 		if osc.ProfileID == "" {
-			return fmt.Errorf("OpenSCAP profile cannot be empty")
+			return warnings, fmt.Errorf("OpenSCAP profile cannot be empty")
 		}
 	}
 
@@ -462,17 +473,17 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 
 	err = blueprint.ValidateDirFileCustomizations(dc, fc)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 	err = blueprint.CheckDirectoryCustomizationsPolicy(dc, pathpolicy.CustomDirectoriesPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
 	err = blueprint.CheckFileCustomizationsPolicy(fc, pathpolicy.CustomFilesPolicies)
 	if err != nil {
-		return err
+		return warnings, err
 	}
 
-	return nil
+	return warnings, nil
 }
diff --git a/internal/store/fixtures.go b/internal/store/fixtures.go
index 93d936a66..15ddc69ab 100644
--- a/internal/store/fixtures.go
+++ b/internal/store/fixtures.go
@@ -51,7 +51,7 @@ func FixtureBase() *Store {
 	if err != nil {
 		panic(fmt.Sprintf("failed to get image type %s for a test distro architecture: %v", test_distro.TestImageTypeName, err))
 	}
-	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
+	manifest, _, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	if err != nil {
 		panic(fmt.Sprintf("failed to create a manifest: %v", err))
 	}
@@ -188,7 +188,7 @@ func FixtureFinished() *Store {
 	if err != nil {
 		panic(fmt.Sprintf("failed to get image type %s for a test distro architecture: %v", test_distro.TestImageTypeName, err))
 	}
-	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
+	manifest, _, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	if err != nil {
 		panic(fmt.Sprintf("failed to create a manifest: %v", err))
 	}
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 50425fcc3..4f473743b 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -2328,8 +2328,9 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		Upload        *uploadRequest       `json:"upload"`
 	}
 	type ComposeReply struct {
-		BuildID uuid.UUID `json:"build_id"`
-		Status  bool      `json:"status"`
+		BuildID  uuid.UUID `json:"build_id"`
+		Status   bool      `json:"status"`
+		Warnings []string  `json:"warnings"`
 	}
 
 	contentType := request.Header["Content-Type"]
@@ -2499,7 +2500,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
-	manifest, err := imageType.Manifest(bp.Customizations,
+	manifest, warnings, err := imageType.Manifest(bp.Customizations,
 		options,
 		imageRepos,
 		packageSets,
@@ -2558,8 +2559,9 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 	}
 
 	err = json.NewEncoder(writer).Encode(ComposeReply{
-		BuildID: composeID,
-		Status:  true,
+		BuildID:  composeID,
+		Status:   true,
+		Warnings: warnings,
 	})
 	common.PanicOnError(err)
 }


Test patch:
diff --git a/internal/distro/distro_test.go b/internal/distro/distro_test.go
index aebb07729..db71b4a2e 100644
--- a/internal/distro/distro_test.go
+++ b/internal/distro/distro_test.go
@@ -231,7 +231,7 @@ func TestImageTypePipelineNames(t *testing.T) {
 						packageSets[plName] = minimalPackageSet
 					}
 
-					m, err := imageType.Manifest(bp.Customizations, options, repos, packageSets, containers, seed)
+					m, _, err := imageType.Manifest(bp.Customizations, options, repos, packageSets, containers, seed)
 					require.NoError(err)
 					pm := new(manifest)
 					err = json.Unmarshal(m, pm)
diff --git a/internal/distro/distro_test_common/distro_test_common.go b/internal/distro/distro_test_common/distro_test_common.go
index b29e1279b..7a458abdc 100644
--- a/internal/distro/distro_test_common/distro_test_common.go
+++ b/internal/distro/distro_test_common/distro_test_common.go
@@ -136,7 +136,7 @@ func TestDistro_Manifest(t *testing.T, pipelinePath string, prefix string, regis
 				imgPackageSpecSets = tt.PackageSpecSets
 			}
 
-			got, err := imageType.Manifest(tt.ComposeRequest.Blueprint.Customizations,
+			got, _, err := imageType.Manifest(tt.ComposeRequest.Blueprint.Customizations,
 				options,
 				repos,
 				imgPackageSpecSets,
diff --git a/internal/distro/fedora/distro_test.go b/internal/distro/fedora/distro_test.go
index 794bae889..5f2f4757c 100644
--- a/internal/distro/fedora/distro_test.go
+++ b/internal/distro/fedora/distro_test.go
@@ -417,7 +417,7 @@ func TestDistro_ManifestError(t *testing.T) {
 				Size: imgType.Size(0),
 			}
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-raw-image" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "iot-installer" {
@@ -559,7 +559,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		arch, _ := fedoraDistro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-raw-image" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "iot-installer" || imgTypeName == "image-installer" {
@@ -588,7 +588,7 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-raw-image" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "iot-installer" || imgTypeName == "image-installer" {
@@ -621,7 +621,7 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if strings.HasPrefix(imgTypeName, "iot-") || strings.HasPrefix(imgTypeName, "image-") {
 				continue
 			} else {
@@ -660,7 +660,7 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if strings.HasPrefix(imgTypeName, "iot-") || strings.HasPrefix(imgTypeName, "image-") {
 				continue
 			} else {
@@ -694,7 +694,7 @@ func TestDistro_DirtyMountpointsNotAllowed(t *testing.T) {
 		arch, _ := fedoraDistro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if strings.HasPrefix(imgTypeName, "iot-") || strings.HasPrefix(imgTypeName, "image-") {
 				continue
 			} else {
@@ -724,7 +724,7 @@ func TestDistro_CustomFileSystemPatternMatching(t *testing.T) {
 		arch, _ := fedoraDistro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-raw-image" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "iot-installer" || imgTypeName == "image-installer" {
@@ -753,7 +753,7 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-raw-image" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "iot-installer" || imgTypeName == "image-installer" {
diff --git a/internal/distro/rhel7/distro_test.go b/internal/distro/rhel7/distro_test.go
index 24bc87f8e..b4e1b2dc4 100644
--- a/internal/distro/rhel7/distro_test.go
+++ b/internal/distro/rhel7/distro_test.go
@@ -178,7 +178,7 @@ func TestDistro_ManifestError(t *testing.T) {
 				Size: imgType.Size(0),
 			}
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
 			assert.NoError(t, err)
 		}
 	}
@@ -284,7 +284,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		arch, _ := r7distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 		}
 	}
@@ -307,7 +307,7 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			assert.NoError(t, err)
 		}
 	}
@@ -334,7 +334,7 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			assert.NoError(t, err)
 		}
 	}
@@ -369,7 +369,7 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			assert.NoError(t, err)
 		}
 	}
@@ -399,7 +399,7 @@ func TestDistro_DirtyMountpointsNotAllowed(t *testing.T) {
 		arch, _ := r7distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			assert.EqualError(t, err, "The following custom mountpoints are not supported [\"//\" \"/var//\" \"/var//log/audit/\"]")
 		}
 	}
@@ -425,7 +425,7 @@ func TestDistro_CustomFileSystemPatternMatching(t *testing.T) {
 		arch, _ := r7distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/variable\" \"/variable/log/audit\"]")
 		}
 	}
@@ -448,7 +448,7 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			assert.NoError(t, err)
 		}
 	}
diff --git a/internal/distro/rhel8/distro_test.go b/internal/distro/rhel8/distro_test.go
index 25e08e33e..c8763497b 100644
--- a/internal/distro/rhel8/distro_test.go
+++ b/internal/distro/rhel8/distro_test.go
@@ -464,7 +464,7 @@ func TestDistro_ManifestError(t *testing.T) {
 				Size: imgType.Size(0),
 			}
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -646,7 +646,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if unsupported[imgTypeName] {
@@ -681,7 +681,7 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if unsupported[imgTypeName] {
@@ -722,7 +722,7 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if unsupported[imgTypeName] {
 				assert.Error(t, err)
 			} else {
@@ -769,7 +769,7 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if unsupported[imgTypeName] {
 				assert.Error(t, err)
 			} else {
@@ -811,7 +811,7 @@ func TestDistro_DirtyMountpointsNotAllowed(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if unsupported[imgTypeName] {
 				assert.Error(t, err)
 			} else {
@@ -847,7 +847,7 @@ func TestDistro_CustomFileSystemPatternMatching(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if unsupported[imgTypeName] {
@@ -882,7 +882,7 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if unsupported[imgTypeName] {
diff --git a/internal/distro/rhel9/distro_test.go b/internal/distro/rhel9/distro_test.go
index b02a434c4..04458d569 100644
--- a/internal/distro/rhel9/distro_test.go
+++ b/internal/distro/rhel9/distro_test.go
@@ -447,7 +447,7 @@ func TestDistro_ManifestError(t *testing.T) {
 				Size: imgType.Size(0),
 			}
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -619,7 +619,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -648,7 +648,7 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -681,7 +681,7 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -720,7 +720,7 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -754,7 +754,7 @@ func TestDistro_DirtyMountpointsNotAllowed(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -784,7 +784,7 @@ func TestDistro_CustomFileSystemPatternMatching(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -813,7 +813,7 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			testPackageSpecSets := distro_test_common.GetTestingImagePackageSpecSets("kernel", imgType)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
+			_, _, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, nil, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
diff --git a/internal/distro/test_distro/distro.go b/internal/distro/test_distro/distro.go
index cdc22bdbd..c4b232c33 100644
--- a/internal/distro/test_distro/distro.go
+++ b/internal/distro/test_distro/distro.go
@@ -239,7 +239,7 @@ func (t *TestImageType) Exports() []string {
 	return distro.ExportsFallback()
 }
 
-func (t *TestImageType) Manifest(b *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSpecSets map[string][]rpmmd.PackageSpec, containers []container.Spec, seed int64) (distro.Manifest, error) {
+func (t *TestImageType) Manifest(b *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSpecSets map[string][]rpmmd.PackageSpec, containers []container.Spec, seed int64) (distro.Manifest, []string, error) {
 	mountpoints := b.GetFilesystems()
 
 	invalidMountpoints := []string{}
@@ -250,15 +250,15 @@ func (t *TestImageType) Manifest(b *blueprint.Customizations, options distro.Ima
 	}
 
 	if len(invalidMountpoints) > 0 {
-		return nil, fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+		return nil, nil, fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
 	}
 
-	return json.Marshal(
+	ret, err := json.Marshal(
 		osbuild.Manifest{
 			Sources:   osbuild.Sources{},
 			Pipelines: []osbuild.Pipeline{},
-		},
-	)
+		})
+	return ret, nil, err
 }
 
 // newTestDistro returns a new instance of TestDistro with the
diff --git a/internal/store/store_test.go b/internal/store/store_test.go
index 2cb0e6b65..7cd2b4127 100644
--- a/internal/store/store_test.go
+++ b/internal/store/store_test.go
@@ -51,7 +51,7 @@ func (suite *storeTest) SetupSuite() {
 	suite.myDistro = test_distro.New()
 	suite.myArch, _ = suite.myDistro.GetArch(test_distro.TestArchName)
 	suite.myImageType, _ = suite.myArch.GetImageType(test_distro.TestImageTypeName)
-	suite.myManifest, _ = suite.myImageType.Manifest(&suite.myCustomizations, suite.myImageOptions, suite.myRepoConfig, nil, nil, 0)
+	suite.myManifest, _, _ = suite.myImageType.Manifest(&suite.myCustomizations, suite.myImageOptions, suite.myRepoConfig, nil, nil, 0)
 	suite.mySourceConfig = SourceConfig{
 		Name: "testSourceConfig",
 	}
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index 17130d1a4..ce2bfebcc 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -926,12 +926,12 @@ func TestCompose(t *testing.T) {
 	require.NoError(t, err)
 	imgType, err := arch.GetImageType(test_distro.TestImageTypeName)
 	require.NoError(t, err)
-	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
+	manifest, _, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	require.NoError(t, err)
 
 	ostreeImgType, err := arch.GetImageType(test_distro.TestImageTypeOSTree)
 	require.NoError(t, err)
-	ostreeManifest, err := ostreeImgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
+	ostreeManifest, _, err := ostreeImgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	require.NoError(t, err)
 
 	expectedComposeLocal := &store.Compose{
@@ -1037,7 +1037,7 @@ func TestCompose(t *testing.T) {
 	require.NoError(t, err)
 	imgType2, err := arch2.GetImageType(test_distro.TestImageTypeName)
 	require.NoError(t, err)
-	manifest2, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
+	manifest2, _, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	require.NoError(t, err)
 
 	expectedComposeGoodDistro := &store.Compose{
@@ -1093,7 +1093,7 @@ func TestCompose(t *testing.T) {
 			http.StatusBadRequest,
 			`{"status":false,"errors":[{"id":"UnknownBlueprint","msg":"Unknown blueprint name: http-server"}]}`,
 			nil,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			false,
@@ -1103,7 +1103,7 @@ func TestCompose(t *testing.T) {
 			http.StatusOK,
 			`{"status": true}`,
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			false,
@@ -1113,7 +1113,7 @@ func TestCompose(t *testing.T) {
 			http.StatusOK,
 			`{"status": true}`,
 			expectedComposeLocalAndAws,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			false,
@@ -1123,7 +1123,7 @@ func TestCompose(t *testing.T) {
 			http.StatusOK,
 			`{"status": true}`,
 			expectedComposeGoodDistro,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			false,
@@ -1133,7 +1133,7 @@ func TestCompose(t *testing.T) {
 			http.StatusBadRequest,
 			`{"status": false,"errors":[{"id":"DistroError", "msg":"Unknown distribution: fedora-1"}]}`,
 			nil,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			false,
@@ -1143,7 +1143,7 @@ func TestCompose(t *testing.T) {
 			http.StatusBadRequest,
 			`{"status": false,"errors":[{"id":"ComposeError", "msg":"Failed to get compose type \"imaginary_type\": invalid image type: imaginary_type"}]}`,
 			nil,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 
 		// === OSTree params ===
@@ -1156,7 +1156,7 @@ func TestCompose(t *testing.T) {
 			http.StatusBadRequest,
 			`{"status": false, "errors":[{"id":"OSTreeOptionsError","msg":"ostree parent ref specified, but no URL to retrieve it"}]}`,
 			expectedComposeOSTree,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		// Valid Ref + URL = OK
 		{
@@ -1167,7 +1167,7 @@ func TestCompose(t *testing.T) {
 			http.StatusOK,
 			`{"status": true}`,
 			expectedComposeOSTree,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		// Ref + invalid URL = error
 		{
@@ -1178,7 +1178,7 @@ func TestCompose(t *testing.T) {
 			http.StatusBadRequest,
 			`{"status":false,"errors":[{"id":"OSTreeOptionsError","msg":"error sending request to ostree repository \"invalid-url/refs/heads/whatever\": Get \"invalid-url/refs/heads/whatever\": unsupported protocol scheme \"\""}]}`,
 			nil,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		// Bad Ref + URL = error
 		{
@@ -1189,7 +1189,7 @@ func TestCompose(t *testing.T) {
 			http.StatusBadRequest,
 			`{"status":false,"errors":[{"id":"OSTreeOptionsError","msg":"Invalid ostree ref \"/bad/ref\""}]}`,
 			expectedComposeOSTree,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		// Incorrect Ref + URL = the parameters are okay, but the ostree repo returns 404
 		{
@@ -1200,7 +1200,7 @@ func TestCompose(t *testing.T) {
 			http.StatusBadRequest,
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"OSTreeOptionsError","msg":"ostree repository \"%s/refs/heads/the/wrong/ref\" returned status: 404 Not Found"}]}`, ostreeRepoDefault.Server.URL),
 			expectedComposeOSTree,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		// Ref + Parent + URL = OK
 		{
@@ -1211,7 +1211,7 @@ func TestCompose(t *testing.T) {
 			http.StatusOK,
 			`{"status":true}`,
 			expectedComposeOSTree,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		// Parent + URL = OK
 		{
@@ -1222,7 +1222,7 @@ func TestCompose(t *testing.T) {
 			http.StatusOK,
 			`{"status":true}`,
 			expectedComposeOSTree,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		// URL only = OK (uses default ref, so we need to specify URL for ostree repo with default ref)
 		{
@@ -1233,7 +1233,7 @@ func TestCompose(t *testing.T) {
 			http.StatusOK,
 			`{"status":true}`,
 			expectedComposeOSTree,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 	}
 
@@ -2028,7 +2028,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 	require.NoError(t, err)
 	imgType2, err := arch.GetImageType(test_distro.TestImageType2Name)
 	require.NoError(t, err)
-	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
+	manifest, _, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	require.NoError(t, err)
 
 	expectedComposeLocal := &store.Compose{
@@ -2103,7 +2103,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			http.StatusOK,
 			`{"status":true}`,
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2112,7 +2112,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			http.StatusOK,
 			`{"status": true}`,
 			expectedComposeLocal2,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2122,7 +2122,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
 				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2131,7 +2131,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			http.StatusOK,
 			`{"status": true}`,
 			expectedComposeLocal2,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2141,7 +2141,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
 				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2151,7 +2151,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
 				test_distro.TestImageType2Name, test_distro.TestDistro2Name),
 			expectedComposeLocal2,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2161,7 +2161,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
 				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2171,7 +2171,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
 				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2181,7 +2181,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
 				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 		{
 			"/api/v1/compose",
@@ -2191,7 +2191,7 @@ func TestComposePOST_ImageTypeDenylist(t *testing.T) {
 			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
 				test_distro.TestImageType2Name, test_distro.TestDistro2Name),
 			expectedComposeLocal,
-			[]string{"build_id"},
+			[]string{"build_id", "warnings"},
 		},
 	}
 
diff --git a/internal/weldr/compose_test.go b/internal/weldr/compose_test.go
index 9c4b9a937..0d3c05d39 100644
--- a/internal/weldr/compose_test.go
+++ b/internal/weldr/compose_test.go
@@ -32,7 +32,7 @@ func TestComposeStatusFromLegacyError(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}
@@ -81,7 +81,7 @@ func TestComposeStatusFromJobError(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}
diff --git a/internal/worker/server_test.go b/internal/worker/server_test.go
index 54ba8f1c3..9505b51c1 100644
--- a/internal/worker/server_test.go
+++ b/internal/worker/server_test.go
@@ -129,7 +129,7 @@ func TestCreate(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}
@@ -154,7 +154,7 @@ func TestCancel(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}
@@ -191,7 +191,7 @@ func TestUpdate(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}
@@ -221,7 +221,7 @@ func TestArgs(t *testing.T) {
 	require.NoError(t, err)
 	imageType, err := arch.GetImageType(test_distro.TestImageTypeName)
 	require.NoError(t, err)
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	require.NoError(t, err)
 
 	server := newTestServer(t, t.TempDir(), time.Duration(0), "/api/worker/v1", false)
@@ -267,7 +267,7 @@ func TestUpload(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}
@@ -297,7 +297,7 @@ func TestUploadNotAcceptingArtifacts(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}
@@ -327,7 +327,7 @@ func TestUploadAlteredBasePath(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch: %v", err)
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
+	manifest, _, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest: %v", err)
 	}