Repository: osbuild/osbuild-composer
Pull request #2940 (2022-08-30 13:36:18)

build(deps): bump github.com/labstack/echo/v4 from 4.7.2 to 4.8.0
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.7.2 to 4.8.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/labstack/echo/releases">github.com/labstack/echo/v4's releases</a>.</em></p>
<blockquote>
<h2>v4.8.0</h2>
<p><strong>Most notable things</strong></p>
<p>You can now add any arbitrary HTTP method type as a route <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2237">#2237</a></p>
<pre lang="go"><code>e.Add(&quot;COPY&quot;, &quot;/*&quot;, func(c echo.Context) error 
  return c.String(http.StatusOK, &quot;OK COPY&quot;)
})
</code></pre>
<p>You can add custom 404 handler for specific paths <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2217">#2217</a></p>
<pre lang="go"><code>e.RouteNotFound(&quot;/*&quot;, func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })
<p>g := e.Group(&quot;/images&quot;)
g.RouteNotFound(&quot;/*&quot;, func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })
</code></pre></p>
<p><strong>Enhancements</strong></p>
<ul>
<li>Add new value binding methods (UnixTimeMilli,TextUnmarshaler,JSONUnmarshaler) to Valuebinder <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2127">#2127</a></li>
<li>Refactor: body_limit middleware unit test <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2145">#2145</a></li>
<li>Refactor: Timeout mw: rework how test waits for timeout. <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2187">#2187</a></li>
<li>BasicAuth middleware returns 500 InternalServerError on invalid base64 strings but should return 400 <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2191">#2191</a></li>
<li>Refactor: duplicated findStaticChild process at findChildWithLabel <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2176">#2176</a></li>
<li>Allow different param names in different methods with same path scheme <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2209">#2209</a></li>
<li>Add support for registering handlers for different 404 routes <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2217">#2217</a></li>
<li>Middlewares should use errors.As() instead of type assertion on HTTPError <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2227">#2227</a></li>
<li>Allow arbitrary HTTP method types to be added as routes <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2237">#2237</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/labstack/echo/blob/master/CHANGELOG.md">github.com/labstack/echo/v4's changelog</a>.</em></p>
<blockquote>
<h2>v4.8.0 - 2022-08-10</h2>
<p><strong>Most notable things</strong></p>
<p>You can now add any arbitrary HTTP method type as a route <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2237">#2237</a></p>
<pre lang="go"><code>e.Add(&quot;COPY&quot;, &quot;/*&quot;, func(c echo.Context) error 
  return c.String(http.StatusOK, &quot;OK COPY&quot;)
})
</code></pre>
<p>You can add custom 404 handler for specific paths <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2217">#2217</a></p>
<pre lang="go"><code>e.RouteNotFound(&quot;/*&quot;, func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })
<p>g := e.Group(&quot;/images&quot;)
g.RouteNotFound(&quot;/*&quot;, func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })
</code></pre></p>
<p><strong>Enhancements</strong></p>
<ul>
<li>Add new value binding methods (UnixTimeMilli,TextUnmarshaler,JSONUnmarshaler) to Valuebinder <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2127">#2127</a></li>
<li>Refactor: body_limit middleware unit test <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2145">#2145</a></li>
<li>Refactor: Timeout mw: rework how test waits for timeout. <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2187">#2187</a></li>
<li>BasicAuth middleware returns 500 InternalServerError on invalid base64 strings but should return 400 <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2191">#2191</a></li>
<li>Refactor: duplicated findStaticChild process at findChildWithLabel <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2176">#2176</a></li>
<li>Allow different param names in different methods with same path scheme <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2209">#2209</a></li>
<li>Add support for registering handlers for different 404 routes <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2217">#2217</a></li>
<li>Middlewares should use errors.As() instead of type assertion on HTTPError <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2227">#2227</a></li>
<li>Allow arbitrary HTTP method types to be added as routes <a href="https://github-redirect.dependabot.com/labstack/echo/pull/2237">#2237</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/labstack/echo/commit/d48197db7af19becf2363496493ed0e2a8d1caea"><code>d48197d</code></a> Changelog for 4.8.0</li>
<li><a href="https://github.com/labstack/echo/commit/cba12a570e8caa1fafabc2d41afc97bd7a83d758"><code>cba12a5</code></a> Allow arbitrary HTTP method types to be added as routes</li>
<li><a href="https://github.com/labstack/echo/commit/a327884b682cc1458580ac583e04dc610429b6fc"><code>a327884</code></a> add:README.md-Third-party middlewares-github.com/go-woo/protoc-gen-echo</li>
<li><a href="https://github.com/labstack/echo/commit/61422dd7de9b0359708ff56b67099b91b5954c31"><code>61422dd</code></a> Update CI-flow (Go 1.19 +deps)</li>
<li><a href="https://github.com/labstack/echo/commit/a9879ffa6b6fe73e43a0e062884f83eb959e6c1a"><code>a9879ff</code></a> Middlewares should use errors.As() instead of type assertion on HTTPError</li>
<li><a href="https://github.com/labstack/echo/commit/70acd57105b15b2db18c730b52083a76d05babf7"><code>70acd57</code></a> Fix case when routeNotFound handler is lost when new route is added to the ro...</li>
<li><a href="https://github.com/labstack/echo/commit/690e3392d984dcbdb9f41a7915ddc0d383311974"><code>690e339</code></a> Add support for registering handlers for 404 routes (<a href="https://github-redirect.dependabot.com/labstack/echo/issues/2217">#2217</a>)</li>
<li><a href="https://github.com/labstack/echo/commit/9bf1e3c8ce6de029186945e4ee82f89f2e2cc661"><code>9bf1e3c</code></a> Allow different param names in different methods with same path scheme (<a href="https://github-redirect.dependabot.com/labstack/echo/issues/2209">#2209</a>)</li>
<li><a href="https://github.com/labstack/echo/commit/ddb66e1ba272fe7580dae6f8543763bc4a760fbd"><code>ddb66e1</code></a> Add logger middleware template variables: <code>${time_unix_milli}</code> and `${time_un...</li>
<li><a href="https://github.com/labstack/echo/commit/0644cd6ecdef4473c38c9f298a15512a47c8db42"><code>0644cd6</code></a> fix: duplicated findStaticChild process at findChildWithLabel (<a href="https://github-redirect.dependabot.com/labstack/echo/issues/2176">#2176</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/labstack/echo/compare/v4.7.2...v4.8.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/labstack/echo/v4&package-manager=go_modules&previous-version=4.7.2&new-version=4.8.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/cmd/osbuild-worker/jobimpl-awsec2.go b/cmd/osbuild-worker/jobimpl-awsec2.go
new file mode 100644
index 000000000..1a055d3ee
--- /dev/null
+++ b/cmd/osbuild-worker/jobimpl-awsec2.go
@@ -0,0 +1,144 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/aws/aws-sdk-go/aws/awserr"
+	"github.com/sirupsen/logrus"
+
+	"github.com/osbuild/osbuild-composer/internal/cloud/awscloud"
+	"github.com/osbuild/osbuild-composer/internal/worker"
+	"github.com/osbuild/osbuild-composer/internal/worker/clienterrors"
+)
+
+func getAWS(awsCreds, region string) (*awscloud.AWS, error) {
+	if awsCreds != "" {
+		return awscloud.NewFromFile(awsCreds, region)
+	}
+	return awscloud.NewDefault(region)
+}
+
+type AWSEC2CopyJobImpl struct {
+	AWSCreds string
+}
+
+func (impl *AWSEC2CopyJobImpl) Run(job worker.Job) error {
+	logWithId := logrus.WithField("jobId", job.Id())
+	result := worker.AWSEC2CopyJobResult{}
+
+	defer func() {
+		err := job.Update(&result)
+		if err != nil {
+			logWithId.Errorf("Error reporting job result: %v", err)
+		}
+	}()
+
+	var args worker.AWSEC2CopyJob
+	err := job.Args(&args)
+	if err != nil {
+		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorParsingJobArgs, fmt.Sprintf("Error parsing arguments: %v", err), nil)
+		return err
+	}
+
+	aws, err := getAWS(impl.AWSCreds, args.TargetRegion)
+	if err != nil {
+		logWithId.Errorf("Error creating aws client: %v", err)
+		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, "Invalid worker config", nil)
+		return err
+	}
+
+	ami, err := aws.CopyImage(args.TargetName, args.Ami, args.SourceRegion)
+	if err != nil {
+		logWithId.Errorf("Error copying ami: %v", err)
+		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Error copying ami %s", args.Ami), nil)
+		if aerr, ok := err.(awserr.Error); ok {
+			switch aerr.Code() {
+			case "InvalidRegion":
+				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Invalid source region '%s'", args.SourceRegion), nil)
+			case "InvalidAMIID.Malformed":
+				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Malformed source ami id '%s'", args.Ami), nil)
+			case "InvalidAMIID.NotFound":
+				fallthrough // CopyImage returns InvalidRequest instead of InvalidAMIID.NotFound
+			case "InvalidRequest":
+				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Source ami '%s' not found", args.Ami), nil)
+			}
+		}
+		return err
+	}
+
+	result.Ami = ami
+	result.Region = args.TargetRegion
+	return nil
+}
+
+type AWSEC2ShareJobImpl struct {
+	AWSCreds string
+}
+
+func (impl *AWSEC2ShareJobImpl) Run(job worker.Job) error {
+	logWithId := logrus.WithField("jobId", job.Id())
+	result := worker.AWSEC2ShareJobResult{}
+
+	defer func() {
+		err := job.Update(&result)
+		if err != nil {
+			logWithId.Errorf("Error reporting job result: %v", err)
+		}
+	}()
+
+	var args worker.AWSEC2ShareJob
+	err := job.Args(&args)
+	if err != nil {
+		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorParsingJobArgs, fmt.Sprintf("Error parsing arguments: %v", err), nil)
+		return err
+	}
+
+	if args.Ami == "" || args.Region == "" {
+		if job.NDynamicArgs() != 1 {
+			logWithId.Error("No arguments given and dynamic args empty")
+			result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorNoDynamicArgs, "An ec2 share job should have args or depend on an ec2 copy job", nil)
+			return nil
+		}
+		var cjResult worker.AWSEC2CopyJobResult
+		err = job.DynamicArgs(0, &cjResult)
+		if err != nil {
+			result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorParsingDynamicArgs, "Error parsing dynamic args as ec2 copy job", nil)
+			return err
+		}
+		if cjResult.JobError != nil {
+			result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorJobDependency, "AWSEC2CopyJob dependency failed", nil)
+			return nil
+		}
+
+		args.Ami = cjResult.Ami
+		args.Region = cjResult.Region
+	}
+
+	aws, err := getAWS(impl.AWSCreds, args.Region)
+	if err != nil {
+		logWithId.Errorf("Error creating aws client: %v", err)
+		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, "Invalid worker config", nil)
+		return err
+	}
+
+	err = aws.ShareImage(args.Ami, args.ShareWithAccounts)
+	if err != nil {
+		logWithId.Errorf("Error sharing image: %v", err)
+		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Error sharing image with target %v", args.ShareWithAccounts), nil)
+		if aerr, ok := err.(awserr.Error); ok {
+			switch aerr.Code() {
+			case "InvalidAMIID.Malformed":
+				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Malformed ami id '%s'", args.Ami), nil)
+			case "InvalidAMIID.NotFound":
+				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Ami '%s' not found in region '%s'", args.Ami, args.Region), nil)
+			case "InvalidAMIAttributeItemValue":
+				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorSharingTarget, fmt.Sprintf("Invalid user id to share ami with: %v", args.ShareWithAccounts), nil)
+			}
+		}
+		return err
+	}
+
+	result.Ami = args.Ami
+	result.Region = args.Region
+	return nil
+}
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index d5e6c94c5..7c5478682 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -464,6 +464,12 @@ func main() {
 		worker.JobTypeContainerResolve: &ContainerResolveJobImpl{
 			AuthFilePath: containersAuthFilePath,
 		},
+		worker.JobTypeAWSEC2Copy: &AWSEC2CopyJobImpl{
+			AWSCreds: awsCredentials,
+		},
+		worker.JobTypeAWSEC2Share: &AWSEC2ShareJobImpl{
+			AWSCreds: awsCredentials,
+		},
 	}
 
 	acceptedJobTypes := []string{}
diff --git a/go.mod b/go.mod
index 54dfa0455..4bbde4f95 100644
--- a/go.mod
+++ b/go.mod
@@ -32,7 +32,7 @@ require (
 	github.com/jackc/pgx/v4 v4.17.1
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b
-	github.com/labstack/echo/v4 v4.7.2
+	github.com/labstack/echo/v4 v4.8.0
 	github.com/labstack/gommon v0.3.1
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198
diff --git a/go.sum b/go.sum
index 86437a8d1..00f4e3920 100644
--- a/go.sum
+++ b/go.sum
@@ -1099,8 +1099,8 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+
 github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg=
 github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
 github.com/labstack/echo/v4 v4.3.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8=
-github.com/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI=
-github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
+github.com/labstack/echo/v4 v4.8.0 h1:wdc6yKVaHxkNOEdz4cRZs1pQkwSXPiRjq69yWP4QQS8=
+github.com/labstack/echo/v4 v4.8.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
 github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
diff --git a/internal/cloud/awscloud/awscloud.go b/internal/cloud/awscloud/awscloud.go
index 62e8d1704..fd896e3c6 100644
--- a/internal/cloud/awscloud/awscloud.go
+++ b/internal/cloud/awscloud/awscloud.go
@@ -266,29 +266,6 @@ func (a *AWS) Register(name, bucket, key string, shareWith []string, rpmArch str
 
 	snapshotID := importOutput.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId
 
-	if len(shareWith) > 0 {
-		logrus.Info("[AWS] 🎥 Sharing ec2 snapshot")
-		var userIds []*string
-		for _, v := range shareWith {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			userIds = append(userIds, &v)
-		}
-		_, err := a.ec2.ModifySnapshotAttribute(
-			&ec2.ModifySnapshotAttributeInput{
-				Attribute:     aws.String("createVolumePermission"),
-				OperationType: aws.String("add"),
-				SnapshotId:    snapshotID,
-				UserIds:       userIds,
-			},
-		)
-		if err != nil {
-			logrus.Warnf("[AWS] 📨 Error sharing ec2 snapshot: %v", err)
-			return nil, err
-		}
-		logrus.Info("[AWS] 📨 Shared ec2 snapshot")
-	}
-
 	// Tag the snapshot with the image name.
 	req, _ := a.ec2.CreateTagsRequest(
 		&ec2.CreateTagsInput{
@@ -348,33 +325,192 @@ func (a *AWS) Register(name, bucket, key string, shareWith []string, rpmArch str
 	}
 
 	if len(shareWith) > 0 {
-		logrus.Info("[AWS] 💿 Sharing ec2 AMI")
-		var launchPerms []*ec2.LaunchPermission
-		for _, id := range shareWith {
-			launchPerms = append(launchPerms, &ec2.LaunchPermission{
-				// Implicit memory alasing doesn't couse any bug in this case
-				/* #nosec G601 */
-				UserId: &id,
-			})
+		err = a.shareSnapshot(snapshotID, shareWith)
+		if err != nil {
+			return nil, err
 		}
-		_, err := a.ec2.ModifyImageAttribute(
-			&ec2.ModifyImageAttributeInput{
-				ImageId: registerOutput.ImageId,
-				LaunchPermission: &ec2.LaunchPermissionModifications{
-					Add: launchPerms,
-				},
-			},
-		)
+		err = a.shareImage(registerOutput.ImageId, shareWith)
 		if err != nil {
-			logrus.Warnf("[AWS] 📨 Error sharing AMI: %v", err)
 			return nil, err
 		}
-		logrus.Info("[AWS] 💿 Shared AMI")
 	}
 
 	return registerOutput.ImageId, nil
 }
 
