Repository: osbuild/osbuild-composer
Pull request #2938 (2022-08-30 13:35:43)

build(deps): bump github.com/golang-jwt/jwt/v4 from 4.4.1 to 4.4.2
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.4.1 to 4.4.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/golang-jwt/jwt/releases">github.com/golang-jwt/jwt/v4's releases</a>.</em></p>
<blockquote>
<h2>v4.4.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Added MicahParks/keyfunc to extensions by <a href="https://github.com/oxisto"><code>@​oxisto</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/194">golang-jwt/jwt#194</a></li>
<li>Update link to v4 on pkg.go.dev page by <a href="https://github.com/polRk"><code>@​polRk</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/195">golang-jwt/jwt#195</a></li>
<li>add installation guidelines to the README file  by <a href="https://github.com/morelmiles"><code>@​morelmiles</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/204">golang-jwt/jwt#204</a></li>
<li>chore: replace ioutil with io and os by <a href="https://github.com/estensen"><code>@​estensen</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/198">golang-jwt/jwt#198</a></li>
<li>CI check for Go code formatting by <a href="https://github.com/mfridman"><code>@​mfridman</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/206">golang-jwt/jwt#206</a></li>
<li>Create SECURITY.md by <a href="https://github.com/mfridman"><code>@​mfridman</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/171">golang-jwt/jwt#171</a></li>
<li>Update SECURITY.md by <a href="https://github.com/oxisto"><code>@​oxisto</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/207">golang-jwt/jwt#207</a></li>
<li>Fixed integer overflow in NumericDate.MarshalJSON by <a href="https://github.com/qqiao"><code>@​qqiao</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/200">golang-jwt/jwt#200</a></li>
<li>Claims in rsa_test.go Table Driven Test are Unused by <a href="https://github.com/gkech"><code>@​gkech</code></a> in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/212">golang-jwt/jwt#212</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/polRk"><code>@​polRk</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/195">golang-jwt/jwt#195</a></li>
<li><a href="https://github.com/morelmiles"><code>@​morelmiles</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/204">golang-jwt/jwt#204</a></li>
<li><a href="https://github.com/estensen"><code>@​estensen</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/198">golang-jwt/jwt#198</a></li>
<li><a href="https://github.com/qqiao"><code>@​qqiao</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/200">golang-jwt/jwt#200</a></li>
<li><a href="https://github.com/gkech"><code>@​gkech</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/golang-jwt/jwt/pull/212">golang-jwt/jwt#212</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/golang-jwt/jwt/compare/v4.4.1...v4.4.2">https://github.com/golang-jwt/jwt/compare/v4.4.1...v4.4.2</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang-jwt/jwt/commit/9294af54b5fcb8c9c92e3d10ed83d2a7c87fe632"><code>9294af5</code></a> chore: remove unused claims in RSA table driven test (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/212">#212</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/2da0bf7566c39035cc969e3d55c1135bfd69546f"><code>2da0bf7</code></a> Fixed integer overflow in NumericDate.MarshalJSON (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/200">#200</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/8fb42696ff6af3d8a559ab9aefbbcf49de6479b7"><code>8fb4269</code></a> Update SECURITY.md (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/207">#207</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/cf43decf7c27a155bc8d4abcf6c43eab01544c45"><code>cf43dec</code></a> Create SECURITY.md (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/171">#171</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/4426925f0cea29cfc39d8fdccde6f6f6f560d187"><code>4426925</code></a> CI check for Go code formatting (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/206">#206</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/f6c6299f67a12c9994673b29bedbe4b675174759"><code>f6c6299</code></a> chore: replace ioutil with io and os (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/198">#198</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/89a6400b7fe5e5b9e7872ceb14ee9b5a6e2255b7"><code>89a6400</code></a> add installation guidelines to the README file  (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/204">#204</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/6e2ab4291f1aa2205dc9e407e2d23c21c3886e0d"><code>6e2ab42</code></a> docs: update link to pkg.go.dev page (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/195">#195</a>)</li>
<li><a href="https://github.com/golang-jwt/jwt/commit/83478b3c8fda0a378495587b11e804b6db28e870"><code>83478b3</code></a> Added MicahParks/keyfunc to extensions (<a href="https://github-redirect.dependabot.com/golang-jwt/jwt/issues/194">#194</a>)</li>
<li>See full diff in <a href="https://github.com/golang-jwt/jwt/compare/v4.4.1...v4.4.2">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/golang-jwt/jwt/v4&package-manager=go_modules&previous-version=4.4.1&new-version=4.4.2)](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>

ondrejbudai: @dependabot rebase

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..ee02a908a 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/Azure/azure-pipeline-go v0.2.3
 	github.com/Azure/azure-sdk-for-go v66.0.0+incompatible
 	github.com/Azure/azure-storage-blob-go v0.13.0
-	github.com/Azure/go-autorest/autorest v0.11.27
+	github.com/Azure/go-autorest/autorest v0.11.28
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
 	github.com/BurntSushi/toml v1.2.0
 	github.com/aws/aws-sdk-go v1.44.44
@@ -23,7 +23,7 @@ require (
 	github.com/getkin/kin-openapi v0.93.0
 	github.com/go-openapi/swag v0.21.1 // indirect
 	github.com/gobwas/glob v0.2.3
-	github.com/golang-jwt/jwt/v4 v4.4.1
+	github.com/golang-jwt/jwt/v4 v4.4.2
 	github.com/google/go-cmp v0.5.8
 	github.com/google/uuid v1.3.0
 	github.com/gophercloud/gophercloud v0.24.0
@@ -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..03a0ae489 100644
--- a/go.sum
+++ b/go.sum
@@ -95,8 +95,9 @@ github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW
 github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
 github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
 github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
-github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A=
 github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U=
+github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
+github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
 github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
 github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=
 github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
@@ -682,8 +683,9 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
-github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ=
 github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
+github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
 github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
@@ -1099,8 +1101,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/Azure/go-autorest/autorest/go.mod b/vendor/github.com/Azure/go-autorest/autorest/go.mod
index 680b9676f..006841631 100644
--- a/vendor/github.com/Azure/go-autorest/autorest/go.mod
+++ b/vendor/github.com/Azure/go-autorest/autorest/go.mod
@@ -9,5 +9,5 @@ require (
 	github.com/Azure/go-autorest/logger v0.2.1
 	github.com/Azure/go-autorest/tracing v0.6.0
 	github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
-	golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
+	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
 )
diff --git a/vendor/github.com/Azure/go-autorest/autorest/go.sum b/vendor/github.com/Azure/go-autorest/autorest/go.sum
index 4bb88d68c..4476cca9a 100644
--- a/vendor/github.com/Azure/go-autorest/autorest/go.sum
+++ b/vendor/github.com/Azure/go-autorest/autorest/go.sum
@@ -15,8 +15,8 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
 github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/vendor/github.com/golang-jwt/jwt/v4/README.md b/vendor/github.com/golang-jwt/jwt/v4/README.md
index 01b21646e..f5d551ca8 100644
--- a/vendor/github.com/golang-jwt/jwt/v4/README.md
+++ b/vendor/github.com/golang-jwt/jwt/v4/README.md
@@ -36,9 +36,23 @@ The part in the middle is the interesting bit.  It's called the Claims and conta
 
 This library supports the parsing and verification as well as the generation and signing of JWTs.  Current supported signing algorithms are HMAC SHA, RSA, RSA-PSS, and ECDSA, though hooks are present for adding your own.
 
+## Installation Guidelines
+
+1. To install the jwt package, you first need to have [Go](https://go.dev/doc/install) installed, then you can use the command below to add `jwt-go` as a dependency in your Go program.
+
+```sh
+go get -u github.com/golang-jwt/jwt/v4
+```
+
+2. Import it in your code:
+
+```go
+import "github.com/golang-jwt/jwt/v4"
+```
+
 ## Examples
 
-See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt) for examples of usage:
+See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt/v4) for examples of usage:
 
 * [Simple example of parsing and validating a token](https://pkg.go.dev/github.com/golang-jwt/jwt#example-Parse-Hmac)
 * [Simple example of building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt#example-New-Hmac)
@@ -46,14 +60,15 @@ See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt) fo
 
 ## Extensions
 
-This library publishes all the necessary components for adding your own signing methods.  Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`. 
+This library publishes all the necessary components for adding your own signing methods or key functions.  Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod` or provide a `jwt.Keyfunc`.
 
-A common use case would be integrating with different 3rd party signature providers, like key management services from various cloud providers or Hardware Security Modules (HSMs).
+A common use case would be integrating with different 3rd party signature providers, like key management services from various cloud providers or Hardware Security Modules (HSMs) or to implement additional standards.
 
-| Extension | Purpose                                                                                      | Repo                                       |
-|-----------|----------------------------------------------------------------------------------------------|--------------------------------------------|
-| GCP       | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS) | https://github.com/someone1/gcp-jwt-go     |
-| AWS       | Integrates with AWS Key Management Service, KMS                                              | https://github.com/matelang/jwt-go-aws-kms |
+| Extension | Purpose                                                                                                  | Repo                                       |
+| --------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
+| GCP       | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS)             | https://github.com/someone1/gcp-jwt-go     |
+| AWS       | Integrates with AWS Key Management Service, KMS                                                          | https://github.com/matelang/jwt-go-aws-kms |
+| JWKS      | Provides support for JWKS ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) as a `jwt.Keyfunc` | https://github.com/MicahParks/keyfunc       |
 
 *Disclaimer*: Unless otherwise specified, these integrations are maintained by third parties and should not be considered as a primary offer by any of the mentioned cloud providers
 