+// target region is determined by the region configured in the aws session
+func (a *AWS) CopyImage(name, ami, sourceRegion string) (string, error) {
+	result, err := a.ec2.CopyImage(
+		&ec2.CopyImageInput{
+			Name:          aws.String(name),
+			SourceImageId: aws.String(ami),
+			SourceRegion:  aws.String(sourceRegion),
+		},
+	)
+	if err != nil {
+		return "", err
+	}
+
+	dIInput := &ec2.DescribeImagesInput{
+		ImageIds: []*string{result.ImageId},
+	}
+
+	// Custom waiter which waits indefinitely until a final state
+	w := request.Waiter{
+		Name:        "WaitUntilImageAvailable",
+		MaxAttempts: 0,
+		Delay:       request.ConstantWaiterDelay(15 * time.Second),
+		Acceptors: []request.WaiterAcceptor{
+			{
+				State:   request.SuccessWaiterState,
+				Matcher: request.PathAllWaiterMatch, Argument: "Images[].State",
+				Expected: "available",
+			},
+			{
+				State:   request.FailureWaiterState,
+				Matcher: request.PathAnyWaiterMatch, Argument: "Images[].State",
+				Expected: "failed",
+			},
+		},
+		Logger: a.ec2.Config.Logger,
+		NewRequest: func(opts []request.Option) (*request.Request, error) {
+			var inCpy *ec2.DescribeImagesInput
+			if dIInput != nil {
+				tmp := *dIInput
+				inCpy = &tmp
+			}
+			req, _ := a.ec2.DescribeImagesRequest(inCpy)
+			req.SetContext(aws.BackgroundContext())
+			req.ApplyOptions(opts...)
+			return req, nil
+		},
+	}
+	err = w.WaitWithContext(aws.BackgroundContext())
+	if err != nil {
+		return *result.ImageId, err
+	}
+
+	// Tag image with name
+	_, err = a.ec2.CreateTags(&ec2.CreateTagsInput{
+		Resources: []*string{result.ImageId},
+		Tags: []*ec2.Tag{
+			{
+				Key:   aws.String("Name"),
+				Value: aws.String(name),
+			},
+		},
+	})
+
+	if err != nil {
+		return *result.ImageId, err
+	}
+
+	imgs, err := a.ec2.DescribeImages(dIInput)
+	if err != nil {
+		return *result.ImageId, err
+	}
+	if len(imgs.Images) == 0 {
+		return *result.ImageId, fmt.Errorf("Unable to find image with id: %v", ami)
+	}
+
+	// Tag snapshot with name
+	for _, bdm := range imgs.Images[0].BlockDeviceMappings {
+		_, err = a.ec2.CreateTags(&ec2.CreateTagsInput{
+			Resources: []*string{bdm.Ebs.SnapshotId},
+			Tags: []*ec2.Tag{
+				{
+					Key:   aws.String("Name"),
+					Value: aws.String(name),
+				},
+			},
+		})
+		if err != nil {
+			return *result.ImageId, err
+		}
+	}
+
+	return *result.ImageId, nil
+}
+
+func (a *AWS) ShareImage(ami string, userIds []string) error {
+	imgs, err := a.ec2.DescribeImages(
+		&ec2.DescribeImagesInput{
+			ImageIds: []*string{aws.String(ami)},
+		},
+	)
+	if err != nil {
+		return err
+	}
+	if len(imgs.Images) == 0 {
+		return fmt.Errorf("Unable to find image with id: %v", ami)
+	}
+
+	for _, bdm := range imgs.Images[0].BlockDeviceMappings {
+		err = a.shareSnapshot(bdm.Ebs.SnapshotId, userIds)
+		if err != nil {
+			return err
+		}
+	}
+
+	err = a.shareImage(aws.String(ami), userIds)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (a *AWS) shareImage(ami *string, userIds []string) error {
+	logrus.Info("[AWS] 🎥 Sharing ec2 snapshot")
+	var uIds []*string
+	for i := range userIds {
+		uIds = append(uIds, &userIds[i])
+	}
+
+	logrus.Info("[AWS] 💿 Sharing ec2 AMI")
+	var launchPerms []*ec2.LaunchPermission
+	for _, id := range uIds {
+		launchPerms = append(launchPerms, &ec2.LaunchPermission{
+			UserId: id,
+		})
+	}
+	_, err := a.ec2.ModifyImageAttribute(
+		&ec2.ModifyImageAttributeInput{
+			ImageId: ami,
+			LaunchPermission: &ec2.LaunchPermissionModifications{
+				Add: launchPerms,
+			},
+		},
+	)
+	if err != nil {
+		logrus.Warnf("[AWS] 📨 Error sharing AMI: %v", err)
+		return err
+	}
+	logrus.Info("[AWS] 💿 Shared AMI")
+	return nil
+}
+
+func (a *AWS) shareSnapshot(snapshotId *string, userIds []string) error {
+	logrus.Info("[AWS] 🎥 Sharing ec2 snapshot")
+	var uIds []*string
+	for i := range userIds {
+		uIds = append(uIds, &userIds[i])
+	}
+	_, err := a.ec2.ModifySnapshotAttribute(
+		&ec2.ModifySnapshotAttributeInput{
+			Attribute:     aws.String(ec2.SnapshotAttributeNameCreateVolumePermission),
+			OperationType: aws.String("add"),
+			SnapshotId:    snapshotId,
+			UserIds:       uIds,
+		},
+	)
+	if err != nil {
+		logrus.Warnf("[AWS] 📨 Error sharing ec2 snapshot: %v", err)
+		return err
+	}
+	logrus.Info("[AWS] 📨 Shared ec2 snapshot")
+	return nil
+}
+
 func (a *AWS) RemoveSnapshotAndDeregisterImage(image *ec2.Image) error {
 	if image == nil {
 		return fmt.Errorf("image is nil")
diff --git a/internal/cloudapi/v2/errors.go b/internal/cloudapi/v2/errors.go
index 79184139c..5f480190e 100644
--- a/internal/cloudapi/v2/errors.go
+++ b/internal/cloudapi/v2/errors.go
@@ -44,6 +44,10 @@ const (
 	ErrorTenantNotFound               ServiceErrorCode = 28
 	ErrorNoGPGKey                     ServiceErrorCode = 29
 	ErrorValidationFailed             ServiceErrorCode = 30
+	ErrorComposeBadState              ServiceErrorCode = 31
+	ErrorUnsupportedImage             ServiceErrorCode = 32
+	ErrorInvalidImageFromComposeId    ServiceErrorCode = 33
+	ErrorImageNotFound                ServiceErrorCode = 34
 
 	// Internal errors, these are bugs
 	ErrorFailedToInitializeBlueprint              ServiceErrorCode = 1000
@@ -63,6 +67,9 @@ const (
 	ErrorDepsolveJobCanceled                      ServiceErrorCode = 1014
 	ErrorUnexpectedNumberOfImageBuilds            ServiceErrorCode = 1015
 	ErrorGettingBuildDependencyStatus             ServiceErrorCode = 1016
+	ErrorGettingOSBuildJobStatus                  ServiceErrorCode = 1017
+	ErrorGettingAWSEC2JobStatus                   ServiceErrorCode = 1018
+	ErrorGettingJobType                           ServiceErrorCode = 1019
 
 	// Errors contained within this file
 	ErrorUnspecified          ServiceErrorCode = 10000
@@ -106,12 +113,16 @@ func getServiceErrors() serviceErrors {
 		serviceError{ErrorMethodNotAllowed, http.StatusMethodNotAllowed, "Requested method isn't supported for resource"},
 		serviceError{ErrorNotAcceptable, http.StatusNotAcceptable, "Only 'application/json' content is supported"},
 		serviceError{ErrorNoBaseURLInPayloadRepository, http.StatusBadRequest, "BaseURL must be specified for payload repositories"},
-		serviceError{ErrorInvalidJobType, http.StatusNotFound, "Requested job has invalid type"},
+		serviceError{ErrorInvalidJobType, http.StatusNotFound, "Job with given id has an invalid type"},
 		serviceError{ErrorInvalidNumberOfImageBuilds, http.StatusBadRequest, "Compose request has unsupported number of image builds"},
 		serviceError{ErrorInvalidOSTreeParams, http.StatusBadRequest, "Invalid OSTree parameters or parameter combination"},
 		serviceError{ErrorTenantNotFound, http.StatusBadRequest, "Tenant not found in JWT claims"},
 		serviceError{ErrorNoGPGKey, http.StatusBadRequest, "Invalid repository, when check_gpg is set, gpgkey must be specified"},
 		serviceError{ErrorValidationFailed, http.StatusBadRequest, "Request could not be validated"},
+		serviceError{ErrorComposeBadState, http.StatusBadRequest, "Compose is running or has failed"},
+		serviceError{ErrorUnsupportedImage, http.StatusBadRequest, "This compose doesn't support the creation of multiple images"},
+		serviceError{ErrorInvalidImageFromComposeId, http.StatusBadRequest, "Invalid format for image id"},
+		serviceError{ErrorImageNotFound, http.StatusBadRequest, "Image with given id not found"},
 
 		serviceError{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"},
 		serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"},
@@ -130,6 +141,9 @@ func getServiceErrors() serviceErrors {
 		serviceError{ErrorDepsolveJobCanceled, http.StatusInternalServerError, "Depsolve job was cancelled"},
 		serviceError{ErrorUnexpectedNumberOfImageBuilds, http.StatusInternalServerError, "Compose has unexpected number of image builds"},
 		serviceError{ErrorGettingBuildDependencyStatus, http.StatusInternalServerError, "Error checking status of build job dependencies"},
+		serviceError{ErrorGettingOSBuildJobStatus, http.StatusInternalServerError, "Unable to get osbuild job status"},
+		serviceError{ErrorGettingAWSEC2JobStatus, http.StatusInternalServerError, "Unable to get ec2 job status"},
+		serviceError{ErrorGettingJobType, http.StatusInternalServerError, "Unable to get job type of existing job"},
 
 		serviceError{ErrorUnspecified, http.StatusInternalServerError, "Unspecified internal error "},
 		serviceError{ErrorNotHTTPError, http.StatusInternalServerError, "Error is not an instance of HTTPError"},
diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go
index 4b0b2a3ca..81d052498 100644
--- a/internal/cloudapi/v2/handler.go
+++ b/internal/cloudapi/v2/handler.go
@@ -648,8 +648,7 @@ func (h *apiHandlers) getComposeStatusImpl(ctx echo.Context, id string) error {
 			if err != nil {
 				return HTTPError(ErrorUnknownUploadTarget)
 			}
-			// TODO: determine upload status based on the target results, not job results
-			us.Status = UploadStatusValue(result.UploadStatus)
+			us.Status = uploadStatusFromJobStatus(jobInfo.JobStatus, result.JobError)
 		}
 
 		return ctx.JSON(http.StatusOK, ComposeStatus{
@@ -705,8 +704,7 @@ func (h *apiHandlers) getComposeStatusImpl(ctx echo.Context, id string) error {
 					if err != nil {
 						return HTTPError(ErrorUnknownUploadTarget)
 					}
-					// TODO: determine upload status based on the target results, not job results
-					us.Status = UploadStatusValue(buildJobResult.UploadStatus)
+					us.Status = uploadStatusFromJobStatus(buildInfo.JobStatus, result.JobError)
 				}
 			}
 
@@ -1234,3 +1232,227 @@ func genRepoConfig(repo Repository) (*rpmmd.RepoConfig, error) {
 
 	return repoConfig, nil
 }
+
+func (h *apiHandlers) PostCloneCompose(ctx echo.Context, id string) error {
+	return h.server.EnsureJobChannel(h.postCloneComposeImpl)(ctx, id)
+}
+
+func (h *apiHandlers) postCloneComposeImpl(ctx echo.Context, id string) error {
+	channel, err := h.server.getTenantChannel(ctx)
+	if err != nil {
+		return HTTPErrorWithInternal(ErrorTenantNotFound, err)
+	}
+
+	jobId, err := uuid.Parse(id)
+	if err != nil {
+		return HTTPError(ErrorInvalidComposeId)
+	}
+
+	jobType, err := h.server.workers.JobType(jobId)
+	if err != nil {
+		return HTTPError(ErrorComposeNotFound)
+	}
+
+	if jobType != worker.JobTypeOSBuild {
+		return HTTPError(ErrorInvalidJobType)
+	}
+
+	var osbuildResult worker.OSBuildJobResult
+	osbuildInfo, err := h.server.workers.OSBuildJobInfo(jobId, &osbuildResult)
+	if err != nil {
+		return HTTPErrorWithInternal(ErrorGettingOSBuildJobStatus, err)
+	}
+
+	if osbuildInfo.JobStatus.Finished.IsZero() || !osbuildResult.Success {
+		return HTTPError(ErrorComposeBadState)
+	}
+
+	if osbuildResult.TargetResults == nil {
+		return HTTPError(ErrorMalformedOSBuildJobResult)
+	}
+	// Only single upload target is allowed, therefore only a single upload target result is allowed as well
+	if len(osbuildResult.TargetResults) != 1 {
+		return HTTPError(ErrorSeveralUploadTargets)
+	}
+	var us *UploadStatus
+	us, err = targetResultToUploadStatus(osbuildResult.TargetResults[0])
+	if err != nil {
+		return HTTPError(ErrorUnknownUploadTarget)
+	}
+
+	var osbuildJob worker.OSBuildJob
+	err = h.server.workers.OSBuildJob(jobId, &osbuildJob)
+	if err != nil {
+		return HTTPErrorWithInternal(ErrorComposeNotFound, err)
+	}
+
+	if len(osbuildJob.Targets) != 1 {
+		return HTTPError(ErrorSeveralUploadTargets)
+	}
+
+	// the id of the last job in the dependency chain which users should wait on
+	finalJob := jobId
+	// look at the upload status of the osbuild dependency to decide what to do
+	if us.Type == UploadTypesAws {
+		options := us.Options.(AWSEC2UploadStatus)
+		var img AWSEC2CloneCompose
+		err := ctx.Bind(&img)
+		if err != nil {
+			return err
+		}
+
+		shareAmi := options.Ami
+		shareRegion := img.Region
+		if img.Region != options.Region {
+			// Let the share job use dynArgs
+			shareAmi = ""
+			shareRegion = ""
+
+			// Check dependents if we need to do a copyjob
+			foundDep := false
+			for _, d := range osbuildInfo.Dependents {
+				jt, err := h.server.workers.JobType(d)
+				if err != nil {
+					return HTTPErrorWithInternal(ErrorGettingJobType, err)
+				}
+				if jt == worker.JobTypeAWSEC2Copy {
+					var cjResult worker.AWSEC2CopyJobResult
+					_, err := h.server.workers.AWSEC2CopyJobInfo(d, &cjResult)
+					if err != nil {
+						return HTTPErrorWithInternal(ErrorGettingAWSEC2JobStatus, err)
+					}
+
+					if cjResult.JobError == nil && options.Region == cjResult.Region {
+						finalJob = d
+						foundDep = true
+						break
+					}
+				}
+			}
+
+			if !foundDep {
+				copyJob := &worker.AWSEC2CopyJob{
+					Ami:          options.Ami,
+					SourceRegion: options.Region,
+					TargetRegion: img.Region,
+					TargetName:   fmt.Sprintf("composer-api-%s", uuid.New().String()),
+				}
+				finalJob, err = h.server.workers.EnqueueAWSEC2CopyJob(copyJob, finalJob, channel)
+				if err != nil {
+					return HTTPErrorWithInternal(ErrorEnqueueingJob, err)
+				}
+			}
+		}
+
+		var shares []string
+		awsT, ok := (osbuildJob.Targets[0].Options).(*target.AWSTargetOptions)
+		if !ok {
+			return HTTPError(ErrorUnknownUploadTarget)
+		}
+		if len(awsT.ShareWithAccounts) > 0 {
+			shares = append(shares, awsT.ShareWithAccounts...)
+		}
+		if img.ShareWithAccounts != nil && len(*img.ShareWithAccounts) > 0 {
+			shares = append(shares, (*img.ShareWithAccounts)...)
+		}
+		if len(shares) > 0 {
+			shareJob := &worker.AWSEC2ShareJob{
+				Ami:               shareAmi,
+				Region:            shareRegion,
+				ShareWithAccounts: shares,
+			}
+			finalJob, err = h.server.workers.EnqueueAWSEC2ShareJob(shareJob, finalJob, channel)
+			if err != nil {
+				return HTTPErrorWithInternal(ErrorEnqueueingJob, err)
+			}
+		}
+	} else {
+		return HTTPError(ErrorUnsupportedImage)
+	}
+
+	return ctx.JSON(http.StatusCreated, CloneComposeResponse{
+		ObjectReference: ObjectReference{
+			Href: fmt.Sprintf("/api/image-builder-composer/v2/composes/%v/clone", jobId),
+			Id:   finalJob.String(),
+			Kind: "CloneComposeId",
+		},
+		Id: finalJob.String(),
+	})
+}
+
+func (h *apiHandlers) GetCloneStatus(ctx echo.Context, id string) error {
+	return h.server.EnsureJobChannel(h.getCloneStatus)(ctx, id)
+}
+
+func (h *apiHandlers) getCloneStatus(ctx echo.Context, id string) error {
+	jobId, err := uuid.Parse(id)
+	if err != nil {
+		return HTTPError(ErrorInvalidComposeId)
+	}
+
+	jobType, err := h.server.workers.JobType(jobId)
+	if err != nil {
+		return HTTPError(ErrorComposeNotFound)
+	}
+
+	var us UploadStatus
+	switch jobType {
+	case worker.JobTypeAWSEC2Copy:
+		var result worker.AWSEC2CopyJobResult
+		info, err := h.server.workers.AWSEC2CopyJobInfo(jobId, &result)
+		if err != nil {
+			return HTTPError(ErrorGettingAWSEC2JobStatus)
+		}
+
+		us = UploadStatus{
+			Status: uploadStatusFromJobStatus(info.JobStatus, result.JobError),
+			Type:   UploadTypesAws,
+			Options: AWSEC2UploadStatus{
+				Ami:    result.Ami,
+				Region: result.Region,
+			},
+		}
+	case worker.JobTypeAWSEC2Share:
+		var result worker.AWSEC2ShareJobResult
+		info, err := h.server.workers.AWSEC2ShareJobInfo(jobId, &result)
+		if err != nil {
+			return HTTPError(ErrorGettingAWSEC2JobStatus)
+		}
+
+		us = UploadStatus{
+			Status: uploadStatusFromJobStatus(info.JobStatus, result.JobError),
+			Type:   UploadTypesAws,
+			Options: AWSEC2UploadStatus{
+				Ami:    result.Ami,
+				Region: result.Region,
+			},
+		}
+	default:
+		return HTTPError(ErrorInvalidJobType)
+	}
+
+	return ctx.JSON(http.StatusOK, CloneStatus{
+		ObjectReference: ObjectReference{
+			Href: fmt.Sprintf("/api/image-builder-composer/v2/clones/%v", jobId),
+			Id:   jobId.String(),
+			Kind: "CloneComposeStatus",
+		},
+		UploadStatus: us,
+	})
+}
+
+// TODO: determine upload status based on the target results, not job results
+func uploadStatusFromJobStatus(js *worker.JobStatus, je *clienterrors.Error) UploadStatusValue {
+	if je != nil || js.Canceled {
+		return UploadStatusValueFailure
+	}
+
+	if js.Started.IsZero() {
+		return UploadStatusValuePending
+	}
+
+	if js.Finished.IsZero() {
+		return UploadStatusValueRunning
+	}
+	return UploadStatusValueSuccess
+}
diff --git a/internal/cloudapi/v2/openapi.v2.gen.go b/internal/cloudapi/v2/openapi.v2.gen.go
index 91cd5ae99..3b9f2b0ca 100644
--- a/internal/cloudapi/v2/openapi.v2.gen.go
+++ b/internal/cloudapi/v2/openapi.v2.gen.go
@@ -101,6 +101,12 @@ const (
 	UploadTypesGcp UploadTypes = "gcp"
 )
 
+// AWSEC2CloneCompose defines model for AWSEC2CloneCompose.
+type AWSEC2CloneCompose struct {
+	Region            string    `json:"region"`
+	ShareWithAccounts *[]string `json:"share_with_accounts,omitempty"`
+}
+
 // AWSEC2UploadOptions defines model for AWSEC2UploadOptions.
 type AWSEC2UploadOptions struct {
 	Region            string   `json:"region"`
@@ -153,6 +159,25 @@ type AzureUploadStatus struct {
 	ImageName string `json:"image_name"`
 }
 
+// CloneComposeBody defines model for CloneComposeBody.
+type CloneComposeBody interface{}
+
+// CloneComposeResponse defines model for CloneComposeResponse.
+type CloneComposeResponse struct {
+	// Embedded struct due to allOf(#/components/schemas/ObjectReference)
+	ObjectReference `yaml:",inline"`
+	// Embedded fields due to inline allOf schema
+	Id string `json:"id"`
+}
+
+// CloneStatus defines model for CloneStatus.
+type CloneStatus struct {
+	// Embedded struct due to allOf(#/components/schemas/ObjectReference)
+	ObjectReference `yaml:",inline"`
+	// Embedded struct due to allOf(#/components/schemas/UploadStatus)
+	UploadStatus `yaml:",inline"`
+}
+
 // ComposeId defines model for ComposeId.
 type ComposeId struct {
 	// Embedded struct due to allOf(#/components/schemas/ObjectReference)
@@ -481,6 +506,9 @@ type Size string
 // PostComposeJSONBody defines parameters for PostCompose.
 type PostComposeJSONBody ComposeRequest
 
+// PostCloneComposeJSONBody defines parameters for PostCloneCompose.
+type PostCloneComposeJSONBody CloneComposeBody
+
 // GetErrorListParams defines parameters for GetErrorList.
 type GetErrorListParams struct {
 	// Page index
@@ -493,14 +521,23 @@ type GetErrorListParams struct {
 // PostComposeJSONRequestBody defines body for PostCompose for application/json ContentType.
 type PostComposeJSONRequestBody PostComposeJSONBody
 
+// PostCloneComposeJSONRequestBody defines body for PostCloneCompose for application/json ContentType.
+type PostCloneComposeJSONRequestBody PostCloneComposeJSONBody
+
 // ServerInterface represents all server handlers.
 type ServerInterface interface {
+	// The status of a cloned compose
+	// (GET /clones/{id})
+	GetCloneStatus(ctx echo.Context, id string) error
 	// Create compose
 	// (POST /compose)
 	PostCompose(ctx echo.Context) error
 	// The status of a compose
 	// (GET /composes/{id})
 	GetComposeStatus(ctx echo.Context, id string) error
+	// Clone an existing compose
+	// (POST /composes/{id}/clone)
+	PostCloneCompose(ctx echo.Context, id string) error
 	// Get logs for a compose.
 	// (GET /composes/{id}/logs)
 	GetComposeLogs(ctx echo.Context, id string) error
@@ -526,6 +563,24 @@ type ServerInterfaceWrapper struct {
 	Handler ServerInterface
 }
 
+// GetCloneStatus converts echo context to params.
+func (w *ServerInterfaceWrapper) GetCloneStatus(ctx echo.Context) error {
+	var err error
+	// ------------- Path parameter "id" -------------
+	var id string
+
+	err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
+	if err != nil {
+		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
+	}
+
+	ctx.Set(BearerScopes, []string{""})
+
+	// Invoke the callback with all the unmarshalled arguments
+	err = w.Handler.GetCloneStatus(ctx, id)
+	return err
+}
+
 // PostCompose converts echo context to params.
 func (w *ServerInterfaceWrapper) PostCompose(ctx echo.Context) error {
 	var err error
@@ -555,6 +610,22 @@ func (w *ServerInterfaceWrapper) GetComposeStatus(ctx echo.Context) error {
 	return err
 }
 
+// PostCloneCompose converts echo context to params.
+func (w *ServerInterfaceWrapper) PostCloneCompose(ctx echo.Context) error {
+	var err error
+	// ------------- Path parameter "id" -------------
+	var id string
+
+	err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
+	if err != nil {
+		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
+	}
+
+	// Invoke the callback with all the unmarshalled arguments
+	err = w.Handler.PostCloneCompose(ctx, id)
+	return err
+}
+
 // GetComposeLogs converts echo context to params.
 func (w *ServerInterfaceWrapper) GetComposeLogs(ctx echo.Context) error {
 	var err error
@@ -689,8 +760,10 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
 		Handler: si,
 	}
 
+	router.GET(baseURL+"/clones/:id", wrapper.GetCloneStatus)
 	router.POST(baseURL+"/compose", wrapper.PostCompose)
 	router.GET(baseURL+"/composes/:id", wrapper.GetComposeStatus)
+	router.POST(baseURL+"/composes/:id/clone", wrapper.PostCloneCompose)
 	router.GET(baseURL+"/composes/:id/logs", wrapper.GetComposeLogs)
 	router.GET(baseURL+"/composes/:id/manifests", wrapper.GetComposeManifests)
 	router.GET(baseURL+"/composes/:id/metadata", wrapper.GetComposeMetadata)
@@ -703,114 +776,116 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
 // Base64 encoded, gzipped, json marshaled Swagger object
 var swaggerSpec = []string{
 
-	"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",
-	"R6B2l8FfAAA=",
+	"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",
 }
 
 // GetSwagger returns the content of the embedded swagger specification file
diff --git a/internal/cloudapi/v2/openapi.v2.yml b/internal/cloudapi/v2/openapi.v2.yml
index 7229bd784..8cccc02b4 100644
--- a/internal/cloudapi/v2/openapi.v2.yml
+++ b/internal/cloudapi/v2/openapi.v2.yml
@@ -211,6 +211,101 @@ paths:
               schema:
                 type: string
 
+  /composes/{id}/clone:
+    post:
+      operationId: postCloneCompose
+      summary: Clone an existing compose
+      parameters:
+        - in: path
+          name: id
+          schema:
+            type: string
+            format: uuid
+            example: 123e4567-e89b-12d3-a456-426655440000
+          required: true
+          description: ID of the compose
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/CloneComposeBody'
+      responses:
+        '201':
+          description: The new image is being created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CloneComposeResponse'
+        '400':
+          description: Invalid compose id
+          content:
+            text/plain:
+              schema:
+                type: string
+        '404':
+          description: Unknown compose id
+          content:
+            text/plain:
+              schema:
+                type: string
+
+  /clones/{id}:
+    get:
+      operationId: getCloneStatus
+      summary: The status of a cloned compose
+      security:
+        - Bearer: []
+      parameters:
+        - in: path
+          name: id
+          schema:
+            type: string
+            format: uuid
+            example: '123e4567-e89b-12d3-a456-426655440000'
+          required: true
+          description: ID of image status to get
+      description: |-
+        Get the status of a running or completed image from a compose.
+        This includes whether or not the image creation succeeded.
+      responses:
+        '200':
+          description: image status
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CloneStatus'
+        '400':
+          description: Invalid compose id
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '401':
+          description: Auth token is invalid
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '403':
+          description: Unauthorized to perform operation
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '404':
+          description: Unknown compose id
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '500':
+          description: Unexpected error occurred
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
   /compose:
     post:
       operationId: postCompose
@@ -1030,6 +1125,40 @@ components:
             format: uuid
             example: '123e4567-e89b-12d3-a456-426655440000'
 
+    CloneComposeBody:
+      oneOf:
+      - $ref: '#/components/schemas/AWSEC2CloneCompose'
+
+    AWSEC2CloneCompose:
+      type: object
+      required:
+        - region
+      properties:
+        region:
+          type: string
+        share_with_accounts:
+          type: array
+          example: ['123456789012']
+          items:
+            type: string
+
+    CloneComposeResponse:
+      allOf:
+      - $ref: '#/components/schemas/ObjectReference'
+      - type: object
+        required:
+          - id
+        properties:
+          id:
+            type: string
+            format: uuid
+            example: '123e4567-e89b-12d3-a456-426655440000'
+
+    CloneStatus:
+      allOf:
+      - $ref: '#/components/schemas/ObjectReference'
+      - $ref: '#/components/schemas/UploadStatus'
+
   parameters:
     page:
       name: page
diff --git a/internal/distro/image_config.go b/internal/distro/image_config.go
index f8847c00c..a428093fa 100644
--- a/internal/distro/image_config.go
+++ b/internal/distro/image_config.go
@@ -31,29 +31,30 @@ type ImageConfig struct {
 
 	// for RHSM configuration, we need to potentially distinguish the case
 	// when the user want the image to be subscribed on first boot and when not
-	RHSMConfig         map[RHSMSubscriptionStatus]*osbuild.RHSMStageOptions
-	SystemdLogind      []*osbuild.SystemdLogindStageOptions
-	CloudInit          []*osbuild.CloudInitStageOptions
-	Modprobe           []*osbuild.ModprobeStageOptions
-	DracutConf         []*osbuild.DracutConfStageOptions
-	SystemdUnit        []*osbuild.SystemdUnitStageOptions
-	Authselect         *osbuild.AuthselectStageOptions
-	SELinuxConfig      *osbuild.SELinuxConfigStageOptions
-	Tuned              *osbuild.TunedStageOptions
-	Tmpfilesd          []*osbuild.TmpfilesdStageOptions
-	PamLimitsConf      []*osbuild.PamLimitsConfStageOptions
-	Sysctld            []*osbuild.SysctldStageOptions
-	DNFConfig          []*osbuild.DNFConfigStageOptions
-	SshdConfig         *osbuild.SshdConfigStageOptions
-	Authconfig         *osbuild.AuthconfigStageOptions
-	PwQuality          *osbuild.PwqualityConfStageOptions
-	WAAgentConfig      *osbuild.WAAgentConfStageOptions
-	Grub2Config        *osbuild.GRUB2Config
-	DNFAutomaticConfig *osbuild.DNFAutomaticConfigStageOptions
-	YumConfig          *osbuild.YumConfigStageOptions
-	YUMRepos           []*osbuild.YumReposStageOptions
-	Firewall           *osbuild.FirewallStageOptions
-	UdevRules          *osbuild.UdevRulesStageOptions
+	RHSMConfig          map[RHSMSubscriptionStatus]*osbuild.RHSMStageOptions
+	SystemdLogind       []*osbuild.SystemdLogindStageOptions
+	CloudInit           []*osbuild.CloudInitStageOptions
+	Modprobe            []*osbuild.ModprobeStageOptions
+	DracutConf          []*osbuild.DracutConfStageOptions
+	SystemdUnit         []*osbuild.SystemdUnitStageOptions
+	Authselect          *osbuild.AuthselectStageOptions
+	SELinuxConfig       *osbuild.SELinuxConfigStageOptions
+	Tuned               *osbuild.TunedStageOptions
+	Tmpfilesd           []*osbuild.TmpfilesdStageOptions
+	PamLimitsConf       []*osbuild.PamLimitsConfStageOptions
+	Sysctld             []*osbuild.SysctldStageOptions
+	DNFConfig           []*osbuild.DNFConfigStageOptions
+	SshdConfig          *osbuild.SshdConfigStageOptions
+	Authconfig          *osbuild.AuthconfigStageOptions
+	PwQuality           *osbuild.PwqualityConfStageOptions
+	WAAgentConfig       *osbuild.WAAgentConfStageOptions
+	Grub2Config         *osbuild.GRUB2Config
+	DNFAutomaticConfig  *osbuild.DNFAutomaticConfigStageOptions
+	YumConfig           *osbuild.YumConfigStageOptions
+	YUMRepos            []*osbuild.YumReposStageOptions
+	Firewall            *osbuild.FirewallStageOptions
+	UdevRules           *osbuild.UdevRulesStageOptions
+	GCPGuestAgentConfig *osbuild.GcpGuestAgentConfigOptions
 }
 
 // InheritFrom inherits unset values from the provided parent configuration and
@@ -151,6 +152,9 @@ func (c *ImageConfig) InheritFrom(parentConfig *ImageConfig) *ImageConfig {
 		if finalConfig.UdevRules == nil {
 			finalConfig.UdevRules = parentConfig.UdevRules
 		}
+		if finalConfig.GCPGuestAgentConfig == nil {
+			finalConfig.GCPGuestAgentConfig = parentConfig.GCPGuestAgentConfig
+		}
 	}
 	return &finalConfig
 }
diff --git a/internal/distro/rhel8/distro.go b/internal/distro/rhel8/distro.go
index 1bb1ebcd4..a1b3891dc 100644
--- a/internal/distro/rhel8/distro.go
+++ b/internal/distro/rhel8/distro.go
@@ -1778,6 +1778,14 @@ func newDistro(distroName string) distro.Distro {
 				},
 			},
 		},
+		GCPGuestAgentConfig: &osbuild.GcpGuestAgentConfigOptions{
+			ConfigScope: osbuild.GcpGuestAgentConfigScopeDistro,
+			Config: &osbuild.GcpGuestAgentConfig{
+				InstanceSetup: &osbuild.GcpGuestAgentConfigInstanceSetup{
+					SetBotoConfig: common.BoolToPtr(false),
+				},
+			},
+		},
 	}
 
 	if rd.osVersion == "8.4" {
diff --git a/internal/distro/rhel8/pipelines.go b/internal/distro/rhel8/pipelines.go
index 4d47ffc51..02fc55216 100644
--- a/internal/distro/rhel8/pipelines.go
+++ b/internal/distro/rhel8/pipelines.go
@@ -647,6 +647,10 @@ func osPipeline(t *imageType,
 		p.AddStage(osbuild.NewYumReposStage(yumRepo))
 	}
 
+	if gcpGuestAgentConfig := imageConfig.GCPGuestAgentConfig; gcpGuestAgentConfig != nil {
+		p.AddStage(osbuild.NewGcpGuestAgentConfigStage(gcpGuestAgentConfig))
+	}
+
 	if udevRules := imageConfig.UdevRules; udevRules != nil {
 		p.AddStage(osbuild.NewUdevRulesStage(udevRules))
 	}
diff --git a/internal/distro/rhel9/distro.go b/internal/distro/rhel9/distro.go
index b4814c38e..6df221757 100644
--- a/internal/distro/rhel9/distro.go
+++ b/internal/distro/rhel9/distro.go
@@ -1666,6 +1666,14 @@ func newDistro(distroName string) distro.Distro {
 				},
 			},
 		},
+		GCPGuestAgentConfig: &osbuild.GcpGuestAgentConfigOptions{
+			ConfigScope: osbuild.GcpGuestAgentConfigScopeDistro,
+			Config: &osbuild.GcpGuestAgentConfig{
+				InstanceSetup: &osbuild.GcpGuestAgentConfigInstanceSetup{
+					SetBotoConfig: common.BoolToPtr(false),
+				},
+			},
+		},
 	}
 
 	gceImgType := imageType{
diff --git a/internal/distro/rhel9/pipelines.go b/internal/distro/rhel9/pipelines.go
index 6b79907df..1102c9e1b 100644
--- a/internal/distro/rhel9/pipelines.go
+++ b/internal/distro/rhel9/pipelines.go
@@ -639,6 +639,10 @@ func osPipeline(t *imageType,
 		p.AddStage(osbuild.NewYumReposStage(yumRepo))
 	}
 
+	if gcpGuestAgentConfig := imageConfig.GCPGuestAgentConfig; gcpGuestAgentConfig != nil {
+		p.AddStage(osbuild.NewGcpGuestAgentConfigStage(gcpGuestAgentConfig))
+	}
+
 	if udevRules := imageConfig.UdevRules; udevRules != nil {
 		p.AddStage(osbuild.NewUdevRulesStage(udevRules))
 	}
diff --git a/internal/jobqueue/fsjobqueue/fsjobqueue.go b/internal/jobqueue/fsjobqueue/fsjobqueue.go
index b2c3fa464..da187b8c6 100644
--- a/internal/jobqueue/fsjobqueue/fsjobqueue.go
+++ b/internal/jobqueue/fsjobqueue/fsjobqueue.go
@@ -66,6 +66,7 @@ type job struct {
 	Type         string          `json:"type"`
 	Args         json.RawMessage `json:"args,omitempty"`
 	Dependencies []uuid.UUID     `json:"dependencies"`
+	Dependents   []uuid.UUID     `json:"dependents"`
 	Result       json.RawMessage `json:"result,omitempty"`
 	Channel      string          `json:"channel"`
 
@@ -151,13 +152,20 @@ func (q *fsJobQueue) Enqueue(jobType string, args interface{}, dependencies []uu
 	// Verify dependendencies early, so that the job doesn't get written
 	// when one of them doesn't exist.
 	for _, d := range j.Dependencies {
-		exists, err := q.db.Read(d.String(), nil)
+		var dep job
+		exists, err := q.db.Read(d.String(), &dep)
 		if err != nil {
 			return uuid.Nil, err
 		}
 		if !exists {
 			return uuid.Nil, jobqueue.ErrNotExist
 		}
+
+		dep.Dependents = append(dep.Dependents, j.Id)
+		err = q.db.Write(d.String(), dep)
+		if err != nil {
+			return uuid.Nil, err
+		}
 	}
 
 	// Write the job before updating in-memory state, so that the latter
@@ -344,7 +352,7 @@ func (q *fsJobQueue) CancelJob(id uuid.UUID) error {
 	return nil
 }
 
-func (q *fsJobQueue) JobStatus(id uuid.UUID) (jobType string, channel string, result json.RawMessage, queued, started, finished time.Time, canceled bool, deps []uuid.UUID, err error) {
+func (q *fsJobQueue) JobStatus(id uuid.UUID) (jobType string, channel string, result json.RawMessage, queued, started, finished time.Time, canceled bool, deps []uuid.UUID, dependents []uuid.UUID, err error) {
 	j, err := q.readJob(id)
 	if err != nil {
 		return
@@ -358,6 +366,7 @@ func (q *fsJobQueue) JobStatus(id uuid.UUID) (jobType string, channel string, re
 	finished = j.FinishedAt
 	canceled = j.Canceled
 	deps = j.Dependencies
+	dependents = j.Dependents
 
 	return
 }
diff --git a/internal/osbuild/gcp_guest_agent_conf_stage.go b/internal/osbuild/gcp_guest_agent_conf_stage.go
new file mode 100644
index 000000000..6a5436d9e
--- /dev/null
+++ b/internal/osbuild/gcp_guest_agent_conf_stage.go
@@ -0,0 +1,118 @@
+package osbuild
+
+import (
+	"fmt"
+)
+
+type GcpGuestAgentConfigScopeValue string
+
+const (
+	GcpGuestAgentConfigScopeDistro   GcpGuestAgentConfigScopeValue = "distro"
+	GcpGuestAgentConfigScopeInstance GcpGuestAgentConfigScopeValue = "instance"
+)
+
+type GcpGuestAgentConfigAccounts struct {
+	DeprovisionRemove *bool    `json:"deprovision_remove,omitempty"`
+	Groups            []string `json:"groups,omitempty"`
+	UseraddCmd        string   `json:"useradd_cmd,omitempty"`
+	UserdelCmd        string   `json:"userdel_cmd,omitempty"`
+	UsermodCmd        string   `json:"usermod_cmd,omitempty"`
+	GpasswdAddCmd     string   `json:"gpasswd_add_cmd,omitempty"`
+	GpasswdRemoveCmd  string   `json:"gpasswd_remove_cmd,omitempty"`
+	GroupaddCmd       string   `json:"groupadd_cmd,omitempty"`
+}
+
+type GcpGuestAgentConfigDaemons struct {
+	AccountsDaemon  *bool `json:"accounts_daemon,omitempty"`
+	ClockSkewDaemon *bool `json:"clock_skew_daemon,omitempty"`
+	NetworkDaemon   *bool `json:"network_daemon,omitempty"`
+}
+
+type GcpGuestAgentConfigInstanceSetup struct {
+	HostKeyTypes     []string `json:"host_key_types,omitempty"`
+	OptimizeLocalSsd *bool    `json:"optimize_local_ssd,omitempty"`
+	NetworkEnabled   *bool    `json:"network_enabled,omitempty"`
+	SetBotoConfig    *bool    `json:"set_boto_config,omitempty"`
+	SetHostKeys      *bool    `json:"set_host_keys,omitempty"`
+	SetMultiqueue    *bool    `json:"set_multiqueue,omitempty"`
+}
+
+type GcpGuestAgentConfigIpForwarding struct {
+	EthernetProtoId   string `json:"ethernet_proto_id,omitempty"`
+	IpAliases         *bool  `json:"ip_aliases,omitempty"`
+	TargetInstanceIps *bool  `json:"target_instance_ips,omitempty"`
+}
+
+type GcpGuestAgentConfigMetadataScripts struct {
+	DefaultShell string `json:"default_shell,omitempty"`
+	RunDir       string `json:"run_dir,omitempty"`
+	Startup      *bool  `json:"startup,omitempty"`
+	Shutdown     *bool  `json:"shutdown,omitempty"`
+}
+
+type GcpGuestAgentConfigNetworkInterfaces struct {
+	Setup        *bool  `json:"setup,omitempty"`
+	IpForwarding *bool  `json:"ip_forwarding,omitempty"`
+	DhcpCommand  string `json:"dhcp_command,omitempty"`
+}
+
+type GcpGuestAgentConfig struct {
+	Accounts          *GcpGuestAgentConfigAccounts          `json:"Accounts,omitempty"`
+	Daemons           *GcpGuestAgentConfigDaemons           `json:"Daemons,omitempty"`
+	InstanceSetup     *GcpGuestAgentConfigInstanceSetup     `json:"InstanceSetup,omitempty"`
+	IpForwarding      *GcpGuestAgentConfigIpForwarding      `json:"IpForwarding,omitempty"`
+	MetadataScripts   *GcpGuestAgentConfigMetadataScripts   `json:"MetadataScripts,omitempty"`
+	NetworkInterfaces *GcpGuestAgentConfigNetworkInterfaces `json:"NetworkInterfaces,omitempty"`
+}
+
+type GcpGuestAgentConfigOptions struct {
+	ConfigScope GcpGuestAgentConfigScopeValue `json:"config_scope,omitempty"`
+	Config      *GcpGuestAgentConfig          `json:"config"`
+}
+
+func (GcpGuestAgentConfigOptions) isStageOptions() {}
+
+func (o GcpGuestAgentConfigOptions) validate() error {
+	allowedScopeValues := []GcpGuestAgentConfigScopeValue{
+		GcpGuestAgentConfigScopeDistro,
+		GcpGuestAgentConfigScopeInstance,
+		"", // default empty value when the option is not set
+	}
+
+	valid := false
+	for _, value := range allowedScopeValues {
+		if o.ConfigScope == value {
+			valid = true
+			break
+		}
+	}
+
+	if !valid {
+		return fmt.Errorf(
+			"scope %q doesn't conform to schema (%s). Must be one of [distro, instance]",
+			o.ConfigScope,
+			repoFilenameRegex,
+		)
+	}
+
+	if o.Config == nil {
+		return fmt.Errorf("config property is required")
+	}
+
+	if (GcpGuestAgentConfig{}) == *o.Config {
+		return fmt.Errorf("at least one config section must be defined")
+	}
+
+	return nil
+}
+
+func NewGcpGuestAgentConfigStage(options *GcpGuestAgentConfigOptions) *Stage {
+	if err := options.validate(); err != nil {
+		panic(err)
+	}
+
+	return &Stage{
+		Type:    "org.osbuild.gcp.guest-agent.conf",
+		Options: options,
+	}
+}
diff --git a/internal/worker/json.go b/internal/worker/json.go
index adb094aac..7adc79447 100644
--- a/internal/worker/json.go
+++ b/internal/worker/json.go
@@ -256,6 +256,33 @@ type ContainerResolveJobResult struct {
 	JobResult
 }
 
+type AWSEC2ShareJob struct {
+	Ami               string   `json:"ami"`
+	Region            string   `json:"region"`
+	ShareWithAccounts []string `json:"shareWithAccounts"`
+}
+
+type AWSEC2ShareJobResult struct {
+	JobResult
+
+	Ami    string `json:"ami"`
+	Region string `json:"region"`
+}
+
+type AWSEC2CopyJob struct {
+	Ami          string `json:"ami"`
+	SourceRegion string `json:"source_region"`
+	TargetRegion string `json:"target_region"`
+	TargetName   string `json:"target_name"`
+}
+
+type AWSEC2CopyJobResult struct {
+	JobResult
+
+	Ami    string `json:"ami"`
+	Region string `json:"region"`
+}
+
 //
 // JSON-serializable types for the client
 //
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 1289a0681..ff57ca8dd 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -35,6 +35,8 @@ const (
 	JobTypeDepsolve         string = "depsolve"
 	JobTypeManifestIDOnly   string = "manifest-id-only"
 	JobTypeContainerResolve string = "container-resolve"
+	JobTypeAWSEC2Copy       string = "aws-ec2-copy"
+	JobTypeAWSEC2Share      string = "aws-ec2-share"
 )
 
 type Server struct {
@@ -51,10 +53,11 @@ type JobStatus struct {
 }
 
 type JobInfo struct {
-	JobType   string
-	Channel   string
-	JobStatus *JobStatus
-	Deps      []uuid.UUID
+	JobType    string
+	Channel    string
+	JobStatus  *JobStatus
+	Deps       []uuid.UUID
+	Dependents []uuid.UUID
 }
 
 var ErrInvalidToken = errors.New("token does not exist")
@@ -157,6 +160,14 @@ func (s *Server) EnqueueContainerResolveJob(job *ContainerResolveJob, channel st
 	return s.enqueue(JobTypeContainerResolve, job, nil, channel)
 }
 
+func (s *Server) EnqueueAWSEC2CopyJob(job *AWSEC2CopyJob, parent uuid.UUID, channel string) (uuid.UUID, error) {
+	return s.enqueue(JobTypeAWSEC2Copy, job, []uuid.UUID{parent}, channel)
+}
+
+func (s *Server) EnqueueAWSEC2ShareJob(job *AWSEC2ShareJob, parent uuid.UUID, channel string) (uuid.UUID, error) {
+	return s.enqueue(JobTypeAWSEC2Share, job, []uuid.UUID{parent}, channel)
+}
+
 func (s *Server) enqueue(jobType string, job interface{}, dependencies []uuid.UUID, channel string) (uuid.UUID, error) {
 	prometheus.EnqueueJobMetrics(strings.Split(jobType, ":")[0], channel)
 	return s.jobs.Enqueue(jobType, job, dependencies, channel)
@@ -359,8 +370,34 @@ func (s *Server) ContainerResolveJobInfo(id uuid.UUID, result *ContainerResolveJ
 	return jobInfo, nil
 }
 
+func (s *Server) AWSEC2CopyJobInfo(id uuid.UUID, result *AWSEC2CopyJobResult) (*JobInfo, error) {
+	jobInfo, err := s.jobInfo(id, result)
+	if err != nil {
+		return nil, err
+	}
+
+	if jobInfo.JobType != JobTypeAWSEC2Copy {
+		return nil, fmt.Errorf("expected %q, found %q job instead", JobTypeAWSEC2Copy, jobInfo.JobType)
+	}
+
+	return jobInfo, nil
+}
+
+func (s *Server) AWSEC2ShareJobInfo(id uuid.UUID, result *AWSEC2ShareJobResult) (*JobInfo, error) {
+	jobInfo, err := s.jobInfo(id, result)
+	if err != nil {
+		return nil, err
+	}
+
+	if jobInfo.JobType != JobTypeAWSEC2Share {
+		return nil, fmt.Errorf("expected %q, found %q job instead", JobTypeAWSEC2Share, jobInfo.JobType)
+	}
+
+	return jobInfo, nil
+}
+
 func (s *Server) jobInfo(id uuid.UUID, result interface{}) (*JobInfo, error) {
-	jobType, channel, rawResult, queued, started, finished, canceled, deps, err := s.jobs.JobStatus(id)
+	jobType, channel, rawResult, queued, started, finished, canceled, deps, dependents, err := s.jobs.JobStatus(id)
 	if err != nil {
 		return nil, err
 	}
@@ -381,7 +418,8 @@ func (s *Server) jobInfo(id uuid.UUID, result interface{}) (*JobInfo, error) {
 			Finished: finished,
 			Canceled: canceled,
 		},
-		Deps: deps,
+		Deps:       deps,
+		Dependents: dependents,
 	}, nil
 }
 
@@ -532,7 +570,7 @@ func (s *Server) requestJob(ctx context.Context, arch string, jobTypes []string,
 		// TODO: include type of arguments
 		var result json.RawMessage
 		var finished time.Time
-		_, _, result, _, _, finished, _, _, err = s.jobs.JobStatus(depID)
+		_, _, result, _, _, finished, _, _, _, err = s.jobs.JobStatus(depID)
 		if err != nil {
 			return
 		}
@@ -624,6 +662,20 @@ func (s *Server) FinishJob(token uuid.UUID, result json.RawMessage) error {
 			return err
 		}
 		jobResult = &kojiFinalizeJR.JobResult
+	case JobTypeAWSEC2Copy:
+		var awsEC2CopyJR AWSEC2CopyJobResult
+		jobInfo, err = s.AWSEC2CopyJobInfo(jobId, &awsEC2CopyJR)
+		if err != nil {
+			return err
+		}
+		jobResult = &awsEC2CopyJR.JobResult
+	case JobTypeAWSEC2Share:
+		var awsEC2ShareJR AWSEC2ShareJobResult
+		jobInfo, err = s.AWSEC2ShareJobInfo(jobId, &awsEC2ShareJR)
+		if err != nil {
+			return err
+		}
+		jobResult = &awsEC2ShareJR.JobResult
 
 	case JobTypeContainerResolve:
 		var containerResolveJR ContainerResolveJobResult
diff --git a/osbuild-composer.spec b/osbuild-composer.spec
index 3763632bf..b29062d24 100644
--- a/osbuild-composer.spec
+++ b/osbuild-composer.spec
@@ -309,7 +309,7 @@ The core osbuild-composer binary. This is suitable both for spawning in containe
 Summary:    The worker for osbuild-composer
 Requires:   systemd
 Requires:   qemu-img
-Requires:   osbuild >= 63
+Requires:   osbuild >= 64
 Requires:   osbuild-ostree >= 63
 Requires:   osbuild-lvm2 >= 63
 Requires:   osbuild-luks2 >= 63
diff --git a/pkg/jobqueue/dbjobqueue/dbjobqueue.go b/pkg/jobqueue/dbjobqueue/dbjobqueue.go
index 25231224f..e59d4f629 100644
--- a/pkg/jobqueue/dbjobqueue/dbjobqueue.go
+++ b/pkg/jobqueue/dbjobqueue/dbjobqueue.go
@@ -61,6 +61,10 @@ const (
 		SELECT dependency_id
 		FROM job_dependencies
 		WHERE job_id = $1`
+	sqlQueryDependents = `
+		SELECT job_id
+		FROM job_dependencies
+		WHERE dependency_id = $1`
 
 	sqlQueryJob = `
 		SELECT type, args, channel, started_at, finished_at, canceled
@@ -462,7 +466,7 @@ func (q *DBJobQueue) CancelJob(id uuid.UUID) error {
 	return nil
 }
 
-func (q *DBJobQueue) JobStatus(id uuid.UUID) (jobType string, channel string, result json.RawMessage, queued, started, finished time.Time, canceled bool, deps []uuid.UUID, err error) {
+func (q *DBJobQueue) JobStatus(id uuid.UUID) (jobType string, channel string, result json.RawMessage, queued, started, finished time.Time, canceled bool, deps []uuid.UUID, dependents []uuid.UUID, err error) {
 	conn, err := q.pool.Acquire(context.Background())
 	if err != nil {
 		return
@@ -490,6 +494,11 @@ func (q *DBJobQueue) JobStatus(id uuid.UUID) (jobType string, channel string, re
 	if err != nil {
 		return
 	}
+
+	dependents, err = q.jobDependents(context.Background(), conn, id)
+	if err != nil {
+		return
+	}
 	return
 }
 
@@ -602,3 +611,27 @@ func (q *DBJobQueue) jobDependencies(ctx context.Context, conn *pgxpool.Conn, id
 
 	return dependencies, nil
 }
+
+func (q *DBJobQueue) jobDependents(ctx context.Context, conn *pgxpool.Conn, id uuid.UUID) ([]uuid.UUID, error) {
+	rows, err := conn.Query(ctx, sqlQueryDependents, id)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	dependents := []uuid.UUID{}
+	for rows.Next() {
+		var d uuid.UUID
+		err = rows.Scan(&d)
+		if err != nil {
+			return nil, err
+		}
+
+		dependents = append(dependents, d)
+	}
+	if rows.Err() != nil {
+		return nil, err
+	}
+
+	return dependents, nil
+}
diff --git a/pkg/jobqueue/jobqueue.go b/pkg/jobqueue/jobqueue.go
index d23133728..ae8fad879 100644
--- a/pkg/jobqueue/jobqueue.go
+++ b/pkg/jobqueue/jobqueue.go
@@ -64,7 +64,7 @@ type JobQueue interface {
 	// finished, respectively.
 	//
 	// Lastly, the IDs of the jobs dependencies are returned.
-	JobStatus(id uuid.UUID) (jobType string, channel string, result json.RawMessage, queued, started, finished time.Time, canceled bool, deps []uuid.UUID, err error)
+	JobStatus(id uuid.UUID) (jobType string, channel string, result json.RawMessage, queued, started, finished time.Time, canceled bool, deps []uuid.UUID, dependents []uuid.UUID, err error)
 
 	// Job returns all the parameters that define a job (everything provided during Enqueue).
 	Job(id uuid.UUID) (jobType string, args json.RawMessage, dependencies []uuid.UUID, channel string, err error)
diff --git a/vendor/github.com/labstack/echo/v4/CHANGELOG.md b/vendor/github.com/labstack/echo/v4/CHANGELOG.md
index ba75d71f6..2fcb2ff7f 100644
--- a/vendor/github.com/labstack/echo/v4/CHANGELOG.md
+++ b/vendor/github.com/labstack/echo/v4/CHANGELOG.md
@@ -1,5 +1,36 @@
 # Changelog
 
+## v4.8.0 - 2022-08-10
+
+**Most notable things**
+
+You can now add any arbitrary HTTP method type as a route [#2237](https://github.com/labstack/echo/pull/2237)
+```go
+e.Add("COPY", "/*", func(c echo.Context) error 
+  return c.String(http.StatusOK, "OK COPY")
+})
+```
+
+You can add custom 404 handler for specific paths [#2217](https://github.com/labstack/echo/pull/2217)
+```go
+e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })
+
+g := e.Group("/images")
+g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })
+```
+
+**Enhancements**
+
+* Add new value binding methods (UnixTimeMilli,TextUnmarshaler,JSONUnmarshaler) to Valuebinder [#2127](https://github.com/labstack/echo/pull/2127)
+* Refactor: body_limit middleware unit test [#2145](https://github.com/labstack/echo/pull/2145)
+* Refactor: Timeout mw: rework how test waits for timeout. [#2187](https://github.com/labstack/echo/pull/2187)
+* BasicAuth middleware returns 500 InternalServerError on invalid base64 strings but should return 400 [#2191](https://github.com/labstack/echo/pull/2191)
+* Refactor: duplicated findStaticChild process at findChildWithLabel [#2176](https://github.com/labstack/echo/pull/2176)
+* Allow different param names in different methods with same path scheme [#2209](https://github.com/labstack/echo/pull/2209)
+* Add support for registering handlers for different 404 routes [#2217](https://github.com/labstack/echo/pull/2217)
+* Middlewares should use errors.As() instead of type assertion on HTTPError [#2227](https://github.com/labstack/echo/pull/2227)
+* Allow arbitrary HTTP method types to be added as routes [#2237](https://github.com/labstack/echo/pull/2237)
+
 ## v4.7.2 - 2022-03-16
 
 **Fixes**
diff --git a/vendor/github.com/labstack/echo/v4/Makefile b/vendor/github.com/labstack/echo/v4/Makefile
index 48061f7e2..a6c4aaa90 100644
--- a/vendor/github.com/labstack/echo/v4/Makefile
+++ b/vendor/github.com/labstack/echo/v4/Makefile
@@ -9,7 +9,7 @@ tag:
 check: lint vet race ## Check project
 
 init:
-	@go get -u golang.org/x/lint/golint
+	@go install golang.org/x/lint/golint@latest
 
 lint: ## Lint the files
 	@golint -set_exit_status ${PKG_LIST}
@@ -29,6 +29,6 @@ benchmark: ## Run benchmarks
 help: ## Display this help screen
 	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
 
-goversion ?= "1.15"
-test_version: ## Run tests inside Docker with given version (defaults to 1.15 oldest supported). Example: make test_version goversion=1.15
+goversion ?= "1.16"
+test_version: ## Run tests inside Docker with given version (defaults to 1.15 oldest supported). Example: make test_version goversion=1.16
 	@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check"
diff --git a/vendor/github.com/labstack/echo/v4/README.md b/vendor/github.com/labstack/echo/v4/README.md
index 8b2321f05..17e6ed934 100644
--- a/vendor/github.com/labstack/echo/v4/README.md
+++ b/vendor/github.com/labstack/echo/v4/README.md
@@ -93,15 +93,16 @@ func hello(c echo.Context) error {
 
 # Third-party middlewares
 
-| Repository | Description                                                                                                                                                                                                                                                                                                                                                                                                    |
-|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib) | (by Echo team) [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | 
-| [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator                                                                                                                                                                                                                       |
-| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger) | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0.                                                                                                                                                                                                                                                        |
-| [github.com/ziflex/lecho](https://github.com/ziflex/lecho) | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                                                    |
-| [github.com/brpaz/echozap](https://github.com/brpaz/echozap) | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                                              |
-| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs.                                                                                                             |
-| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda) | Rapid, easy full-stack web development starter kit built with Echo.
+| Repository                                                                                           | Description                                                                                                                                                                                                                                                                                                                                  |
+|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib)                         | (by Echo team) [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | 
+| [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen)                                      | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator                                                                                                                                                                                                          |
+| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger)                             | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0.                                                                                                                                                                                                                                                    |
+| [github.com/ziflex/lecho](https://github.com/ziflex/lecho)                                           | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                                  |
+| [github.com/brpaz/echozap](https://github.com/brpaz/echozap)                                         | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                              |
+| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs.                                                                                                                                     |
+| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda)                         | Rapid, easy full-stack web development starter kit built with Echo.                                                                                                                                                                                                                                                                          |
+| [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo)                       | ProtoBuf generate Echo server side code                                                                                                                                                                                                                                                                                                      |
 
 Please send a PR to add your own library here.
 
diff --git a/vendor/github.com/labstack/echo/v4/binder.go b/vendor/github.com/labstack/echo/v4/binder.go
index 0900ce8dc..5a6cf9d9b 100644
--- a/vendor/github.com/labstack/echo/v4/binder.go
+++ b/vendor/github.com/labstack/echo/v4/binder.go
@@ -1,6 +1,8 @@
 package echo
 
 import (
+	"encoding"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -52,8 +54,11 @@ import (
 		* time
 		* duration
 		* BindUnmarshaler() interface
+		* TextUnmarshaler() interface
+		* JSONUnmarshaler() interface
 		* UnixTime() - converts unix time (integer) to time.Time
-		* UnixTimeNano() - converts unix time with nano second precision (integer) to time.Time
+		* UnixTimeMilli() - converts unix time with millisecond precision (integer) to time.Time
+		* UnixTimeNano() - converts unix time with nanosecond precision (integer) to time.Time
 		* CustomFunc() - callback function for your custom conversion logic. Signature `func(values []string) []error`
 */
 
@@ -204,7 +209,7 @@ func (b *ValueBinder) CustomFunc(sourceParam string, customFunc func(values []st
 	return b.customFunc(sourceParam, customFunc, false)
 }
 
-// MustCustomFunc requires parameter values to exist to be bind with Func. Returns error when value does not exist.
+// MustCustomFunc requires parameter values to exist to bind with Func. Returns error when value does not exist.
 func (b *ValueBinder) MustCustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder {
 	return b.customFunc(sourceParam, customFunc, true)
 }
@@ -241,7 +246,7 @@ func (b *ValueBinder) String(sourceParam string, dest *string) *ValueBinder {
 	return b
 }
 
-// MustString requires parameter value to exist to be bind to string variable. Returns error when value does not exist
+// MustString requires parameter value to exist to bind to string variable. Returns error when value does not exist
 func (b *ValueBinder) MustString(sourceParam string, dest *string) *ValueBinder {
 	if b.failFast && b.errors != nil {
 		return b
@@ -270,7 +275,7 @@ func (b *ValueBinder) Strings(sourceParam string, dest *[]string) *ValueBinder {
 	return b
 }
 
-// MustStrings requires parameter values to exist to be bind to slice of string variables. Returns error when value does not exist
+// MustStrings requires parameter values to exist to bind to slice of string variables. Returns error when value does not exist
 func (b *ValueBinder) MustStrings(sourceParam string, dest *[]string) *ValueBinder {
 	if b.failFast && b.errors != nil {
 		return b
@@ -302,7 +307,7 @@ func (b *ValueBinder) BindUnmarshaler(sourceParam string, dest BindUnmarshaler)
 	return b
 }
 
-// MustBindUnmarshaler requires parameter value to exist to be bind to destination implementing BindUnmarshaler interface.
+// MustBindUnmarshaler requires parameter value to exist to bind to destination implementing BindUnmarshaler interface.
 // Returns error when value does not exist
 func (b *ValueBinder) MustBindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder {
 	if b.failFast && b.errors != nil {
@@ -321,13 +326,85 @@ func (b *ValueBinder) MustBindUnmarshaler(sourceParam string, dest BindUnmarshal
 	return b
 }
 
+// JSONUnmarshaler binds parameter to destination implementing json.Unmarshaler interface
+func (b *ValueBinder) JSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	tmp := b.ValueFunc(sourceParam)
+	if tmp == "" {
+		return b
+	}
+
+	if err := dest.UnmarshalJSON([]byte(tmp)); err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to json.Unmarshaler interface", err))
+	}
+	return b
+}
+
+// MustJSONUnmarshaler requires parameter value to exist to bind to destination implementing json.Unmarshaler interface.
+// Returns error when value does not exist
+func (b *ValueBinder) MustJSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	tmp := b.ValueFunc(sourceParam)
+	if tmp == "" {
+		b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "required field value is empty", nil))
+		return b
+	}
+
+	if err := dest.UnmarshalJSON([]byte(tmp)); err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to json.Unmarshaler interface", err))
+	}
+	return b
+}
+
+// TextUnmarshaler binds parameter to destination implementing encoding.TextUnmarshaler interface
+func (b *ValueBinder) TextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	tmp := b.ValueFunc(sourceParam)
+	if tmp == "" {
+		return b
+	}
+
+	if err := dest.UnmarshalText([]byte(tmp)); err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to encoding.TextUnmarshaler interface", err))
+	}
+	return b
+}
+
+// MustTextUnmarshaler requires parameter value to exist to bind to destination implementing encoding.TextUnmarshaler interface.
+// Returns error when value does not exist
+func (b *ValueBinder) MustTextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	tmp := b.ValueFunc(sourceParam)
+	if tmp == "" {
+		b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "required field value is empty", nil))
+		return b
+	}
+
+	if err := dest.UnmarshalText([]byte(tmp)); err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to encoding.TextUnmarshaler interface", err))
+	}
+	return b
+}
+
 // BindWithDelimiter binds parameter to destination by suitable conversion function.
 // Delimiter is used before conversion to split parameter value to separate values
 func (b *ValueBinder) BindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder {
 	return b.bindWithDelimiter(sourceParam, dest, delimiter, false)
 }
 
-// MustBindWithDelimiter requires parameter value to exist to be bind destination by suitable conversion function.
+// MustBindWithDelimiter requires parameter value to exist to bind destination by suitable conversion function.
 // Delimiter is used before conversion to split parameter value to separate values
 func (b *ValueBinder) MustBindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder {
 	return b.bindWithDelimiter(sourceParam, dest, delimiter, true)
@@ -376,7 +453,7 @@ func (b *ValueBinder) Int64(sourceParam string, dest *int64) *ValueBinder {
 	return b.intValue(sourceParam, dest, 64, false)
 }
 
-// MustInt64 requires parameter value to exist to be bind to int64 variable. Returns error when value does not exist
+// MustInt64 requires parameter value to exist to bind to int64 variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt64(sourceParam string, dest *int64) *ValueBinder {
 	return b.intValue(sourceParam, dest, 64, true)
 }
@@ -386,7 +463,7 @@ func (b *ValueBinder) Int32(sourceParam string, dest *int32) *ValueBinder {
 	return b.intValue(sourceParam, dest, 32, false)
 }
 
-// MustInt32 requires parameter value to exist to be bind to int32 variable. Returns error when value does not exist
+// MustInt32 requires parameter value to exist to bind to int32 variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt32(sourceParam string, dest *int32) *ValueBinder {
 	return b.intValue(sourceParam, dest, 32, true)
 }
@@ -396,7 +473,7 @@ func (b *ValueBinder) Int16(sourceParam string, dest *int16) *ValueBinder {
 	return b.intValue(sourceParam, dest, 16, false)
 }
 
-// MustInt16 requires parameter value to exist to be bind to int16 variable. Returns error when value does not exist
+// MustInt16 requires parameter value to exist to bind to int16 variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt16(sourceParam string, dest *int16) *ValueBinder {
 	return b.intValue(sourceParam, dest, 16, true)
 }
@@ -406,7 +483,7 @@ func (b *ValueBinder) Int8(sourceParam string, dest *int8) *ValueBinder {
 	return b.intValue(sourceParam, dest, 8, false)
 }
 
-// MustInt8 requires parameter value to exist to be bind to int8 variable. Returns error when value does not exist
+// MustInt8 requires parameter value to exist to bind to int8 variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt8(sourceParam string, dest *int8) *ValueBinder {
 	return b.intValue(sourceParam, dest, 8, true)
 }
@@ -416,7 +493,7 @@ func (b *ValueBinder) Int(sourceParam string, dest *int) *ValueBinder {
 	return b.intValue(sourceParam, dest, 0, false)
 }
 
-// MustInt requires parameter value to exist to be bind to int variable. Returns error when value does not exist
+// MustInt requires parameter value to exist to bind to int variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt(sourceParam string, dest *int) *ValueBinder {
 	return b.intValue(sourceParam, dest, 0, true)
 }
@@ -544,7 +621,7 @@ func (b *ValueBinder) Int64s(sourceParam string, dest *[]int64) *ValueBinder {
 	return b.intsValue(sourceParam, dest, false)
 }
 
-// MustInt64s requires parameter value to exist to be bind to int64 slice variable. Returns error when value does not exist
+// MustInt64s requires parameter value to exist to bind to int64 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt64s(sourceParam string, dest *[]int64) *ValueBinder {
 	return b.intsValue(sourceParam, dest, true)
 }
@@ -554,7 +631,7 @@ func (b *ValueBinder) Int32s(sourceParam string, dest *[]int32) *ValueBinder {
 	return b.intsValue(sourceParam, dest, false)
 }
 
-// MustInt32s requires parameter value to exist to be bind to int32 slice variable. Returns error when value does not exist
+// MustInt32s requires parameter value to exist to bind to int32 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt32s(sourceParam string, dest *[]int32) *ValueBinder {
 	return b.intsValue(sourceParam, dest, true)
 }
@@ -564,7 +641,7 @@ func (b *ValueBinder) Int16s(sourceParam string, dest *[]int16) *ValueBinder {
 	return b.intsValue(sourceParam, dest, false)
 }
 
-// MustInt16s requires parameter value to exist to be bind to int16 slice variable. Returns error when value does not exist
+// MustInt16s requires parameter value to exist to bind to int16 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt16s(sourceParam string, dest *[]int16) *ValueBinder {
 	return b.intsValue(sourceParam, dest, true)
 }
@@ -574,7 +651,7 @@ func (b *ValueBinder) Int8s(sourceParam string, dest *[]int8) *ValueBinder {
 	return b.intsValue(sourceParam, dest, false)
 }
 
-// MustInt8s requires parameter value to exist to be bind to int8 slice variable. Returns error when value does not exist
+// MustInt8s requires parameter value to exist to bind to int8 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustInt8s(sourceParam string, dest *[]int8) *ValueBinder {
 	return b.intsValue(sourceParam, dest, true)
 }
@@ -584,7 +661,7 @@ func (b *ValueBinder) Ints(sourceParam string, dest *[]int) *ValueBinder {
 	return b.intsValue(sourceParam, dest, false)
 }
 
-// MustInts requires parameter value to exist to be bind to int slice variable. Returns error when value does not exist
+// MustInts requires parameter value to exist to bind to int slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustInts(sourceParam string, dest *[]int) *ValueBinder {
 	return b.intsValue(sourceParam, dest, true)
 }
@@ -594,7 +671,7 @@ func (b *ValueBinder) Uint64(sourceParam string, dest *uint64) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 64, false)
 }
 
-// MustUint64 requires parameter value to exist to be bind to uint64 variable. Returns error when value does not exist
+// MustUint64 requires parameter value to exist to bind to uint64 variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint64(sourceParam string, dest *uint64) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 64, true)
 }
@@ -604,7 +681,7 @@ func (b *ValueBinder) Uint32(sourceParam string, dest *uint32) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 32, false)
 }
 
-// MustUint32 requires parameter value to exist to be bind to uint32 variable. Returns error when value does not exist
+// MustUint32 requires parameter value to exist to bind to uint32 variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint32(sourceParam string, dest *uint32) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 32, true)
 }
@@ -614,7 +691,7 @@ func (b *ValueBinder) Uint16(sourceParam string, dest *uint16) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 16, false)
 }
 
-// MustUint16 requires parameter value to exist to be bind to uint16 variable. Returns error when value does not exist
+// MustUint16 requires parameter value to exist to bind to uint16 variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint16(sourceParam string, dest *uint16) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 16, true)
 }
@@ -624,7 +701,7 @@ func (b *ValueBinder) Uint8(sourceParam string, dest *uint8) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 8, false)
 }
 
-// MustUint8 requires parameter value to exist to be bind to uint8 variable. Returns error when value does not exist
+// MustUint8 requires parameter value to exist to bind to uint8 variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint8(sourceParam string, dest *uint8) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 8, true)
 }
@@ -634,7 +711,7 @@ func (b *ValueBinder) Byte(sourceParam string, dest *byte) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 8, false)
 }
 
-// MustByte requires parameter value to exist to be bind to byte variable. Returns error when value does not exist
+// MustByte requires parameter value to exist to bind to byte variable. Returns error when value does not exist
 func (b *ValueBinder) MustByte(sourceParam string, dest *byte) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 8, true)
 }
@@ -644,7 +721,7 @@ func (b *ValueBinder) Uint(sourceParam string, dest *uint) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 0, false)
 }
 
-// MustUint requires parameter value to exist to be bind to uint variable. Returns error when value does not exist
+// MustUint requires parameter value to exist to bind to uint variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint(sourceParam string, dest *uint) *ValueBinder {
 	return b.uintValue(sourceParam, dest, 0, true)
 }
@@ -772,7 +849,7 @@ func (b *ValueBinder) Uint64s(sourceParam string, dest *[]uint64) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, false)
 }
 
-// MustUint64s requires parameter value to exist to be bind to uint64 slice variable. Returns error when value does not exist
+// MustUint64s requires parameter value to exist to bind to uint64 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint64s(sourceParam string, dest *[]uint64) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, true)
 }
@@ -782,7 +859,7 @@ func (b *ValueBinder) Uint32s(sourceParam string, dest *[]uint32) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, false)
 }
 
-// MustUint32s requires parameter value to exist to be bind to uint32 slice variable. Returns error when value does not exist
+// MustUint32s requires parameter value to exist to bind to uint32 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint32s(sourceParam string, dest *[]uint32) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, true)
 }
@@ -792,7 +869,7 @@ func (b *ValueBinder) Uint16s(sourceParam string, dest *[]uint16) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, false)
 }
 
-// MustUint16s requires parameter value to exist to be bind to uint16 slice variable. Returns error when value does not exist
+// MustUint16s requires parameter value to exist to bind to uint16 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint16s(sourceParam string, dest *[]uint16) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, true)
 }
@@ -802,7 +879,7 @@ func (b *ValueBinder) Uint8s(sourceParam string, dest *[]uint8) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, false)
 }
 
-// MustUint8s requires parameter value to exist to be bind to uint8 slice variable. Returns error when value does not exist
+// MustUint8s requires parameter value to exist to bind to uint8 slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustUint8s(sourceParam string, dest *[]uint8) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, true)
 }
@@ -812,7 +889,7 @@ func (b *ValueBinder) Uints(sourceParam string, dest *[]uint) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, false)
 }
 