diff --git a/vendor/github.com/golang-jwt/jwt/v4/SECURITY.md b/vendor/github.com/golang-jwt/jwt/v4/SECURITY.md
new file mode 100644
index 000000000..b08402c34
--- /dev/null
+++ b/vendor/github.com/golang-jwt/jwt/v4/SECURITY.md
@@ -0,0 +1,19 @@
+# Security Policy
+
+## Supported Versions
+
+As of February 2022 (and until this document is updated), the latest version `v4` is supported.
+
+## Reporting a Vulnerability
+
+If you think you found a vulnerability, and even if you are not sure, please report it to jwt-go-security@googlegroups.com or one of the other [golang-jwt maintainers](https://github.com/orgs/golang-jwt/people). Please try be explicit, describe steps to reproduce the security issue with code example(s).
+
+You will receive a response within a timely manner. If the issue is confirmed, we will do our best to release a patch as soon as possible given the complexity of the problem.
+
+## Public Discussions
+
+Please avoid publicly discussing a potential security vulnerability.
+
+Let's take this offline and find a solution first, this limits the potential impact as much as possible.
+
+We appreciate your help!
diff --git a/vendor/github.com/golang-jwt/jwt/v4/rsa_pss.go b/vendor/github.com/golang-jwt/jwt/v4/rsa_pss.go
index 5a8502feb..4fd6f9e61 100644
--- a/vendor/github.com/golang-jwt/jwt/v4/rsa_pss.go
+++ b/vendor/github.com/golang-jwt/jwt/v4/rsa_pss.go
@@ -1,3 +1,4 @@
+//go:build go1.4
 // +build go1.4
 
 package jwt
diff --git a/vendor/github.com/golang-jwt/jwt/v4/token.go b/vendor/github.com/golang-jwt/jwt/v4/token.go
index 09b4cde5a..3cb0f3f0e 100644
--- a/vendor/github.com/golang-jwt/jwt/v4/token.go
+++ b/vendor/github.com/golang-jwt/jwt/v4/token.go
@@ -7,7 +7,6 @@ import (
 	"time"
 )
 
-
 // DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515
 // states that the tokens will utilize a Base64url encoding with no padding. Unfortunately, some implementations
 // of JWT are producing non-standard tokens, and thus require support for decoding. Note that this is a global
diff --git a/vendor/github.com/golang-jwt/jwt/v4/types.go b/vendor/github.com/golang-jwt/jwt/v4/types.go
index 2c647fd2e..ac8e140eb 100644
--- a/vendor/github.com/golang-jwt/jwt/v4/types.go
+++ b/vendor/github.com/golang-jwt/jwt/v4/types.go
@@ -53,9 +53,23 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) {
 	if TimePrecision < time.Second {
 		prec = int(math.Log10(float64(time.Second) / float64(TimePrecision)))
 	}
-	f := float64(date.Truncate(TimePrecision).UnixNano()) / float64(time.Second)
-
-	return []byte(strconv.FormatFloat(f, 'f', prec, 64)), nil
+	truncatedDate := date.Truncate(TimePrecision)
+
+	// For very large timestamps, UnixNano would overflow an int64, but this
+	// function requires nanosecond level precision, so we have to use the
+	// following technique to get round the issue:
+	// 1. Take the normal unix timestamp to form the whole number part of the
+	//    output,
+	// 2. Take the result of the Nanosecond function, which retuns the offset
+	//    within the second of the particular unix time instance, to form the
+	//    decimal part of the output
+	// 3. Concatenate them to produce the final result
+	seconds := strconv.FormatInt(truncatedDate.Unix(), 10)
+	nanosecondsOffset := strconv.FormatFloat(float64(truncatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64)
+
+	output := append([]byte(seconds), []byte(nanosecondsOffset)[1:]...)
+
+	return output, nil
 }
 
 // UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a
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..89f54ce32 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -41,7 +41,7 @@ github.com/Azure/azure-sdk-for-go/version
 github.com/Azure/azure-storage-blob-go/azblob
 # github.com/Azure/go-autorest v14.2.0+incompatible
 github.com/Azure/go-autorest
-# github.com/Azure/go-autorest/autorest v0.11.27
+# github.com/Azure/go-autorest/autorest v0.11.28
 ## explicit
 github.com/Azure/go-autorest/autorest
 github.com/Azure/go-autorest/autorest/azure
@@ -295,7 +295,7 @@ github.com/gobwas/glob/util/runes
 github.com/gobwas/glob/util/strings
 # github.com/golang-jwt/jwt v3.2.2+incompatible
 github.com/golang-jwt/jwt
-# github.com/golang-jwt/jwt/v4 v4.4.1
+# github.com/golang-jwt/jwt/v4 v4.4.2
 ## explicit
 github.com/golang-jwt/jwt/v4
 # github.com/golang/glog v1.0.0
@@ -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": {