-// MustUints requires parameter value to exist to be bind to uint slice variable. Returns error when value does not exist
+// MustUints requires parameter value to exist to bind to uint slice variable. Returns error when value does not exist
 func (b *ValueBinder) MustUints(sourceParam string, dest *[]uint) *ValueBinder {
 	return b.uintsValue(sourceParam, dest, true)
 }
@@ -822,7 +899,7 @@ func (b *ValueBinder) Bool(sourceParam string, dest *bool) *ValueBinder {
 	return b.boolValue(sourceParam, dest, false)
 }
 
-// MustBool requires parameter value to exist to be bind to bool variable. Returns error when value does not exist
+// MustBool requires parameter value to exist to bind to bool variable. Returns error when value does not exist
 func (b *ValueBinder) MustBool(sourceParam string, dest *bool) *ValueBinder {
 	return b.boolValue(sourceParam, dest, true)
 }
@@ -887,7 +964,7 @@ func (b *ValueBinder) Bools(sourceParam string, dest *[]bool) *ValueBinder {
 	return b.boolsValue(sourceParam, dest, false)
 }
 
-// MustBools requires parameter values to exist to be bind to slice of bool variables. Returns error when values does not exist
+// MustBools requires parameter values to exist to bind to slice of bool variables. Returns error when values does not exist
 func (b *ValueBinder) MustBools(sourceParam string, dest *[]bool) *ValueBinder {
 	return b.boolsValue(sourceParam, dest, true)
 }
@@ -897,7 +974,7 @@ func (b *ValueBinder) Float64(sourceParam string, dest *float64) *ValueBinder {
 	return b.floatValue(sourceParam, dest, 64, false)
 }
 
-// MustFloat64 requires parameter value to exist to be bind to float64 variable. Returns error when value does not exist
+// MustFloat64 requires parameter value to exist to bind to float64 variable. Returns error when value does not exist
 func (b *ValueBinder) MustFloat64(sourceParam string, dest *float64) *ValueBinder {
 	return b.floatValue(sourceParam, dest, 64, true)
 }
@@ -907,7 +984,7 @@ func (b *ValueBinder) Float32(sourceParam string, dest *float32) *ValueBinder {
 	return b.floatValue(sourceParam, dest, 32, false)
 }
 
-// MustFloat32 requires parameter value to exist to be bind to float32 variable. Returns error when value does not exist
+// MustFloat32 requires parameter value to exist to bind to float32 variable. Returns error when value does not exist
 func (b *ValueBinder) MustFloat32(sourceParam string, dest *float32) *ValueBinder {
 	return b.floatValue(sourceParam, dest, 32, true)
 }
@@ -992,7 +1069,7 @@ func (b *ValueBinder) Float64s(sourceParam string, dest *[]float64) *ValueBinder
 	return b.floatsValue(sourceParam, dest, false)
 }
 
-// MustFloat64s requires parameter values to exist to be bind to slice of float64 variables. Returns error when values does not exist
+// MustFloat64s requires parameter values to exist to bind to slice of float64 variables. Returns error when values does not exist
 func (b *ValueBinder) MustFloat64s(sourceParam string, dest *[]float64) *ValueBinder {
 	return b.floatsValue(sourceParam, dest, true)
 }
@@ -1002,7 +1079,7 @@ func (b *ValueBinder) Float32s(sourceParam string, dest *[]float32) *ValueBinder
 	return b.floatsValue(sourceParam, dest, false)
 }
 
-// MustFloat32s requires parameter values to exist to be bind to slice of float32 variables. Returns error when values does not exist
+// MustFloat32s requires parameter values to exist to bind to slice of float32 variables. Returns error when values does not exist
 func (b *ValueBinder) MustFloat32s(sourceParam string, dest *[]float32) *ValueBinder {
 	return b.floatsValue(sourceParam, dest, true)
 }
@@ -1012,7 +1089,7 @@ func (b *ValueBinder) Time(sourceParam string, dest *time.Time, layout string) *
 	return b.time(sourceParam, dest, layout, false)
 }
 
-// MustTime requires parameter value to exist to be bind to time.Time variable. Returns error when value does not exist
+// MustTime requires parameter value to exist to bind to time.Time variable. Returns error when value does not exist
 func (b *ValueBinder) MustTime(sourceParam string, dest *time.Time, layout string) *ValueBinder {
 	return b.time(sourceParam, dest, layout, true)
 }
@@ -1043,7 +1120,7 @@ func (b *ValueBinder) Times(sourceParam string, dest *[]time.Time, layout string
 	return b.times(sourceParam, dest, layout, false)
 }
 
-// MustTimes requires parameter values to exist to be bind to slice of time.Time variables. Returns error when values does not exist
+// MustTimes requires parameter values to exist to bind to slice of time.Time variables. Returns error when values does not exist
 func (b *ValueBinder) MustTimes(sourceParam string, dest *[]time.Time, layout string) *ValueBinder {
 	return b.times(sourceParam, dest, layout, true)
 }
@@ -1084,7 +1161,7 @@ func (b *ValueBinder) Duration(sourceParam string, dest *time.Duration) *ValueBi
 	return b.duration(sourceParam, dest, false)
 }
 
-// MustDuration requires parameter value to exist to be bind to time.Duration variable. Returns error when value does not exist
+// MustDuration requires parameter value to exist to bind to time.Duration variable. Returns error when value does not exist
 func (b *ValueBinder) MustDuration(sourceParam string, dest *time.Duration) *ValueBinder {
 	return b.duration(sourceParam, dest, true)
 }
@@ -1115,7 +1192,7 @@ func (b *ValueBinder) Durations(sourceParam string, dest *[]time.Duration) *Valu
 	return b.durationsValue(sourceParam, dest, false)
 }
 
-// MustDurations requires parameter values to exist to be bind to slice of time.Duration variables. Returns error when values does not exist
+// MustDurations requires parameter values to exist to bind to slice of time.Duration variables. Returns error when values does not exist
 func (b *ValueBinder) MustDurations(sourceParam string, dest *[]time.Duration) *ValueBinder {
 	return b.durationsValue(sourceParam, dest, true)
 }
@@ -1161,10 +1238,10 @@ func (b *ValueBinder) durations(sourceParam string, values []string, dest *[]tim
 // Note:
 //  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
 func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder {
-	return b.unixTime(sourceParam, dest, false, false)
+	return b.unixTime(sourceParam, dest, false, time.Second)
 }
 
-// MustUnixTime requires parameter value to exist to be bind to time.Duration variable  (in local Time corresponding
+// MustUnixTime requires parameter value to exist to bind to time.Duration variable (in local time corresponding
 // to the given Unix time). Returns error when value does not exist.
 //
 // Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00
@@ -1172,10 +1249,31 @@ func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder
 // Note:
 //  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
 func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder {
-	return b.unixTime(sourceParam, dest, true, false)
+	return b.unixTime(sourceParam, dest, true, time.Second)
+}
+
+// UnixTimeMilli binds parameter to time.Time variable (in local time corresponding to the given Unix time in millisecond precision).
+//
+// Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00
+//
+// Note:
+//  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
+func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {
+	return b.unixTime(sourceParam, dest, false, time.Millisecond)
 }
 
-// UnixTimeNano binds parameter to time.Time variable (in local Time corresponding to the given Unix time in nano second precision).
+// MustUnixTimeMilli requires parameter value to exist to bind to time.Duration variable  (in local time corresponding
+// to the given Unix time in millisecond precision). Returns error when value does not exist.
+//
+// Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00
+//
+// Note:
+//  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
+func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {
+	return b.unixTime(sourceParam, dest, true, time.Millisecond)
+}
+
+// UnixTimeNano binds parameter to time.Time variable (in local time corresponding to the given Unix time in nanosecond precision).
 //
 // Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00
 // Example:          1000000000 binds to 1970-01-01T00:00:01.000000000+00:00
@@ -1185,10 +1283,10 @@ func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBi
 //  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
 //  * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.
 func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {
-	return b.unixTime(sourceParam, dest, false, true)
+	return b.unixTime(sourceParam, dest, false, time.Nanosecond)
 }
 
-// MustUnixTimeNano requires parameter value to exist to be bind to time.Duration variable  (in local Time corresponding
+// MustUnixTimeNano requires parameter value to exist to bind to time.Duration variable  (in local Time corresponding
 // to the given Unix time value in nano second precision). Returns error when value does not exist.
 //
 // Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00
@@ -1199,10 +1297,10 @@ func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBi
 //  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
 //  * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.
 func (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {
-	return b.unixTime(sourceParam, dest, true, true)
+	return b.unixTime(sourceParam, dest, true, time.Nanosecond)
 }
 
-func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, isNano bool) *ValueBinder {
+func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, precision time.Duration) *ValueBinder {
 	if b.failFast && b.errors != nil {
 		return b
 	}
@@ -1221,10 +1319,13 @@ func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExi
 		return b
 	}
 
-	if isNano {
-		*dest = time.Unix(0, n)
-	} else {
+	switch precision {
+	case time.Second:
 		*dest = time.Unix(n, 0)
+	case time.Millisecond:
+		*dest = time.Unix(n/1e3, (n%1e3)*1e6) // TODO: time.UnixMilli(n) exists since Go1.17 switch to that when min version allows
+	case time.Nanosecond:
+		*dest = time.Unix(0, n)
 	}
 	return b
 }
diff --git a/vendor/github.com/labstack/echo/v4/echo.go b/vendor/github.com/labstack/echo/v4/echo.go
index 8829619c7..291c4047e 100644
--- a/vendor/github.com/labstack/echo/v4/echo.go
+++ b/vendor/github.com/labstack/echo/v4/echo.go
@@ -183,6 +183,8 @@ const (
 	PROPFIND = "PROPFIND"
 	// REPORT Method can be used to get information about a resource, see rfc 3253
 	REPORT = "REPORT"
+	// RouteNotFound is special method type for routes handling "route not found" (404) cases
+	RouteNotFound = "echo_route_not_found"
 )
 
 // Headers
@@ -246,7 +248,7 @@ const (
 
 const (
 	// Version of Echo
-	Version = "4.7.2"
+	Version = "4.8.0"
 	website = "https://echo.labstack.com"
 	// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
 	banner = `
@@ -480,8 +482,21 @@ func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
 	return e.Add(http.MethodTrace, path, h, m...)
 }
 
-// Any registers a new route for all HTTP methods and path with matching handler
+// RouteNotFound registers a special-case route which is executed when no other route is found (i.e. HTTP 404 cases)
+// for current request URL.
+// Path supports static and named/any parameters just like other http method is defined. Generally path is ended with
+// wildcard/match-any character (`/*`, `/download/*` etc).
+//
+// Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })`
+func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
+	return e.Add(RouteNotFound, path, h, m...)
+}
+
+// Any registers a new route for all HTTP methods (supported by Echo) and path with matching handler
 // in the router with optional route-level middleware.
+//
+// Note: this method only adds specific set of supported HTTP methods as handler and is not true
+// "catch-any-arbitrary-method" way of matching requests.
 func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
 	routes := make([]*Route, len(methods))
 	for i, m := range methods {
@@ -515,6 +530,7 @@ func (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route {
 func (e *Echo) add(host, method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
 	name := handlerName(handler)
 	router := e.findRouter(host)
+	// FIXME: when handler+middleware are both nil ... make it behave like handler removal
 	router.Add(method, path, func(c Context) error {
 		h := applyMiddleware(handler, middleware...)
 		return h(c)
diff --git a/vendor/github.com/labstack/echo/v4/group.go b/vendor/github.com/labstack/echo/v4/group.go
index bba470ce8..28ce0dd9a 100644
--- a/vendor/github.com/labstack/echo/v4/group.go
+++ b/vendor/github.com/labstack/echo/v4/group.go
@@ -107,6 +107,13 @@ func (g *Group) File(path, file string) {
 	g.file(path, file, g.GET)
 }
 
+// RouteNotFound implements `Echo#RouteNotFound()` for sub-routes within the Group.
+//
+// Example: `g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })`
+func (g *Group) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
+	return g.Add(RouteNotFound, path, h, m...)
+}
+
 // Add implements `Echo#Add()` for sub-routes within the Group.
 func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
 	// Combine into a new slice to avoid accidentally passing the same slice for
diff --git a/vendor/github.com/labstack/echo/v4/middleware/basic_auth.go b/vendor/github.com/labstack/echo/v4/middleware/basic_auth.go
index 8cf1ed9fc..52ef1042f 100644
--- a/vendor/github.com/labstack/echo/v4/middleware/basic_auth.go
+++ b/vendor/github.com/labstack/echo/v4/middleware/basic_auth.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"strconv"
 	"strings"
+	"net/http"
 
 	"github.com/labstack/echo/v4"
 )
@@ -74,10 +75,13 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
 			l := len(basic)
 
 			if len(auth) > l+1 && strings.EqualFold(auth[:l], basic) {
+				// Invalid base64 shouldn't be treated as error
+				// instead should be treated as invalid client input
 				b, err := base64.StdEncoding.DecodeString(auth[l+1:])
 				if err != nil {
-					return err
+					return echo.NewHTTPError(http.StatusBadRequest).SetInternal(err)
 				}
+
 				cred := string(b)
 				for i := 0; i < len(cred); i++ {
 					if cred[i] == ':' {
diff --git a/vendor/github.com/labstack/echo/v4/middleware/logger.go b/vendor/github.com/labstack/echo/v4/middleware/logger.go
index 9baac4769..a21df8f39 100644
--- a/vendor/github.com/labstack/echo/v4/middleware/logger.go
+++ b/vendor/github.com/labstack/echo/v4/middleware/logger.go
@@ -23,6 +23,8 @@ type (
 		// Tags to construct the logger format.
 		//
 		// - time_unix
+		// - time_unix_milli
+		// - time_unix_micro
 		// - time_unix_nano
 		// - time_rfc3339
 		// - time_rfc3339_nano
@@ -126,6 +128,12 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
 				switch tag {
 				case "time_unix":
 					return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
+				case "time_unix_milli":
+					// go 1.17 or later, it supports time#UnixMilli()
+					return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000000, 10))
+				case "time_unix_micro":
+					// go 1.17 or later, it supports time#UnixMicro()
+					return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000, 10))
 				case "time_unix_nano":
 					return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10))
 				case "time_rfc3339":
diff --git a/vendor/github.com/labstack/echo/v4/middleware/request_logger.go b/vendor/github.com/labstack/echo/v4/middleware/request_logger.go
index 1b3e3eaad..7a4d9822e 100644
--- a/vendor/github.com/labstack/echo/v4/middleware/request_logger.go
+++ b/vendor/github.com/labstack/echo/v4/middleware/request_logger.go
@@ -2,9 +2,10 @@ package middleware
 
 import (
 	"errors"
-	"github.com/labstack/echo/v4"
 	"net/http"
 	"time"
+
+	"github.com/labstack/echo/v4"
 )
 
 // Example for `fmt.Printf`
@@ -264,7 +265,8 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 			if config.LogStatus {
 				v.Status = res.Status
 				if err != nil {
-					if httpErr, ok := err.(*echo.HTTPError); ok {
+					var httpErr *echo.HTTPError
+					if errors.As(err, &httpErr) {
 						v.Status = httpErr.Code
 					}
 				}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/static.go b/vendor/github.com/labstack/echo/v4/middleware/static.go
index 0106f7ce2..27ccf4117 100644
--- a/vendor/github.com/labstack/echo/v4/middleware/static.go
+++ b/vendor/github.com/labstack/echo/v4/middleware/static.go
@@ -1,6 +1,7 @@
 package middleware
 
 import (
+	"errors"
 	"fmt"
 	"html/template"
 	"net/http"
@@ -196,8 +197,8 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
 					return err
 				}
 
-				he, ok := err.(*echo.HTTPError)
-				if !(ok && config.HTML5 && he.Code == http.StatusNotFound) {
+				var he *echo.HTTPError
+				if !(errors.As(err, &he) && config.HTML5 && he.Code == http.StatusNotFound) {
 					return err
 				}
 
diff --git a/vendor/github.com/labstack/echo/v4/router.go b/vendor/github.com/labstack/echo/v4/router.go
index a1de2d6e3..23c5bd3ba 100644
--- a/vendor/github.com/labstack/echo/v4/router.go
+++ b/vendor/github.com/labstack/echo/v4/router.go
@@ -19,30 +19,39 @@ type (
 		prefix         string
 		parent         *node
 		staticChildren children
-		ppath          string
-		pnames         []string
-		methodHandler  *methodHandler
+		originalPath   string
+		methods        *routeMethods
 		paramChild     *node
 		anyChild       *node
+		paramsCount    int
 		// isLeaf indicates that node does not have child routes
 		isLeaf bool
 		// isHandler indicates that node has at least one handler registered to it
 		isHandler bool
-	}
-	kind          uint8
-	children      []*node
-	methodHandler struct {
-		connect     HandlerFunc
-		delete      HandlerFunc
-		get         HandlerFunc
-		head        HandlerFunc
-		options     HandlerFunc
-		patch       HandlerFunc
-		post        HandlerFunc
-		propfind    HandlerFunc
-		put         HandlerFunc
-		trace       HandlerFunc
-		report      HandlerFunc
+
+		// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
+		notFoundHandler *routeMethod
+	}
+	kind        uint8
+	children    []*node
+	routeMethod struct {
+		ppath   string
+		pnames  []string
+		handler HandlerFunc
+	}
+	routeMethods struct {
+		connect     *routeMethod
+		delete      *routeMethod
+		get         *routeMethod
+		head        *routeMethod
+		options     *routeMethod
+		patch       *routeMethod
+		post        *routeMethod
+		propfind    *routeMethod
+		put         *routeMethod
+		trace       *routeMethod
+		report      *routeMethod
+		anyOther    map[string]*routeMethod
 		allowHeader string
 	}
 )
@@ -56,7 +65,7 @@ const (
 	anyLabel   = byte('*')
 )
 
-func (m *methodHandler) isHandler() bool {
+func (m *routeMethods) isHandler() bool {
 	return m.connect != nil ||
 		m.delete != nil ||
 		m.get != nil ||
@@ -67,10 +76,12 @@ func (m *methodHandler) isHandler() bool {
 		m.propfind != nil ||
 		m.put != nil ||
 		m.trace != nil ||
-		m.report != nil
+		m.report != nil ||
+		len(m.anyOther) != 0
+	// RouteNotFound/404 is not considered as a handler
 }
 
-func (m *methodHandler) updateAllowHeader() {
+func (m *routeMethods) updateAllowHeader() {
 	buf := new(bytes.Buffer)
 	buf.WriteString(http.MethodOptions)
 
@@ -112,6 +123,10 @@ func (m *methodHandler) updateAllowHeader() {
 	if m.report != nil {
 		buf.WriteString(", REPORT")
 	}
+	for method := range m.anyOther { // for simplicity, we use map and therefore order is not deterministic here
+		buf.WriteString(", ")
+		buf.WriteString(method)
+	}
 	m.allowHeader = buf.String()
 }
 
@@ -119,7 +134,7 @@ func (m *methodHandler) updateAllowHeader() {
 func NewRouter(e *Echo) *Router {
 	return &Router{
 		tree: &node{
-			methodHandler: new(methodHandler),
+			methods: new(routeMethods),
 		},
 		routes: map[string]*Route{},
 		echo:   e,
@@ -153,7 +168,7 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 			}
 			j := i + 1
 
-			r.insert(method, path[:i], nil, staticKind, "", nil)
+			r.insert(method, path[:i], staticKind, routeMethod{})
 			for ; i < lcpIndex && path[i] != '/'; i++ {
 			}
 
@@ -163,23 +178,23 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 
 			if i == lcpIndex {
 				// path node is last fragment of route path. ie. `/users/:id`
-				r.insert(method, path[:i], h, paramKind, ppath, pnames)
+				r.insert(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
 			} else {
-				r.insert(method, path[:i], nil, paramKind, "", nil)
+				r.insert(method, path[:i], paramKind, routeMethod{})
 			}
 		} else if path[i] == '*' {
-			r.insert(method, path[:i], nil, staticKind, "", nil)
+			r.insert(method, path[:i], staticKind, routeMethod{})
 			pnames = append(pnames, "*")
-			r.insert(method, path[:i+1], h, anyKind, ppath, pnames)
+			r.insert(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
 		}
 	}
 
-	r.insert(method, path, h, staticKind, ppath, pnames)
+	r.insert(method, path, staticKind, routeMethod{ppath, pnames, h})
 }
 
-func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string, pnames []string) {
+func (r *Router) insert(method, path string, t kind, rm routeMethod) {
 	// Adjust max param
-	paramLen := len(pnames)
+	paramLen := len(rm.pnames)
 	if *r.echo.maxParam < paramLen {
 		*r.echo.maxParam = paramLen
 	}
@@ -207,25 +222,31 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 			// At root node
 			currentNode.label = search[0]
 			currentNode.prefix = search
-			if h != nil {
+			if rm.handler != nil {
 				currentNode.kind = t
-				currentNode.addHandler(method, h)
-				currentNode.ppath = ppath
-				currentNode.pnames = pnames
+				currentNode.addMethod(method, &rm)
+				currentNode.paramsCount = len(rm.pnames)
+				currentNode.originalPath = rm.ppath
 			}
 			currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
 		} else if lcpLen < prefixLen {
-			// Split node
+			// Split node into two before we insert new node.
+			// This happens when we are inserting path that is submatch of any existing inserted paths.
+			// For example, we have node `/test` and now are about to insert `/te/*`. In that case
+			// 1. overlapping part is `/te` that is used as parent node
+			// 2. `st` is part from existing node that is not matching - it gets its own node (child to `/te`)
+			// 3. `/*` is the new part we are about to insert (child to `/te`)
 			n := newNode(
 				currentNode.kind,
 				currentNode.prefix[lcpLen:],
 				currentNode,
 				currentNode.staticChildren,
-				currentNode.methodHandler,
-				currentNode.ppath,
-				currentNode.pnames,
+				currentNode.originalPath,
+				currentNode.methods,
+				currentNode.paramsCount,
 				currentNode.paramChild,
 				currentNode.anyChild,
+				currentNode.notFoundHandler,
 			)
 			// Update parent path for all children to new node
 			for _, child := range currentNode.staticChildren {
@@ -243,13 +264,14 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 			currentNode.label = currentNode.prefix[0]
 			currentNode.prefix = currentNode.prefix[:lcpLen]
 			currentNode.staticChildren = nil
-			currentNode.methodHandler = new(methodHandler)
-			currentNode.ppath = ""
-			currentNode.pnames = nil
+			currentNode.originalPath = ""
+			currentNode.methods = new(routeMethods)
+			currentNode.paramsCount = 0
 			currentNode.paramChild = nil
 			currentNode.anyChild = nil
 			currentNode.isLeaf = false
 			currentNode.isHandler = false
+			currentNode.notFoundHandler = nil
 
 			// Only Static children could reach here
 			currentNode.addStaticChild(n)
@@ -257,13 +279,19 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 			if lcpLen == searchLen {
 				// At parent node
 				currentNode.kind = t
-				currentNode.addHandler(method, h)
-				currentNode.ppath = ppath
-				currentNode.pnames = pnames
+				if rm.handler != nil {
+					currentNode.addMethod(method, &rm)
+					currentNode.paramsCount = len(rm.pnames)
+					currentNode.originalPath = rm.ppath
+				}
 			} else {
 				// Create child node
-				n = newNode(t, search[lcpLen:], currentNode, nil, new(methodHandler), ppath, pnames, nil, nil)
-				n.addHandler(method, h)
+				n = newNode(t, search[lcpLen:], currentNode, nil, "", new(routeMethods), 0, nil, nil, nil)
+				if rm.handler != nil {
+					n.addMethod(method, &rm)
+					n.paramsCount = len(rm.pnames)
+					n.originalPath = rm.ppath
+				}
 				// Only Static children could reach here
 				currentNode.addStaticChild(n)
 			}
@@ -277,8 +305,12 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 				continue
 			}
 			// Create child node
-			n := newNode(t, search, currentNode, nil, new(methodHandler), ppath, pnames, nil, nil)
-			n.addHandler(method, h)
+			n := newNode(t, search, currentNode, nil, rm.ppath, new(routeMethods), 0, nil, nil, nil)
+			if rm.handler != nil {
+				n.addMethod(method, &rm)
+				n.paramsCount = len(rm.pnames)
+			}
+
 			switch t {
 			case staticKind:
 				currentNode.addStaticChild(n)
@@ -290,32 +322,42 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 			currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
 		} else {
 			// Node already exists
-			if h != nil {
-				currentNode.addHandler(method, h)
-				currentNode.ppath = ppath
-				if len(currentNode.pnames) == 0 { // Issue #729
-					currentNode.pnames = pnames
-				}
+			if rm.handler != nil {
+				currentNode.addMethod(method, &rm)
+				currentNode.paramsCount = len(rm.pnames)
+				currentNode.originalPath = rm.ppath
 			}
 		}
 		return
 	}
 }
 
-func newNode(t kind, pre string, p *node, sc children, mh *methodHandler, ppath string, pnames []string, paramChildren, anyChildren *node) *node {
+func newNode(
+	t kind,
+	pre string,
+	p *node,
+	sc children,
+	originalPath string,
+	methods *routeMethods,
+	paramsCount int,
+	paramChildren,
+	anyChildren *node,
+	notFoundHandler *routeMethod,
+) *node {
 	return &node{
-		kind:           t,
-		label:          pre[0],
-		prefix:         pre,
-		parent:         p,
-		staticChildren: sc,
-		ppath:          ppath,
-		pnames:         pnames,
-		methodHandler:  mh,
-		paramChild:     paramChildren,
-		anyChild:       anyChildren,
-		isLeaf:         sc == nil && paramChildren == nil && anyChildren == nil,
-		isHandler:      mh.isHandler(),
+		kind:            t,
+		label:           pre[0],
+		prefix:          pre,
+		parent:          p,
+		staticChildren:  sc,
+		originalPath:    originalPath,
+		methods:         methods,
+		paramsCount:     paramsCount,
+		paramChild:      paramChildren,
+		anyChild:        anyChildren,
+		isLeaf:          sc == nil && paramChildren == nil && anyChildren == nil,
+		isHandler:       methods.isHandler(),
+		notFoundHandler: notFoundHandler,
 	}
 }
 
@@ -333,10 +375,8 @@ func (n *node) findStaticChild(l byte) *node {
 }
 
 func (n *node) findChildWithLabel(l byte) *node {
-	for _, c := range n.staticChildren {
-		if c.label == l {
-			return c
-		}
+	if c := n.findStaticChild(l); c != nil {
+		return c
 	}
 	if l == paramLabel {
 		return n.paramChild
@@ -347,66 +387,74 @@ func (n *node) findChildWithLabel(l byte) *node {
 	return nil
 }
 
-func (n *node) addHandler(method string, h HandlerFunc) {
+func (n *node) addMethod(method string, h *routeMethod) {
 	switch method {
 	case http.MethodConnect:
-		n.methodHandler.connect = h
+		n.methods.connect = h
 	case http.MethodDelete:
-		n.methodHandler.delete = h
+		n.methods.delete = h
 	case http.MethodGet:
-		n.methodHandler.get = h
+		n.methods.get = h
 	case http.MethodHead:
-		n.methodHandler.head = h
+		n.methods.head = h
 	case http.MethodOptions:
-		n.methodHandler.options = h
+		n.methods.options = h
 	case http.MethodPatch:
-		n.methodHandler.patch = h
+		n.methods.patch = h
 	case http.MethodPost:
-		n.methodHandler.post = h
+		n.methods.post = h
 	case PROPFIND:
-		n.methodHandler.propfind = h
+		n.methods.propfind = h
 	case http.MethodPut:
-		n.methodHandler.put = h
+		n.methods.put = h
 	case http.MethodTrace:
-		n.methodHandler.trace = h
+		n.methods.trace = h
 	case REPORT:
-		n.methodHandler.report = h
+		n.methods.report = h
+	case RouteNotFound:
+		n.notFoundHandler = h
+		return // RouteNotFound/404 is not considered as a handler so no further logic needs to be executed
+	default:
+		if n.methods.anyOther == nil {
+			n.methods.anyOther = make(map[string]*routeMethod)
+		}
+		if h.handler == nil {
+			delete(n.methods.anyOther, method)
+		} else {
+			n.methods.anyOther[method] = h
+		}
 	}
 
-	n.methodHandler.updateAllowHeader()
-	if h != nil {
-		n.isHandler = true
-	} else {
-		n.isHandler = n.methodHandler.isHandler()
-	}
+	n.methods.updateAllowHeader()
+	n.isHandler = true
 }
 
-func (n *node) findHandler(method string) HandlerFunc {
+func (n *node) findMethod(method string) *routeMethod {
 	switch method {
 	case http.MethodConnect:
-		return n.methodHandler.connect
+		return n.methods.connect
 	case http.MethodDelete:
-		return n.methodHandler.delete
+		return n.methods.delete
 	case http.MethodGet:
-		return n.methodHandler.get
+		return n.methods.get
 	case http.MethodHead:
-		return n.methodHandler.head
+		return n.methods.head
 	case http.MethodOptions:
-		return n.methodHandler.options
+		return n.methods.options
 	case http.MethodPatch:
-		return n.methodHandler.patch
+		return n.methods.patch
 	case http.MethodPost:
-		return n.methodHandler.post
+		return n.methods.post
 	case PROPFIND:
-		return n.methodHandler.propfind
+		return n.methods.propfind
 	case http.MethodPut:
-		return n.methodHandler.put
+		return n.methods.put
 	case http.MethodTrace:
-		return n.methodHandler.trace
+		return n.methods.trace
 	case REPORT:
-		return n.methodHandler.report
-	default:
-		return nil
+		return n.methods.report
+	default: // RouteNotFound/404 is not considered as a handler
+		return n.methods.anyOther[method]
 	}
 }
 
@@ -435,7 +483,7 @@ func (r *Router) Find(method, path string, c Context) {
 
 	var (
 		previousBestMatchNode *node
-		matchedHandler        HandlerFunc
+		matchedRouteMethod    *routeMethod
 		// search stores the remaining path to check for match. By each iteration we move from start of path to end of the path
 		// and search value gets shorter and shorter.
 		search      = path
@@ -508,7 +556,7 @@ func (r *Router) Find(method, path string, c Context) {
 			// No matching prefix, let's backtrack to the first possible alternative node of the decision path
 			nk, ok := backtrackToNextNodeKind(staticKind)
 			if !ok {
-				return // No other possibilities on the decision path
+				return // No other possibilities on the decision path, handler will be whatever context is reset to.
 			} else if nk == paramKind {
 				goto Param
 				// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
@@ -524,15 +572,21 @@ func (r *Router) Find(method, path string, c Context) {
 		search = search[lcpLen:]
 		searchIndex = searchIndex + lcpLen
 
-		// Finish routing if no remaining search and we are on a node with handler and matching method type
-		if search == "" && currentNode.isHandler {
-			// check if current node has handler registered for http method we are looking for. we store currentNode as
-			// best matching in case we do no find no more routes matching this path+method
-			if previousBestMatchNode == nil {
-				previousBestMatchNode = currentNode
-			}
-			if h := currentNode.findHandler(method); h != nil {
-				matchedHandler = h
+		// Finish routing if is no request path remaining to search
+		if search == "" {
+			// in case of node that is handler we have exact method type match or something for 405 to use
+			if currentNode.isHandler {
+				// check if current node has handler registered for http method we are looking for. we store currentNode as
+				// best matching in case we do no find no more routes matching this path+method
+				if previousBestMatchNode == nil {
+					previousBestMatchNode = currentNode
+				}
+				if h := currentNode.findMethod(method); h != nil {
+					matchedRouteMethod = h
+					break
+				}
+			} else if currentNode.notFoundHandler != nil {
+				matchedRouteMethod = currentNode.notFoundHandler
 				break
 			}
 		}
@@ -552,7 +606,8 @@ func (r *Router) Find(method, path string, c Context) {
 			i := 0
 			l := len(search)
 			if currentNode.isLeaf {
-				// when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
+				// when param node does not have any children (path param is last piece of route path) then param node should
+				// act similarly to any node - consider all remaining search as match
 				i = l
 			} else {
 				for ; i < l && search[i] != '/'; i++ {
@@ -571,19 +626,23 @@ func (r *Router) Find(method, path string, c Context) {
 		if child := currentNode.anyChild; child != nil {
 			// If any node is found, use remaining path for paramValues
 			currentNode = child
-			paramValues[len(currentNode.pnames)-1] = search
+			paramValues[currentNode.paramsCount-1] = search
+
 			// update indexes/search in case we need to backtrack when no handler match is found
 			paramIndex++
 			searchIndex += +len(search)
 			search = ""
 
-			// check if current node has handler registered for http method we are looking for. we store currentNode as
-			// best matching in case we do no find no more routes matching this path+method
+			if h := currentNode.findMethod(method); h != nil {
+				matchedRouteMethod = h
+				break
+			}
+			// we store currentNode as best matching in case we do not find more routes matching this path+method. Needed for 405
 			if previousBestMatchNode == nil {
 				previousBestMatchNode = currentNode
 			}
-			if h := currentNode.findHandler(method); h != nil {
-				matchedHandler = h
+			if currentNode.notFoundHandler != nil {
+				matchedRouteMethod = currentNode.notFoundHandler
 				break
 			}
 		}
@@ -606,22 +665,34 @@ func (r *Router) Find(method, path string, c Context) {
 		return // nothing matched at all
 	}
 
-	if matchedHandler != nil {
-		ctx.handler = matchedHandler
+	// matchedHandler could be method+path handler that we matched or notFoundHandler from node with matching path
+	// user provided not found (404) handler has priority over generic method not found (405) handler or global 404 handler
+	var rPath string
+	var rPNames []string
+	if matchedRouteMethod != nil {
+		rPath = matchedRouteMethod.ppath
+		rPNames = matchedRouteMethod.pnames
+		ctx.handler = matchedRouteMethod.handler
 	} else {
 		// use previous match as basis. although we have no matching handler we have path match.
 		// so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404)
 		currentNode = previousBestMatchNode
 
+		rPath = currentNode.originalPath
+		rPNames = nil // no params here
 		ctx.handler = NotFoundHandler
-		if currentNode.isHandler {
-			ctx.Set(ContextKeyHeaderAllow, currentNode.methodHandler.allowHeader)
+		if currentNode.notFoundHandler != nil {
+			rPath = currentNode.notFoundHandler.ppath
+			rPNames = currentNode.notFoundHandler.pnames
+			ctx.handler = currentNode.notFoundHandler.handler
+		} else if currentNode.isHandler {
+			ctx.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
 			ctx.handler = MethodNotAllowedHandler
 			if method == http.MethodOptions {
-				ctx.handler = optionsMethodHandler(currentNode.methodHandler.allowHeader)
+				ctx.handler = optionsMethodHandler(currentNode.methods.allowHeader)
 			}
 		}
 	}
-	ctx.path = currentNode.ppath
-	ctx.pnames = currentNode.pnames
+	ctx.path = rPath
+	ctx.pnames = rPNames
 }
diff --git a/vendor/modules.txt b/vendor/modules.txt
index c2a97499a..658175cc0 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -408,7 +408,7 @@ github.com/klauspost/pgzip
 github.com/kolo/xmlrpc
 # github.com/kr/text v0.2.0
 github.com/kr/text
-# github.com/labstack/echo/v4 v4.7.2
+# github.com/labstack/echo/v4 v4.8.0
 ## explicit
 github.com/labstack/echo/v4
 github.com/labstack/echo/v4/middleware


Test patch:
diff --git a/cmd/osbuild-service-maintenance/db_test.go b/cmd/osbuild-service-maintenance/db_test.go
index 762d4d7d4..e1861111c 100644
--- a/cmd/osbuild-service-maintenance/db_test.go
+++ b/cmd/osbuild-service-maintenance/db_test.go
@@ -58,7 +58,7 @@ func testDeleteJob(t *testing.T, d db, q *dbjobqueue.DBJobQueue) {
 	err = q.FinishJob(id, res)
 	require.NoError(t, err)
 
-	_, _, r, _, _, _, _, _, err := q.JobStatus(id)
+	_, _, r, _, _, _, _, _, _, err := q.JobStatus(id)
 	require.NoError(t, err)
 
 	var r1 Result
@@ -78,7 +78,7 @@ func testDeleteJob(t *testing.T, d db, q *dbjobqueue.DBJobQueue) {
 	require.NoError(t, err)
 	require.Equal(t, int64(1), rows)
 
-	_, _, _, _, _, _, _, _, err = q.JobStatus(id)
+	_, _, _, _, _, _, _, _, _, err = q.JobStatus(id)
 	require.Error(t, err)
 }
 
diff --git a/internal/cloudapi/v2/v2_koji_test.go b/internal/cloudapi/v2/v2_koji_test.go
index b63e36010..fb38ba6b8 100644
--- a/internal/cloudapi/v2/v2_koji_test.go
+++ b/internal/cloudapi/v2/v2_koji_test.go
@@ -539,7 +539,7 @@ func TestKojiJobTypeValidation(t *testing.T) {
 		test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", finalizeID, path), ``, http.StatusOK, "*")
 
 		// The other IDs should fail
-		test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", initID, path), ``, http.StatusNotFound, `{"code":"IMAGE-BUILDER-COMPOSER-26", "details": "", "href":"/api/image-builder-composer/v2/errors/26","id":"26","kind":"Error","reason":"Requested job has invalid type"}`, `operation_id`)
+		test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", initID, path), ``, http.StatusNotFound, `{"code":"IMAGE-BUILDER-COMPOSER-26", "details": "", "href":"/api/image-builder-composer/v2/errors/26","id":"26","kind":"Error","reason":"Job with given id has an invalid type"}`, `operation_id`)
 
 		for _, buildID := range buildJobIDs {
 			test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", buildID, path), ``, http.StatusOK, "*")
diff --git a/internal/cloudapi/v2/v2_test.go b/internal/cloudapi/v2/v2_test.go
index 4b4b2e37a..75e2c61c3 100644
--- a/internal/cloudapi/v2/v2_test.go
+++ b/internal/cloudapi/v2/v2_test.go
@@ -951,7 +951,7 @@ func TestComposeTargetErrors(t *testing.T) {
 					"ami": "",
 					"region": ""
 				},
-				"status": "",
+				"status": "failure",
 				"type": "aws"
 			}
 		},
@@ -1198,3 +1198,132 @@ func TestImageTypes(t *testing.T) {
 		"kind": "ComposeId"
 	}`, "id")
 }
+
+func TestImageFromCompose(t *testing.T) {
+	srv, wrksrv, _, cancel := newV2Server(t, t.TempDir(), []string{""}, false, false)
+	defer cancel()
+
+	test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST", "/api/image-builder-composer/v2/compose", fmt.Sprintf(`
+	{
+		"distribution": "%s",
+		"image_request":{
+			"architecture": "%s",
+			"image_type": "aws",
+			"repositories": [{
+				"baseurl": "somerepo.org",
+				"rhsm": false
+			}],
+			"upload_options": {
+				"region": "eu-central-1"
+			}
+		 }
+	}`, test_distro.TestDistroName, test_distro.TestArch3Name), http.StatusCreated, `
+	{
+		"href": "/api/image-builder-composer/v2/compose",
+		"kind": "ComposeId"
+	}`, "id")
+
+	jobId, token, jobType, _, _, err := wrksrv.RequestJob(context.Background(), test_distro.TestArch3Name, []string{worker.JobTypeOSBuild}, []string{""})
+	require.NoError(t, err)
+	require.Equal(t, worker.JobTypeOSBuild, jobType)
+
+	test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
+	{
+		"href": "/api/image-builder-composer/v2/composes/%v",
+		"kind": "ComposeStatus",
+		"id": "%v",
+		"image_status": {"status": "building"},
+		"status": "pending"
+	}`, jobId, jobId))
+
+	tr := target.NewAWSTargetResult(&target.AWSTargetResultOptions{
+		Ami:    "ami-abc123",
+		Region: "eu-central-1",
+	})
+	res, err := json.Marshal(&worker.OSBuildJobResult{
+		Success:       true,
+		OSBuildOutput: &osbuild.Result{Success: true},
+		TargetResults: []*target.TargetResult{
+			tr,
+		},
+	})
+	require.NoError(t, err)
+
+	err = wrksrv.FinishJob(token, res)
+	require.NoError(t, err)
+	test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
+	{
+		"href": "/api/image-builder-composer/v2/composes/%v",
+		"kind": "ComposeStatus",
+		"id": "%v",
+		"status": "success",
+		"image_status": {
+			"status": "success",
+			"upload_status": {
+				"type": "aws",
+				"status": "success",
+				"options": {
+					"ami": "ami-abc123",
+					"region": "eu-central-1"
+				}
+			}
+		}
+	}`, jobId, jobId))
+
+	test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST",
+		fmt.Sprintf("/api/image-builder-composer/v2/composes/%v/clone", jobId), `
+	{
+		"region": "eu-central-2",
+                "share_with_accounts": ["123456789012"]
+	}`, http.StatusCreated, fmt.Sprintf(`
+	{
+		"href": "/api/image-builder-composer/v2/composes/%v/clone",
+		"kind": "CloneComposeId"
+	}`, jobId), "id")
+
+	_, token, jobType, _, _, err = wrksrv.RequestJob(context.Background(), test_distro.TestArch3Name, []string{worker.JobTypeAWSEC2Copy}, []string{""})
+	require.NoError(t, err)
+	require.Equal(t, worker.JobTypeAWSEC2Copy, jobType)
+
+	res, err = json.Marshal(&worker.AWSEC2CopyJobResult{
+		Ami:    "ami-def456",
+		Region: "eu-central-2",
+	})
+	require.NoError(t, err)
+	err = wrksrv.FinishJob(token, res)
+	require.NoError(t, err)
+
+	imgJobId, token, jobType, _, _, err := wrksrv.RequestJob(context.Background(), test_distro.TestArch3Name, []string{worker.JobTypeAWSEC2Share}, []string{""})
+	require.NoError(t, err)
+	require.Equal(t, worker.JobTypeAWSEC2Share, jobType)
+
+	test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/clones/%v", imgJobId), ``, http.StatusOK, fmt.Sprintf(`
+	{
+		"href": "/api/image-builder-composer/v2/clones/%v",
+		"kind": "CloneComposeStatus",
+		"id": "%v",
+		"status": "running",
+		"type": "aws"
+	}`, imgJobId, imgJobId), "options")
+
+	res, err = json.Marshal(&worker.AWSEC2ShareJobResult{
+		Ami:    "ami-def456",
+		Region: "eu-central-2",
+	})
+	require.NoError(t, err)
+	err = wrksrv.FinishJob(token, res)
+	require.NoError(t, err)
+
+	test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/clones/%v", imgJobId), ``, http.StatusOK, fmt.Sprintf(`
+	{
+		"href": "/api/image-builder-composer/v2/clones/%v",
+		"kind": "CloneComposeStatus",
+		"id": "%v",
+		"status": "success",
+		"type": "aws",
+		"options": {
+			"ami": "ami-def456",
+			"region": "eu-central-2"
+		}
+	}`, imgJobId, imgJobId))
+}
diff --git a/internal/jobqueue/jobqueuetest/jobqueuetest.go b/internal/jobqueue/jobqueuetest/jobqueuetest.go
index b589aff6d..8fa6d775d 100644
--- a/internal/jobqueue/jobqueuetest/jobqueuetest.go
+++ b/internal/jobqueue/jobqueuetest/jobqueuetest.go
@@ -211,7 +211,11 @@ func testDependencies(t *testing.T, q jobqueue.JobQueue) {
 		require.ElementsMatch(t, []uuid.UUID{one, two}, r)
 
 		j := pushTestJob(t, q, "test", nil, []uuid.UUID{one, two}, "")
-		jobType, _, _, queued, started, finished, canceled, deps, err := q.JobStatus(j)
+		_, _, _, _, _, _, _, _, dependents, err := q.JobStatus(one)
+		require.NoError(t, err)
+		require.ElementsMatch(t, dependents, []uuid.UUID{j})
+
+		jobType, _, _, queued, started, finished, canceled, deps, dependents, err := q.JobStatus(j)
 		require.NoError(t, err)
 		require.Equal(t, jobType, "test")
 		require.True(t, !queued.IsZero())
@@ -219,10 +223,11 @@ func testDependencies(t *testing.T, q jobqueue.JobQueue) {
 		require.True(t, finished.IsZero())
 		require.False(t, canceled)
 		require.ElementsMatch(t, deps, []uuid.UUID{one, two})
+		require.Empty(t, dependents)
 
 		require.Equal(t, j, finishNextTestJob(t, q, "test", testResult{}, []uuid.UUID{one, two}))
 
-		jobType, _, result, queued, started, finished, canceled, deps, err := q.JobStatus(j)
+		jobType, _, result, queued, started, finished, canceled, deps, dependents, err := q.JobStatus(j)
 		require.NoError(t, err)
 		require.Equal(t, jobType, "test")
 		require.True(t, !queued.IsZero())
@@ -230,6 +235,7 @@ func testDependencies(t *testing.T, q jobqueue.JobQueue) {
 		require.True(t, !finished.IsZero())
 		require.False(t, canceled)
 		require.ElementsMatch(t, deps, []uuid.UUID{one, two})
+		require.Empty(t, dependents)
 
 		err = json.Unmarshal(result, &testResult{})
 		require.NoError(t, err)
@@ -240,7 +246,7 @@ func testDependencies(t *testing.T, q jobqueue.JobQueue) {
 		two := pushTestJob(t, q, "test", nil, nil, "")
 
 		j := pushTestJob(t, q, "test", nil, []uuid.UUID{one, two}, "")
-		jobType, _, _, queued, started, finished, canceled, deps, err := q.JobStatus(j)
+		jobType, _, _, queued, started, finished, canceled, deps, _, err := q.JobStatus(j)
 		require.NoError(t, err)
 		require.Equal(t, jobType, "test")
 		require.True(t, !queued.IsZero())
@@ -256,7 +262,7 @@ func testDependencies(t *testing.T, q jobqueue.JobQueue) {
 
 		require.Equal(t, j, finishNextTestJob(t, q, "test", testResult{}, []uuid.UUID{one, two}))
 
-		jobType, _, result, queued, started, finished, canceled, deps, err := q.JobStatus(j)
+		jobType, _, result, queued, started, finished, canceled, deps, _, err := q.JobStatus(j)
 		require.NoError(t, err)
 		require.Equal(t, jobType, "test")
 		require.True(t, !queued.IsZero())
@@ -352,7 +358,7 @@ func testCancel(t *testing.T, q jobqueue.JobQueue) {
 	require.NotEmpty(t, id)
 	err = q.CancelJob(id)
 	require.NoError(t, err)
-	jobType, _, result, _, _, _, canceled, _, err := q.JobStatus(id)
+	jobType, _, result, _, _, _, canceled, _, _, err := q.JobStatus(id)
 	require.NoError(t, err)
 	require.Equal(t, jobType, "clownfish")
 	require.True(t, canceled)
@@ -372,7 +378,7 @@ func testCancel(t *testing.T, q jobqueue.JobQueue) {
 	require.Equal(t, json.RawMessage("null"), args)
 	err = q.CancelJob(id)
 	require.NoError(t, err)
-	jobType, _, result, _, _, _, canceled, _, err = q.JobStatus(id)
+	jobType, _, result, _, _, _, canceled, _, _, err = q.JobStatus(id)
 	require.NoError(t, err)
 	require.Equal(t, jobType, "clownfish")
 	require.True(t, canceled)
@@ -395,7 +401,7 @@ func testCancel(t *testing.T, q jobqueue.JobQueue) {
 	err = q.CancelJob(id)
 	require.Error(t, err)
 	require.Equal(t, jobqueue.ErrNotRunning, err)
-	jobType, _, result, _, _, _, canceled, _, err = q.JobStatus(id)
+	jobType, _, result, _, _, _, canceled, _, _, err = q.JobStatus(id)
 	require.NoError(t, err)
 	require.Equal(t, jobType, "clownfish")
 	require.False(t, canceled)
@@ -612,7 +618,7 @@ func test100dequeuers(t *testing.T, q jobqueue.JobQueue) {
 	// try to do some other operations on the jobqueue
 	id := pushTestJob(t, q, "clownfish", nil, nil, "")
 
-	_, _, _, _, _, _, _, _, err := q.JobStatus(id)
+	_, _, _, _, _, _, _, _, _, err := q.JobStatus(id)
 	require.NoError(t, err)
 
 	finishNextTestJob(t, q, "clownfish", testResult{}, nil)
diff --git a/internal/osbuild/gcp_guest_agent_conf_stage_test.go b/internal/osbuild/gcp_guest_agent_conf_stage_test.go
new file mode 100644
index 000000000..6b1a4e768
--- /dev/null
+++ b/internal/osbuild/gcp_guest_agent_conf_stage_test.go
@@ -0,0 +1,84 @@
+package osbuild
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewGcpGuestAgentConfigOptionsValidate(t *testing.T) {
+	tests := []struct {
+		name    string
+		options GcpGuestAgentConfigOptions
+		err     bool
+	}{
+		{
+			name:    "empty-config",
+			options: GcpGuestAgentConfigOptions{},
+			err:     true,
+		},
+		{
+			name: "empty-config",
+			options: GcpGuestAgentConfigOptions{
+				ConfigScope: GcpGuestAgentConfigScopeDistro,
+				Config:      &GcpGuestAgentConfig{},
+			},
+			err: true,
+		},
+		{
+			name: "invalid-ConfigScope",
+			options: GcpGuestAgentConfigOptions{
+				ConfigScope: "incorrect",
+				Config: &GcpGuestAgentConfig{
+					Accounts: &GcpGuestAgentConfigAccounts{
+						Groups: []string{"group1", "group2"},
+					},
+				},
+			},
+			err: true,
+		},
+		{
+			name: "valid-data",
+			options: GcpGuestAgentConfigOptions{
+				ConfigScope: GcpGuestAgentConfigScopeDistro,
+				Config: &GcpGuestAgentConfig{
+					Accounts: &GcpGuestAgentConfigAccounts{
+						Groups: []string{"group1", "group2"},
+					},
+				},
+			},
+			err: false,
+		},
+	}
+	for idx, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.err {
+				assert.Errorf(t, tt.options.validate(), "%q didn't return an error [idx: %d]", tt.name, idx)
+				assert.Panics(t, func() { NewGcpGuestAgentConfigStage(&tt.options) })
+			} else {
+				assert.NoErrorf(t, tt.options.validate(), "%q returned an error [idx: %d]", tt.name, idx)
+				assert.NotPanics(t, func() { NewGcpGuestAgentConfigStage(&tt.options) })
+			}
+		})
+	}
+}
+func TestNewGcpGuestAgentConfigStage(t *testing.T) {
+	expectedStage := &Stage{
+		Type: "org.osbuild.gcp.guest-agent.conf",
+		Options: &GcpGuestAgentConfigOptions{
+			Config: &GcpGuestAgentConfig{
+				Accounts: &GcpGuestAgentConfigAccounts{
+					Groups: []string{"group1", "group2"},
+				},
+			},
+		},
+	}
+	actualStage := NewGcpGuestAgentConfigStage(&GcpGuestAgentConfigOptions{
+		Config: &GcpGuestAgentConfig{
+			Accounts: &GcpGuestAgentConfigAccounts{
+				Groups: []string{"group1", "group2"},
+			},
+		},
+	})
+	assert.Equal(t, expectedStage, actualStage)
+}
diff --git a/test/cases/api.sh b/test/cases/api.sh
index d77a1d70e..c43a18524 100755
--- a/test/cases/api.sh
+++ b/test/cases/api.sh
@@ -284,7 +284,8 @@ curl \
 # Prepare a request to be sent to the composer API.
 #
 
-REQUEST_FILE="${WORKDIR}/request.json"
+REQUEST_FILE="${WORKDIR}/compose_request.json"
+IMG_COMPOSE_REQ_FILE="${WORKDIR}/img_compose_request.json"
 ARCH=$(uname -m)
 SSH_USER=
 TEST_ID="$(uuidgen)"
@@ -418,6 +419,64 @@ function waitForState() {
     export UPLOAD_OPTIONS
 }
 
+function sendImgFromCompose() {
+    OUTPUT=$(mktemp)
+    HTTPSTATUS=$(curl \
+                 --silent \
+                 --show-error \
+                 --cacert /etc/osbuild-composer/ca-crt.pem \
+                 --key /etc/osbuild-composer/client-key.pem \
+                 --cert /etc/osbuild-composer/client-crt.pem \
+                 --header 'Content-Type: application/json' \
+                 --request POST \
+                 --data @"$1" \
+                 --write-out '%{http_code}' \
+                 --output "$OUTPUT" \
+                 https://localhost/api/image-builder-composer/v2/composes/"$COMPOSE_ID"/clone)
+
+    test "$HTTPSTATUS" = "201"
+    IMG_ID=$(jq -r '.id' "$OUTPUT")
+}
+
+function waitForImgState() {
+    while true
+    do
+        OUTPUT=$(curl \
+                     --silent \
+                     --show-error \
+                     --cacert /etc/osbuild-composer/ca-crt.pem \
+                     --key /etc/osbuild-composer/client-key.pem \
+                     --cert /etc/osbuild-composer/client-crt.pem \
+                     "https://localhost/api/image-builder-composer/v2/clones/$IMG_ID")
+
+        IMG_UPLOAD_STATUS=$(echo "$OUTPUT" | jq -r '.status')
+        IMG_UPLOAD_OPTIONS=$(echo "$OUTPUT" | jq -r '.options')
+
+        case "$IMG_UPLOAD_STATUS" in
+            "success")
+                break
+                ;;
+            # all valid status values for a compose which hasn't finished yet
+            "pending"|"running")
+                ;;
+            # default undesired state
+            "failure")
+                echo "Image compose failed"
+                exit 1
+                ;;
+            *)
+                echo "API returned unexpected image status value: '$IMG_UPLOAD_STATUS'"
+                exit 1
+                ;;
+        esac
+
+        sleep 30
+    done
+
+    # export for use in subcases
+    export IMG_UPLOAD_OPTIONS
+}
+
 #
 # Make sure that requesting a non existing paquet results in failure
 #
@@ -427,7 +486,6 @@ jq '.customizations.packages = [ "jesuisunpaquetquinexistepas" ]' "$REQUEST_FILE
 sendCompose "$REQUEST_FILE2"
 waitForState "failure"
 
-
 # crashed/stopped/killed worker should result in a failed state
 sendCompose "$REQUEST_FILE"
 waitForState "building"
@@ -449,6 +507,12 @@ fi
 test "$UPLOAD_TYPE" = "$EXPECTED_UPLOAD_TYPE"
 test $((INIT_COMPOSES+1)) = "$SUBS_COMPOSES"
 
+
+if [ -s "$IMG_COMPOSE_REQ_FILE" ]; then
+    sendImgFromCompose "$IMG_COMPOSE_REQ_FILE"
+    waitForImgState
+fi
+
 #
 # Verify the Cloud-provider specific upload_status options
 #
diff --git a/test/cases/api/aws.sh b/test/cases/api/aws.sh
index a8786c811..e6911dd86 100644
--- a/test/cases/api/aws.sh
+++ b/test/cases/api/aws.sh
@@ -13,12 +13,17 @@ function cleanup() {
   AWS_INSTANCE_ID="${AWS_INSTANCE_ID:-}"
   AMI_IMAGE_ID="${AMI_IMAGE_ID:-}"
   AWS_SNAPSHOT_ID="${AWS_SNAPSHOT_ID:-}"
+  AMI_ID_2="${AMI_ID_2:-}"
+  SNAPSHOT_ID_2="${SNAPSHOT_ID_2:-}"
 
   if [ -n "$AWS_CMD" ]; then
     $AWS_CMD ec2 terminate-instances --instance-ids "$AWS_INSTANCE_ID"
     $AWS_CMD ec2 deregister-image --image-id "$AMI_IMAGE_ID"
     $AWS_CMD ec2 delete-snapshot --snapshot-id "$AWS_SNAPSHOT_ID"
     $AWS_CMD ec2 delete-key-pair --key-name "key-for-$AMI_IMAGE_ID"
+
+    $AWS_CMD ec2 deregister-image --region "$REGION_2" --image-id "$AMI_2"
+    $AWS_CMD ec2 delete-snapshot --region "$REGION_2" --snapshot-id "$SNAPSHOT_ID_2"
   fi
 }
 
@@ -68,6 +73,13 @@ function createReqFile() {
     }
   }
 }
+EOF
+
+  cat > "$IMG_COMPOSE_REQ_FILE" <<EOF
+{
+  "region": "${AWS_REGION_2}",
+  "share_with_accounts":  ["${AWS_API_TEST_SHARE_ACCOUNT_2}"]
+}
 EOF
 }
 
@@ -122,6 +134,38 @@ function verify() {
     exit 1
   fi
 
+  # Verify that the 2nd image from the same compose was copied and shared with existing and new account
+  AMI_ID_2=$(echo "$IMG_UPLOAD_OPTIONS" | jq -r .ami)
+  REGION_2=$(echo "$IMG_UPLOAD_OPTIONS" | jq -r .region)
+  $AWS_CMD ec2 describe-images --owners self --region "$REGION_2" --image-ids "$AMI_ID_2" \
+           > "$WORKDIR/ami2.json"
+
+  SNAPSHOT_ID_2=$(jq -r '.Images[].BlockDeviceMappings[].Ebs.SnapshotId' "$WORKDIR/ami2.json")
+  $AWS_CMD ec2 describe-snapshot-attribute --region "$REGION_2" --snapshot-id "$SNAPSHOT_ID_2" \
+           --attribute createVolumePermission > "$WORKDIR/snapshot-attributes2.json"
+  SHARED_ID_2=$(jq -r ".CreateVolumePermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT\").UserId" "$WORKDIR/snapshot-attributes2.json")
+  if [ "$AWS_API_TEST_SHARE_ACCOUNT" != "$SHARED_ID_2" ]; then
+      echo "EC2 Snapshot wasn't shared with AWS_API_TEST_SHARE_ACCOUNT"
+      exit 1
+  fi
+  SHARED_ID_2=$(jq -r ".CreateVolumePermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT_2\").UserId" "$WORKDIR/snapshot-attributes2.json")
+  if [ "$AWS_API_TEST_SHARE_ACCOUNT_2" != "$SHARED_ID_2" ]; then
+      echo "EC2 Snapshot wasn't shared with AWS_API_TEST_SHARE_ACCOUNT_2"
+      exit 1
+  fi
+
+  $AWS_CMD ec2 describe-image-attribute --attribute launchPermission --region "$REGION_2" --image-id "$AMI_ID_2" > "$WORKDIR/ami-attributes2.json"
+  SHARED_ID_2=$(jq -r ".LaunchPermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT\").UserId" "$WORKDIR/ami-attributes2.json")
+  if [ "$AWS_API_TEST_SHARE_ACCOUNT" != "$SHARED_ID_2" ]; then
+      echo "EC2 ami wasn't shared with AWS_API_TEST_SHARE_ACCOUNT"
+      exit 1
+  fi
+  SHARED_ID_2=$(jq -r ".LaunchPermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT_2\").UserId" "$WORKDIR/ami-attributes2.json")
+  if [ "$AWS_API_TEST_SHARE_ACCOUNT_2" != "$SHARED_ID_2" ]; then
+      echo "EC2 ami wasn't shared with AWS_API_TEST_SHARE_ACCOUNT_2"
+      exit 1
+  fi
+
   # Create key-pair
   $AWS_CMD ec2 create-key-pair --key-name "key-for-$AMI_IMAGE_ID" --query 'KeyMaterial' --output text > keypair.pem
   chmod 400 ./keypair.pem
diff --git a/test/cases/gcp.sh b/test/cases/gcp.sh
index 2533dc361..994c2c7b9 100755
--- a/test/cases/gcp.sh
+++ b/test/cases/gcp.sh
@@ -278,6 +278,8 @@ get_compose_metadata "$COMPOSE_ID"
 # Kill the journal monitor immediately and remove the trap
 sudo pkill -P ${WORKER_JOURNAL_PID}
 trap - EXIT
+# trap cleanup again
+trap cleanup EXIT
 
 # Did the compose finish with success?
 if [[ $COMPOSE_STATUS != FINISHED ]]; then
diff --git a/test/data/manifests/centos_8-x86_64-gce-boot.json b/test/data/manifests/centos_8-x86_64-gce-boot.json
index 3f0f3d7db..85ffcba3f 100644
--- a/test/data/manifests/centos_8-x86_64-gce-boot.json
+++ b/test/data/manifests/centos_8-x86_64-gce-boot.json
@@ -5528,6 +5528,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/centos_9-x86_64-gce-boot.json b/test/data/manifests/centos_9-x86_64-gce-boot.json
index ff9ec59fe..84afb4f99 100644
--- a/test/data/manifests/centos_9-x86_64-gce-boot.json
+++ b/test/data/manifests/centos_9-x86_64-gce-boot.json
@@ -5122,6 +5122,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_8-x86_64-gce-boot.json b/test/data/manifests/rhel_8-x86_64-gce-boot.json
index ec2ad8a20..e3a0772f1 100644
--- a/test/data/manifests/rhel_8-x86_64-gce-boot.json
+++ b/test/data/manifests/rhel_8-x86_64-gce-boot.json
@@ -2332,6 +2332,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_8-x86_64-gce_rhui-boot.json b/test/data/manifests/rhel_8-x86_64-gce_rhui-boot.json
index af4a6a7be..aaea4387c 100644
--- a/test/data/manifests/rhel_8-x86_64-gce_rhui-boot.json
+++ b/test/data/manifests/rhel_8-x86_64-gce_rhui-boot.json
@@ -2343,6 +2343,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_84-x86_64-gce-boot.json b/test/data/manifests/rhel_84-x86_64-gce-boot.json
index f4ca05e27..6da0fd58f 100644
--- a/test/data/manifests/rhel_84-x86_64-gce-boot.json
+++ b/test/data/manifests/rhel_84-x86_64-gce-boot.json
@@ -2364,6 +2364,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_84-x86_64-gce_rhui-boot.json b/test/data/manifests/rhel_84-x86_64-gce_rhui-boot.json
index 7d67bb041..ad048ec92 100644
--- a/test/data/manifests/rhel_84-x86_64-gce_rhui-boot.json
+++ b/test/data/manifests/rhel_84-x86_64-gce_rhui-boot.json
@@ -2375,6 +2375,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_85-x86_64-gce-boot.json b/test/data/manifests/rhel_85-x86_64-gce-boot.json
index c23c38d48..a23954a7b 100644
--- a/test/data/manifests/rhel_85-x86_64-gce-boot.json
+++ b/test/data/manifests/rhel_85-x86_64-gce-boot.json
@@ -2332,6 +2332,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_85-x86_64-gce_rhui-boot.json b/test/data/manifests/rhel_85-x86_64-gce_rhui-boot.json
index 9a74fb4f1..cfdd0b538 100644
--- a/test/data/manifests/rhel_85-x86_64-gce_rhui-boot.json
+++ b/test/data/manifests/rhel_85-x86_64-gce_rhui-boot.json
@@ -2343,6 +2343,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_86-x86_64-gce-boot.json b/test/data/manifests/rhel_86-x86_64-gce-boot.json
index 587b7d947..980b76407 100644
--- a/test/data/manifests/rhel_86-x86_64-gce-boot.json
+++ b/test/data/manifests/rhel_86-x86_64-gce-boot.json
@@ -2332,6 +2332,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_86-x86_64-gce_rhui-boot.json b/test/data/manifests/rhel_86-x86_64-gce_rhui-boot.json
index 9958559a4..e0dc613d7 100644
--- a/test/data/manifests/rhel_86-x86_64-gce_rhui-boot.json
+++ b/test/data/manifests/rhel_86-x86_64-gce_rhui-boot.json
@@ -2343,6 +2343,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_87-x86_64-gce-boot.json b/test/data/manifests/rhel_87-x86_64-gce-boot.json
index 9e2ce616a..0dd01ce48 100644
--- a/test/data/manifests/rhel_87-x86_64-gce-boot.json
+++ b/test/data/manifests/rhel_87-x86_64-gce-boot.json
@@ -2326,6 +2326,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_87-x86_64-gce_rhui-boot.json b/test/data/manifests/rhel_87-x86_64-gce_rhui-boot.json
index ffdf47a8c..f002c6318 100644
--- a/test/data/manifests/rhel_87-x86_64-gce_rhui-boot.json
+++ b/test/data/manifests/rhel_87-x86_64-gce_rhui-boot.json
@@ -2337,6 +2337,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_90-x86_64-gce-boot.json b/test/data/manifests/rhel_90-x86_64-gce-boot.json
index c119af364..b4481455f 100644
--- a/test/data/manifests/rhel_90-x86_64-gce-boot.json
+++ b/test/data/manifests/rhel_90-x86_64-gce-boot.json
@@ -2154,6 +2154,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_90-x86_64-gce_rhui-boot.json b/test/data/manifests/rhel_90-x86_64-gce_rhui-boot.json
index 76b6be3e0..6280e40c6 100644
--- a/test/data/manifests/rhel_90-x86_64-gce_rhui-boot.json
+++ b/test/data/manifests/rhel_90-x86_64-gce_rhui-boot.json
@@ -2160,6 +2160,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_91-x86_64-gce-boot.json b/test/data/manifests/rhel_91-x86_64-gce-boot.json
index 9da731311..4f9e1b131 100644
--- a/test/data/manifests/rhel_91-x86_64-gce-boot.json
+++ b/test/data/manifests/rhel_91-x86_64-gce-boot.json
@@ -5316,6 +5316,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {
diff --git a/test/data/manifests/rhel_91-x86_64-gce_rhui-boot.json b/test/data/manifests/rhel_91-x86_64-gce_rhui-boot.json
index ab1509d92..44722210b 100644
--- a/test/data/manifests/rhel_91-x86_64-gce_rhui-boot.json
+++ b/test/data/manifests/rhel_91-x86_64-gce_rhui-boot.json
@@ -5322,6 +5322,17 @@
               ]
             }
           },
+          {
+            "type": "org.osbuild.gcp.guest-agent.conf",
+            "options": {
+              "config_scope": "distro",
+              "config": {
+                "InstanceSetup": {
+                  "set_boto_config": false
+                }
+              }
+            }
+          },
           {
             "type": "org.osbuild.fstab",
             "options": {