Repository: osbuild/osbuild-composer
Pull request #1830 (2021-09-24 04:46:32)

build(deps): bump github.com/Azure/azure-sdk-for-go from 57.2.0+incompatible to 57.4.0+incompatible
Bumps [github.com/Azure/azure-sdk-for-go](https://github.com/Azure/azure-sdk-for-go) from 57.2.0+incompatible to 57.4.0+incompatible.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/Azure/azure-sdk-for-go/releases">github.com/Azure/azure-sdk-for-go's releases</a>.</em></p>
<blockquote>
<h2>v57.4.0</h2>
<h3>New Packages</h3>
<ul>
<li><code>github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2021-07-01/containerinstance</code></li>
<li><code>github.com/Azure/azure-sdk-for-go/services/iothub/mgmt/2021-03-31/devices</code></li>
<li><code>github.com/Azure/azure-sdk-for-go/services/kusto/mgmt/2021-08-27/kusto</code></li>
</ul>
<h3>Updated Packages</h3>
<table>
<thead>
<tr>
<th align="left">Package Path</th>
<th align="center">Changelog</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>github.com/Azure/azure-sdk-for-go/services/netapp/mgmt/2021-06-01/netapp</code></td>
<td align="center"><a href="https://github.com/Azure/azure-sdk-for-go/tree/main/services/netapp/mgmt/2021-06-01/netapp/CHANGELOG.md">details</a></td>
</tr>
</tbody>
</table>
<h2>v57.3.0</h2>
<h3>New Packages</h3>
<ul>
<li><code>github.com/Azure/azure-sdk-for-go/services/extendedlocation/mgmt/2021-08-15/extendedlocation</code></li>
<li><code>github.com/Azure/azure-sdk-for-go/services/preview/resources/mgmt/2021-06-01-preview/policy</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/82d0b8f39a26daa4eabe2b656cf27506ce4be408"><code>82d0b8f</code></a> Release v57.4.0 (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15622">#15622</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/c4db2dafbf1338d5364c9410951260722e1294c9"><code>c4db2da</code></a> Add generator Tools release and refresh (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15324">#15324</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/ed2240f118ccbfdc3259a674c2c8af22e231cb24"><code>ed2240f</code></a> Remove samples ci.yml file (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15595">#15595</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/3e0806d68757153e7fadc1468476ccac24fdf9b0"><code>3e0806d</code></a> Clean-up changelog scripts (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15606">#15606</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/6cc90dc46686dc7595e2f28687fc4e5ec216571e"><code>6cc90dc</code></a> Add ChainedTokenCredentialOptions (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15591">#15591</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/d331316562a81a099dda1bd21d29d5f09399dd20"><code>d331316</code></a> [Tables] Fix ClientOptions struct (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15597">#15597</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/97af53ce275a11724962591d934eac6ac83fa9a4"><code>97af53c</code></a> Example for httpresponse error (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15598">#15598</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/9c875cbd403f7c4c97087acfdc03cafe235ff8c9"><code>9c875cb</code></a> Add type for credential authority hosts (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15574">#15574</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/1668e59d954b982bd289d4d1ebeecbf5a6248f45"><code>1668e59</code></a> Add branch filters to ci.yml files (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15594">#15594</a>)</li>
<li><a href="https://github.com/Azure/azure-sdk-for-go/commit/9f0473573e311ae8d77c6e46c423dc3093c83c67"><code>9f04735</code></a> Add version info to unblock EngSys (<a href="https://github-redirect.dependabot.com/Azure/azure-sdk-for-go/issues/15590">#15590</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/Azure/azure-sdk-for-go/compare/v57.2.0...v57.4.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/Azure/azure-sdk-for-go&package-manager=go_modules&previous-version=57.2.0+incompatible&new-version=57.4.0+incompatible)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>


Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8ad0d47ec..4c45dfded 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -179,6 +179,7 @@ OSTree:
           # - openstack/fedora-34-x86_64
           - openstack/rhel-8-x86_64
           - openstack/rhel-8.5-x86_64
+          - openstack/rhel-9.0-beta-nightly-x86_64
 
 New OSTree:
   stage: test
@@ -191,6 +192,17 @@ New OSTree:
       - RUNNER:
           - openstack/rhel-8.5-x86_64-large
 
+New OSTree (RHEL9-BETA):
+  stage: test
+  extends: OSTree
+  script:
+    - schutzbot/deploy.sh
+    - /usr/libexec/tests/osbuild-composer/ostree-ng-og.sh
+  parallel:
+    matrix:
+      - RUNNER:
+          - openstack/rhel-9.0-beta-nightly-x86_64
+
 OSTree simplified installer:
   stage: test
   extends: OSTree
diff --git a/NEWS.md b/NEWS.md
index 613a6a74c..85346d4f6 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,3 @@
-# OSBuild Composer - Operating System Image Composition Services
-
 ## CHANGES WITH 34:
 
   * Support temporary AWS credentials
diff --git a/distribution/osbuild-composer.service b/distribution/osbuild-composer.service
index b75e8f863..a277c8604 100644
--- a/distribution/osbuild-composer.service
+++ b/distribution/osbuild-composer.service
@@ -1,6 +1,6 @@
 [Unit]
 Description=OSBuild Composer
-After=multi-user.target
+After=network.target
 
 # Weldr API needs a local worker by default.
 # Run `systemctl mask osbuild-worker@1.service`
diff --git a/distribution/osbuild-remote-worker@.service b/distribution/osbuild-remote-worker@.service
index c2cfe7f21..ab19e95c3 100644
--- a/distribution/osbuild-remote-worker@.service
+++ b/distribution/osbuild-remote-worker@.service
@@ -1,6 +1,6 @@
 [Unit]
 Description=OSBuild Composer Remote Worker (%i)
-After=multi-user.target
+After=network.target
 
 [Service]
 Type=simple
diff --git a/distribution/osbuild-worker@.service b/distribution/osbuild-worker@.service
index 36d10f6da..4423085d8 100644
--- a/distribution/osbuild-worker@.service
+++ b/distribution/osbuild-worker@.service
@@ -1,7 +1,7 @@
 [Unit]
 Description=OSBuild Composer Worker (%i)
 Requires=osbuild-local-worker.socket
-After=multi-user.target osbuild-local-worker.socket
+After=network.target osbuild-local-worker.socket
 
 [Service]
 Type=simple
diff --git a/go.mod b/go.mod
index 8718a9553..3eeb70a38 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	cloud.google.com/go/cloudbuild v0.2.0
 	cloud.google.com/go/storage v1.16.1
 	github.com/Azure/azure-pipeline-go v0.2.3
-	github.com/Azure/azure-sdk-for-go v57.2.0+incompatible
+	github.com/Azure/azure-sdk-for-go v57.4.0+incompatible
 	github.com/Azure/azure-storage-blob-go v0.14.0
 	github.com/Azure/go-autorest/autorest v0.11.21
 	github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect
diff --git a/go.sum b/go.sum
index 901dd0c68..811a32219 100644
--- a/go.sum
+++ b/go.sum
@@ -49,8 +49,8 @@ cloud.google.com/go/storage v1.16.1/go.mod h1:LaNorbty3ehnU3rEjXSNV/NRgQA0O8Y+uh
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
-github.com/Azure/azure-sdk-for-go v57.2.0+incompatible h1:zoJapafogLazoyp0x9aQENzNNqxvU6pnGtb2P8/i+HI=
-github.com/Azure/azure-sdk-for-go v57.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v57.4.0+incompatible h1:uzhy/mTwu4rq6Be/fJJnE82PRx2A2vjGjzsA3aQ6Qu0=
+github.com/Azure/azure-sdk-for-go v57.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-storage-blob-go v0.14.0 h1:1BCg74AmVdYwO3dlKwtFU1V0wU2PZdREkXvAmZJRUlM=
 github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@@ -902,6 +902,7 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
 golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/internal/cloudapi/v2/v2.go b/internal/cloudapi/v2/v2.go
index 7b5fcce29..2b1c18796 100644
--- a/internal/cloudapi/v2/v2.go
+++ b/internal/cloudapi/v2/v2.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/google/uuid"
 	"github.com/labstack/echo/v4"
+	"github.com/labstack/echo/v4/middleware"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/common"
@@ -58,6 +59,8 @@ func (server *Server) Handler(path string) http.Handler {
 	e.StdLogger = server.logger
 	e.Pre(common.OperationIDMiddleware)
 
+	e.Use(middleware.Recover())
+
 	handler := apiHandlers{
 		server: server,
 	}
diff --git a/internal/common/constants.go b/internal/common/constants.go
new file mode 100644
index 000000000..57dc40843
--- /dev/null
+++ b/internal/common/constants.go
@@ -0,0 +1,27 @@
+package common
+
+import "fmt"
+
+// These constants are set during buildtime using additional
+// compiler flags. Not all of them are necessarily defined
+// because RPMs can be build from a tarball and spec file without
+// being in a git repository. On the other hand when building
+// composer inside of a container, there is no RPM layer so in
+// that case the RPM version doesn't exist at all.
+var (
+	// Git revision from which this code was built
+	GitRev = "undefined"
+
+	// RPM Version
+	RpmVersion = "undefined"
+)
+
+func BuildVersion() string {
+	if GitRev != "undefined" {
+		return fmt.Sprintf("git-rev:%s", GitRev)
+	} else if RpmVersion != "undefined" {
+		return fmt.Sprintf("NEVRA:%s", RpmVersion)
+	} else {
+		return "devel"
+	}
+}
diff --git a/internal/distro/rhel90/distro.go b/internal/distro/rhel90/distro.go
index 8911ef16e..5d915a36d 100644
--- a/internal/distro/rhel90/distro.go
+++ b/internal/distro/rhel90/distro.go
@@ -550,6 +550,9 @@ func newDistro(name, modulePlatformID, ostreeRef string) distro.Distro {
 	// Shared Services
 	edgeServices := []string{
 		"NetworkManager.service", "firewalld.service", "sshd.service",
+		"greenboot-grub2-set-counter", "greenboot-grub2-set-success", "greenboot-healthcheck",
+		"greenboot-rpm-ostree-grub2-check-fallback", "greenboot-status", "greenboot-task-runner",
+		"redboot-auto-reboot", "redboot-task-runner",
 	}
 
 	// Image Definitions
diff --git a/internal/distro/rhel90/package_sets.go b/internal/distro/rhel90/package_sets.go
index 27e1b4b35..7843a6fd0 100644
--- a/internal/distro/rhel90/package_sets.go
+++ b/internal/distro/rhel90/package_sets.go
@@ -279,21 +279,6 @@ func aarch64EdgeCommitPackageSet() rpmmd.PackageSet {
 // INSTALLER PACKAGE SET
 func installerPackageSet() rpmmd.PackageSet {
 	// TODO: simplify
-	/* TODO: These packages were temporarily removed because they don't
-	  exist in RHEL 9:
-		- dump
-		- gfs2-utils
-		- iwl3945-firmware
-		- iwl4965-firmware
-		- iwl6000-firmware
-		- libertas-sd8686-firmware
-		- libertas-sd8787-firmware
-		- libertas-usb8388-firmware
-		- libertas-usb8388-olpc-firmware
-		- libreport-rhel-anaconda-bugzilla
-		- metacity
-		- system-storage-manager
-	*/
 	return rpmmd.PackageSet{
 		Include: []string{
 			"aajohan-comfortaa-fonts", "abattis-cantarell-fonts",
@@ -304,7 +289,7 @@ func installerPackageSet() rpmmd.PackageSet {
 			"dejavu-sans-mono-fonts", "device-mapper-persistent-data",
 			"dmidecode", "dnf", "dracut-config-generic", "dracut-network",
 			/*"dump",*/ "efibootmgr", "ethtool", "ftp", "gdb-gdbserver", "gdisk",
-			/*"gfs2-utils",*/ "glibc-all-langpacks",
+			/*"gfs2-utils",*/ "glibc-all-langpacks", "gnome-kiosk",
 			"google-noto-sans-cjk-ttc-fonts", "grub2-efi-ia32-cdboot",
 			"grub2-efi-x64-cdboot", "grub2-tools", "grub2-tools-efi",
 			"grub2-tools-extra", "grub2-tools-minimal", "grubby",
@@ -356,30 +341,30 @@ func edgeInstallerPackageSet() rpmmd.PackageSet {
 			"bzip2", "cryptsetup", "curl", "dbus-x11", "dejavu-sans-fonts",
 			"dejavu-sans-mono-fonts", "device-mapper-persistent-data",
 			"dmidecode", "dnf", "dracut-config-generic", "dracut-network",
-			"dump", "efibootmgr", "ethtool", "ftp", "gdb-gdbserver", "gdisk",
-			"gfs2-utils", "glibc-all-langpacks",
+			/*"dump",*/ "efibootmgr", "ethtool", "ftp", "gdb-gdbserver", "gdisk",
+			/*"gfs2-utils",*/ "glibc-all-langpacks", "gnome-kiosk",
 			"google-noto-sans-cjk-ttc-fonts", "grub2-efi-ia32-cdboot",
 			"grub2-efi-x64-cdboot", "grub2-tools", "grub2-tools-efi",
 			"grub2-tools-extra", "grub2-tools-minimal", "grubby",
 			"gsettings-desktop-schemas", "hdparm", "hexedit", "hostname",
 			"initscripts", "ipmitool", "iwl1000-firmware", "iwl100-firmware",
 			"iwl105-firmware", "iwl135-firmware", "iwl2000-firmware",
-			"iwl2030-firmware", "iwl3160-firmware", "iwl3945-firmware",
-			"iwl4965-firmware", "iwl5000-firmware", "iwl5150-firmware",
+			"iwl2030-firmware", "iwl3160-firmware", /*"iwl3945-firmware",*/
+			/*"iwl4965-firmware",*/ "iwl5000-firmware", "iwl5150-firmware",
 			"iwl6000g2a-firmware", "iwl6000g2b-firmware",
 			"iwl6050-firmware", "iwl7260-firmware", "jomolhari-fonts",
 			"kacst-farsi-fonts", "kacst-qurn-fonts", "kbd", "kbd-misc",
 			"kdump-anaconda-addon", "kernel", "khmeros-base-fonts", "less",
-			"libblockdev-lvm-dbus", "libertas-sd8686-firmware",
-			"libertas-sd8787-firmware", "libertas-usb8388-firmware",
-			"libertas-usb8388-olpc-firmware", "libibverbs",
+			"libblockdev-lvm-dbus", /*"libertas-sd8686-firmware",*/
+			/*"libertas-sd8787-firmware",*/ /*"libertas-usb8388-firmware",*/
+			/*"libertas-usb8388-olpc-firmware",*/ "libibverbs",
 			"libreport-plugin-bugzilla", "libreport-plugin-reportuploader",
-			"libreport-rhel-anaconda-bugzilla", "librsvg2", "linux-firmware",
+			/*"libreport-rhel-anaconda-bugzilla",*/ "librsvg2", "linux-firmware",
 			"lklug-fonts", "lohit-assamese-fonts", "lohit-bengali-fonts",
 			"lohit-devanagari-fonts", "lohit-gujarati-fonts",
 			"lohit-gurmukhi-fonts", "lohit-kannada-fonts", "lohit-odia-fonts",
 			"lohit-tamil-fonts", "lohit-telugu-fonts", "lsof", "madan-fonts",
-			"memtest86+", "metacity", "mtr", "mt-st", "net-tools", "nfs-utils",
+			"memtest86+" /*"metacity",*/, "mtr", "mt-st", "net-tools", "nfs-utils",
 			"nmap-ncat", "nm-connection-editor", "nss-tools",
 			"openssh-clients", "openssh-server", "oscap-anaconda-addon",
 			"ostree", "pciutils", "perl-interpreter", "pigz", "plymouth",
@@ -389,7 +374,7 @@ func edgeInstallerPackageSet() rpmmd.PackageSet {
 			"shim-ia32", "shim-x64", "sil-abyssinica-fonts",
 			"sil-padauk-fonts", "sil-scheherazade-fonts", "smartmontools",
 			"smc-meera-fonts", "spice-vdagent", "strace", "syslinux",
-			"systemd", "system-storage-manager", "tar",
+			"systemd" /*"system-storage-manager",*/, "tar",
 			"thai-scalable-waree-fonts", "tigervnc-server-minimal",
 			"tigervnc-server-module", "udisks2", "udisks2-iscsi", "usbutils",
 			"vim-minimal", "volume_key", "wget", "xfsdump", "xfsprogs",
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index af90a4c18..798682ce3 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -561,7 +561,7 @@ func (api *API) statusHandler(writer http.ResponseWriter, request *http.Request,
 		DBVersion:     "0",
 		SchemaVersion: "0",
 		Backend:       "osbuild-composer",
-		Build:         "devel",
+		Build:         common.BuildVersion(),
 		Messages:      make([]string, 0),
 	})
 	common.PanicOnError(err)
diff --git a/osbuild-composer.spec b/osbuild-composer.spec
index 28f62871e..756104aad 100644
--- a/osbuild-composer.spec
+++ b/osbuild-composer.spec
@@ -124,6 +124,14 @@ export GOPATH=$GO_BUILD_PATH:%{gopath}
 export GOFLAGS=-mod=vendor
 %endif
 
+# Set the commit hash so that composer can report what source version
+# was used to build it. This has to be set explicitly when calling rpmbuild,
+# this script will not attempt to automatically discover it.
+%if %{?commit:1}0
+export LDFLAGS="${LDFLAGS} -X 'github.com/osbuild/osbuild-composer/internal/common.GitRev=%{commit}'"
+%endif
+export LDFLAGS="${LDFLAGS} -X 'github.com/osbuild/osbuild-composer/internal/common.RpmVersion=%{name}-%{epoch}:%{version}-%{release}.%{_arch}'"
+
 %gobuild -o _bin/osbuild-composer %{goipath}/cmd/osbuild-composer
 %gobuild -o _bin/osbuild-worker %{goipath}/cmd/osbuild-worker
 
diff --git a/tools/provision.sh b/tools/provision.sh
index ec4bb8021..25badacc8 100755
--- a/tools/provision.sh
+++ b/tools/provision.sh
@@ -20,7 +20,12 @@ elif [[ $ID == rhel || $ID == centos ]] && [[ ${VERSION_ID%.*} == 9 ]]; then
     sudo dnf install -y ./redhat-internal-cert-install-0.1-23.el7.csb.noarch.rpm dnf-plugins-core
     sudo dnf copr enable -y copr.devel.redhat.com/osbuild-team/epel-el9 "rhel-9.dev-$ARCH"
     # koji is not available yet apparently
-    sudo dnf install -y ansible
+    # jmespath required for json_query
+    sudo dnf install -y ansible python3-jmespath
+
+    # json_query filter, used in our ansible playbooks, was moved to the
+    # 'community.general' collection
+    sudo ansible-galaxy collection install community.general
 fi
 
 sudo mkdir -p /etc/osbuild-composer
diff --git a/vendor/github.com/Azure/azure-sdk-for-go/version/version.go b/vendor/github.com/Azure/azure-sdk-for-go/version/version.go
index b224028b8..cd0b38184 100644
--- a/vendor/github.com/Azure/azure-sdk-for-go/version/version.go
+++ b/vendor/github.com/Azure/azure-sdk-for-go/version/version.go
@@ -4,4 +4,4 @@ package version
 // Licensed under the MIT License. See License.txt in the project root for license information.
 
 // Number contains the semantic version of this SDK.
-const Number = "v57.2.0"
+const Number = "v57.4.0"
diff --git a/vendor/github.com/labstack/echo/v4/middleware/basic_auth.go b/vendor/github.com/labstack/echo/v4/middleware/basic_auth.go
new file mode 100644
index 000000000..8cf1ed9fc
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/basic_auth.go
@@ -0,0 +1,106 @@
+package middleware
+
+import (
+	"encoding/base64"
+	"strconv"
+	"strings"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// BasicAuthConfig defines the config for BasicAuth middleware.
+	BasicAuthConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Validator is a function to validate BasicAuth credentials.
+		// Required.
+		Validator BasicAuthValidator
+
+		// Realm is a string to define realm attribute of BasicAuth.
+		// Default value "Restricted".
+		Realm string
+	}
+
+	// BasicAuthValidator defines a function to validate BasicAuth credentials.
+	BasicAuthValidator func(string, string, echo.Context) (bool, error)
+)
+
+const (
+	basic        = "basic"
+	defaultRealm = "Restricted"
+)
+
+var (
+	// DefaultBasicAuthConfig is the default BasicAuth middleware config.
+	DefaultBasicAuthConfig = BasicAuthConfig{
+		Skipper: DefaultSkipper,
+		Realm:   defaultRealm,
+	}
+)
+
+// BasicAuth returns an BasicAuth middleware.
+//
+// For valid credentials it calls the next handler.
+// For missing or invalid credentials, it sends "401 - Unauthorized" response.
+func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc {
+	c := DefaultBasicAuthConfig
+	c.Validator = fn
+	return BasicAuthWithConfig(c)
+}
+
+// BasicAuthWithConfig returns an BasicAuth middleware with config.
+// See `BasicAuth()`.
+func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Validator == nil {
+		panic("echo: basic-auth middleware requires a validator function")
+	}
+	if config.Skipper == nil {
+		config.Skipper = DefaultBasicAuthConfig.Skipper
+	}
+	if config.Realm == "" {
+		config.Realm = defaultRealm
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			auth := c.Request().Header.Get(echo.HeaderAuthorization)
+			l := len(basic)
+
+			if len(auth) > l+1 && strings.EqualFold(auth[:l], basic) {
+				b, err := base64.StdEncoding.DecodeString(auth[l+1:])
+				if err != nil {
+					return err
+				}
+				cred := string(b)
+				for i := 0; i < len(cred); i++ {
+					if cred[i] == ':' {
+						// Verify credentials
+						valid, err := config.Validator(cred[:i], cred[i+1:], c)
+						if err != nil {
+							return err
+						} else if valid {
+							return next(c)
+						}
+						break
+					}
+				}
+			}
+
+			realm := defaultRealm
+			if config.Realm != defaultRealm {
+				realm = strconv.Quote(config.Realm)
+			}
+
+			// Need to return `401` for browsers to pop-up login box.
+			c.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+" realm="+realm)
+			return echo.ErrUnauthorized
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/body_dump.go b/vendor/github.com/labstack/echo/v4/middleware/body_dump.go
new file mode 100644
index 000000000..ebd0d0ab2
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/body_dump.go
@@ -0,0 +1,107 @@
+package middleware
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/http"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// BodyDumpConfig defines the config for BodyDump middleware.
+	BodyDumpConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Handler receives request and response payload.
+		// Required.
+		Handler BodyDumpHandler
+	}
+
+	// BodyDumpHandler receives the request and response payload.
+	BodyDumpHandler func(echo.Context, []byte, []byte)
+
+	bodyDumpResponseWriter struct {
+		io.Writer
+		http.ResponseWriter
+	}
+)
+
+var (
+	// DefaultBodyDumpConfig is the default BodyDump middleware config.
+	DefaultBodyDumpConfig = BodyDumpConfig{
+		Skipper: DefaultSkipper,
+	}
+)
+
+// BodyDump returns a BodyDump middleware.
+//
+// BodyDump middleware captures the request and response payload and calls the
+// registered handler.
+func BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc {
+	c := DefaultBodyDumpConfig
+	c.Handler = handler
+	return BodyDumpWithConfig(c)
+}
+
+// BodyDumpWithConfig returns a BodyDump middleware with config.
+// See: `BodyDump()`.
+func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Handler == nil {
+		panic("echo: body-dump middleware requires a handler function")
+	}
+	if config.Skipper == nil {
+		config.Skipper = DefaultBodyDumpConfig.Skipper
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) (err error) {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			// Request
+			reqBody := []byte{}
+			if c.Request().Body != nil { // Read
+				reqBody, _ = ioutil.ReadAll(c.Request().Body)
+			}
+			c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
+
+			// Response
+			resBody := new(bytes.Buffer)
+			mw := io.MultiWriter(c.Response().Writer, resBody)
+			writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}
+			c.Response().Writer = writer
+
+			if err = next(c); err != nil {
+				c.Error(err)
+			}
+
+			// Callback
+			config.Handler(c, reqBody, resBody.Bytes())
+
+			return
+		}
+	}
+}
+
+func (w *bodyDumpResponseWriter) WriteHeader(code int) {
+	w.ResponseWriter.WriteHeader(code)
+}
+
+func (w *bodyDumpResponseWriter) Write(b []byte) (int, error) {
+	return w.Writer.Write(b)
+}
+
+func (w *bodyDumpResponseWriter) Flush() {
+	w.ResponseWriter.(http.Flusher).Flush()
+}
+
+func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	return w.ResponseWriter.(http.Hijacker).Hijack()
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/body_limit.go b/vendor/github.com/labstack/echo/v4/middleware/body_limit.go
new file mode 100644
index 000000000..b436bd595
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/body_limit.go
@@ -0,0 +1,117 @@
+package middleware
+
+import (
+	"fmt"
+	"io"
+	"sync"
+
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/bytes"
+)
+
+type (
+	// BodyLimitConfig defines the config for BodyLimit middleware.
+	BodyLimitConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Maximum allowed size for a request body, it can be specified
+		// as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P.
+		Limit string `yaml:"limit"`
+		limit int64
+	}
+
+	limitedReader struct {
+		BodyLimitConfig
+		reader  io.ReadCloser
+		read    int64
+		context echo.Context
+	}
+)
+
+var (
+	// DefaultBodyLimitConfig is the default BodyLimit middleware config.
+	DefaultBodyLimitConfig = BodyLimitConfig{
+		Skipper: DefaultSkipper,
+	}
+)
+
+// BodyLimit returns a BodyLimit middleware.
+//
+// BodyLimit middleware sets the maximum allowed size for a request body, if the
+// size exceeds the configured limit, it sends "413 - Request Entity Too Large"
+// response. The BodyLimit is determined based on both `Content-Length` request
+// header and actual content read, which makes it super secure.
+// Limit can be specified as `4x` or `4xB`, where x is one of the multiple from K, M,
+// G, T or P.
+func BodyLimit(limit string) echo.MiddlewareFunc {
+	c := DefaultBodyLimitConfig
+	c.Limit = limit
+	return BodyLimitWithConfig(c)
+}
+
+// BodyLimitWithConfig returns a BodyLimit middleware with config.
+// See: `BodyLimit()`.
+func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultBodyLimitConfig.Skipper
+	}
+
+	limit, err := bytes.Parse(config.Limit)
+	if err != nil {
+		panic(fmt.Errorf("echo: invalid body-limit=%s", config.Limit))
+	}
+	config.limit = limit
+	pool := limitedReaderPool(config)
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+
+			// Based on content length
+			if req.ContentLength > config.limit {
+				return echo.ErrStatusRequestEntityTooLarge
+			}
+
+			// Based on content read
+			r := pool.Get().(*limitedReader)
+			r.Reset(req.Body, c)
+			defer pool.Put(r)
+			req.Body = r
+
+			return next(c)
+		}
+	}
+}
+
+func (r *limitedReader) Read(b []byte) (n int, err error) {
+	n, err = r.reader.Read(b)
+	r.read += int64(n)
+	if r.read > r.limit {
+		return n, echo.ErrStatusRequestEntityTooLarge
+	}
+	return
+}
+
+func (r *limitedReader) Close() error {
+	return r.reader.Close()
+}
+
+func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) {
+	r.reader = reader
+	r.context = context
+	r.read = 0
+}
+
+func limitedReaderPool(c BodyLimitConfig) sync.Pool {
+	return sync.Pool{
+		New: func() interface{} {
+			return &limitedReader{BodyLimitConfig: c}
+		},
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/compress.go b/vendor/github.com/labstack/echo/v4/middleware/compress.go
new file mode 100644
index 000000000..6ae197453
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/compress.go
@@ -0,0 +1,146 @@
+package middleware
+
+import (
+	"bufio"
+	"compress/gzip"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"strings"
+	"sync"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// GzipConfig defines the config for Gzip middleware.
+	GzipConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Gzip compression level.
+		// Optional. Default value -1.
+		Level int `yaml:"level"`
+	}
+
+	gzipResponseWriter struct {
+		io.Writer
+		http.ResponseWriter
+	}
+)
+
+const (
+	gzipScheme = "gzip"
+)
+
+var (
+	// DefaultGzipConfig is the default Gzip middleware config.
+	DefaultGzipConfig = GzipConfig{
+		Skipper: DefaultSkipper,
+		Level:   -1,
+	}
+)
+
+// Gzip returns a middleware which compresses HTTP response using gzip compression
+// scheme.
+func Gzip() echo.MiddlewareFunc {
+	return GzipWithConfig(DefaultGzipConfig)
+}
+
+// GzipWithConfig return Gzip middleware with config.
+// See: `Gzip()`.
+func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultGzipConfig.Skipper
+	}
+	if config.Level == 0 {
+		config.Level = DefaultGzipConfig.Level
+	}
+
+	pool := gzipCompressPool(config)
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			res := c.Response()
+			res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
+			if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {
+				res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
+				i := pool.Get()
+				w, ok := i.(*gzip.Writer)
+				if !ok {
+					return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error())
+				}
+				rw := res.Writer
+				w.Reset(rw)
+				defer func() {
+					if res.Size == 0 {
+						if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme {
+							res.Header().Del(echo.HeaderContentEncoding)
+						}
+						// We have to reset response to it's pristine state when
+						// nothing is written to body or error is returned.
+						// See issue #424, #407.
+						res.Writer = rw
+						w.Reset(ioutil.Discard)
+					}
+					w.Close()
+					pool.Put(w)
+				}()
+				grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw}
+				res.Writer = grw
+			}
+			return next(c)
+		}
+	}
+}
+
+func (w *gzipResponseWriter) WriteHeader(code int) {
+	if code == http.StatusNoContent { // Issue #489
+		w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
+	}
+	w.Header().Del(echo.HeaderContentLength) // Issue #444
+	w.ResponseWriter.WriteHeader(code)
+}
+
+func (w *gzipResponseWriter) Write(b []byte) (int, error) {
+	if w.Header().Get(echo.HeaderContentType) == "" {
+		w.Header().Set(echo.HeaderContentType, http.DetectContentType(b))
+	}
+	return w.Writer.Write(b)
+}
+
+func (w *gzipResponseWriter) Flush() {
+	w.Writer.(*gzip.Writer).Flush()
+	if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
+		flusher.Flush()
+	}
+}
+
+func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	return w.ResponseWriter.(http.Hijacker).Hijack()
+}
+
+func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
+	if p, ok := w.ResponseWriter.(http.Pusher); ok {
+		return p.Push(target, opts)
+	}
+	return http.ErrNotSupported
+}
+
+func gzipCompressPool(config GzipConfig) sync.Pool {
+	return sync.Pool{
+		New: func() interface{} {
+			w, err := gzip.NewWriterLevel(ioutil.Discard, config.Level)
+			if err != nil {
+				return err
+			}
+			return w
+		},
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/cors.go b/vendor/github.com/labstack/echo/v4/middleware/cors.go
new file mode 100644
index 000000000..d6ef89644
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/cors.go
@@ -0,0 +1,211 @@
+package middleware
+
+import (
+	"net/http"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// CORSConfig defines the config for CORS middleware.
+	CORSConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// AllowOrigin defines a list of origins that may access the resource.
+		// Optional. Default value []string{"*"}.
+		AllowOrigins []string `yaml:"allow_origins"`
+
+		// AllowOriginFunc is a custom function to validate the origin. It takes the
+		// origin as an argument and returns true if allowed or false otherwise. If
+		// an error is returned, it is returned by the handler. If this option is
+		// set, AllowOrigins is ignored.
+		// Optional.
+		AllowOriginFunc func(origin string) (bool, error) `yaml:"allow_origin_func"`
+
+		// AllowMethods defines a list methods allowed when accessing the resource.
+		// This is used in response to a preflight request.
+		// Optional. Default value DefaultCORSConfig.AllowMethods.
+		AllowMethods []string `yaml:"allow_methods"`
+
+		// AllowHeaders defines a list of request headers that can be used when
+		// making the actual request. This is in response to a preflight request.
+		// Optional. Default value []string{}.
+		AllowHeaders []string `yaml:"allow_headers"`
+
+		// AllowCredentials indicates whether or not the response to the request
+		// can be exposed when the credentials flag is true. When used as part of
+		// a response to a preflight request, this indicates whether or not the
+		// actual request can be made using credentials.
+		// Optional. Default value false.
+		AllowCredentials bool `yaml:"allow_credentials"`
+
+		// ExposeHeaders defines a whitelist headers that clients are allowed to
+		// access.
+		// Optional. Default value []string{}.
+		ExposeHeaders []string `yaml:"expose_headers"`
+
+		// MaxAge indicates how long (in seconds) the results of a preflight request
+		// can be cached.
+		// Optional. Default value 0.
+		MaxAge int `yaml:"max_age"`
+	}
+)
+
+var (
+	// DefaultCORSConfig is the default CORS middleware config.
+	DefaultCORSConfig = CORSConfig{
+		Skipper:      DefaultSkipper,
+		AllowOrigins: []string{"*"},
+		AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
+	}
+)
+
+// CORS returns a Cross-Origin Resource Sharing (CORS) middleware.
+// See: https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_CORS
+func CORS() echo.MiddlewareFunc {
+	return CORSWithConfig(DefaultCORSConfig)
+}
+
+// CORSWithConfig returns a CORS middleware with config.
+// See: `CORS()`.
+func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultCORSConfig.Skipper
+	}
+	if len(config.AllowOrigins) == 0 {
+		config.AllowOrigins = DefaultCORSConfig.AllowOrigins
+	}
+	if len(config.AllowMethods) == 0 {
+		config.AllowMethods = DefaultCORSConfig.AllowMethods
+	}
+
+	allowOriginPatterns := []string{}
+	for _, origin := range config.AllowOrigins {
+		pattern := regexp.QuoteMeta(origin)
+		pattern = strings.Replace(pattern, "\\*", ".*", -1)
+		pattern = strings.Replace(pattern, "\\?", ".", -1)
+		pattern = "^" + pattern + "$"
+		allowOriginPatterns = append(allowOriginPatterns, pattern)
+	}
+
+	allowMethods := strings.Join(config.AllowMethods, ",")
+	allowHeaders := strings.Join(config.AllowHeaders, ",")
+	exposeHeaders := strings.Join(config.ExposeHeaders, ",")
+	maxAge := strconv.Itoa(config.MaxAge)
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			res := c.Response()
+			origin := req.Header.Get(echo.HeaderOrigin)
+			allowOrigin := ""
+
+			preflight := req.Method == http.MethodOptions
+			res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
+
+			// No Origin provided
+			if origin == "" {
+				if !preflight {
+					return next(c)
+				}
+				return c.NoContent(http.StatusNoContent)
+			}
+
+			if config.AllowOriginFunc != nil {
+				allowed, err := config.AllowOriginFunc(origin)
+				if err != nil {
+					return err
+				}
+				if allowed {
+					allowOrigin = origin
+				}
+			} else {
+				// Check allowed origins
+				for _, o := range config.AllowOrigins {
+					if o == "*" && config.AllowCredentials {
+						allowOrigin = origin
+						break
+					}
+					if o == "*" || o == origin {
+						allowOrigin = o
+						break
+					}
+					if matchSubdomain(origin, o) {
+						allowOrigin = origin
+						break
+					}
+				}
+
+				// Check allowed origin patterns
+				for _, re := range allowOriginPatterns {
+					if allowOrigin == "" {
+						didx := strings.Index(origin, "://")
+						if didx == -1 {
+							continue
+						}
+						domAuth := origin[didx+3:]
+						// to avoid regex cost by invalid long domain
+						if len(domAuth) > 253 {
+							break
+						}
+
+						if match, _ := regexp.MatchString(re, origin); match {
+							allowOrigin = origin
+							break
+						}
+					}
+				}
+			}
+
+			// Origin not allowed
+			if allowOrigin == "" {
+				if !preflight {
+					return next(c)
+				}
+				return c.NoContent(http.StatusNoContent)
+			}
+
+			// Simple request
+			if !preflight {
+				res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
+				if config.AllowCredentials {
+					res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
+				}
+				if exposeHeaders != "" {
+					res.Header().Set(echo.HeaderAccessControlExposeHeaders, exposeHeaders)
+				}
+				return next(c)
+			}
+
+			// Preflight request
+			res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod)
+			res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders)
+			res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
+			res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods)
+			if config.AllowCredentials {
+				res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
+			}
+			if allowHeaders != "" {
+				res.Header().Set(echo.HeaderAccessControlAllowHeaders, allowHeaders)
+			} else {
+				h := req.Header.Get(echo.HeaderAccessControlRequestHeaders)
+				if h != "" {
+					res.Header().Set(echo.HeaderAccessControlAllowHeaders, h)
+				}
+			}
+			if config.MaxAge > 0 {
+				res.Header().Set(echo.HeaderAccessControlMaxAge, maxAge)
+			}
+			return c.NoContent(http.StatusNoContent)
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/csrf.go b/vendor/github.com/labstack/echo/v4/middleware/csrf.go
new file mode 100644
index 000000000..7804997d4
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/csrf.go
@@ -0,0 +1,221 @@
+package middleware
+
+import (
+	"crypto/subtle"
+	"errors"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/random"
+)
+
+type (
+	// CSRFConfig defines the config for CSRF middleware.
+	CSRFConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// TokenLength is the length of the generated token.
+		TokenLength uint8 `yaml:"token_length"`
+		// Optional. Default value 32.
+
+		// TokenLookup is a string in the form of "<source>:<key>" that is used
+		// to extract token from the request.
+		// Optional. Default value "header:X-CSRF-Token".
+		// Possible values:
+		// - "header:<name>"
+		// - "form:<name>"
+		// - "query:<name>"
+		TokenLookup string `yaml:"token_lookup"`
+
+		// Context key to store generated CSRF token into context.
+		// Optional. Default value "csrf".
+		ContextKey string `yaml:"context_key"`
+
+		// Name of the CSRF cookie. This cookie will store CSRF token.
+		// Optional. Default value "csrf".
+		CookieName string `yaml:"cookie_name"`
+
+		// Domain of the CSRF cookie.
+		// Optional. Default value none.
+		CookieDomain string `yaml:"cookie_domain"`
+
+		// Path of the CSRF cookie.
+		// Optional. Default value none.
+		CookiePath string `yaml:"cookie_path"`
+
+		// Max age (in seconds) of the CSRF cookie.
+		// Optional. Default value 86400 (24hr).
+		CookieMaxAge int `yaml:"cookie_max_age"`
+
+		// Indicates if CSRF cookie is secure.
+		// Optional. Default value false.
+		CookieSecure bool `yaml:"cookie_secure"`
+
+		// Indicates if CSRF cookie is HTTP only.
+		// Optional. Default value false.
+		CookieHTTPOnly bool `yaml:"cookie_http_only"`
+
+		// Indicates SameSite mode of the CSRF cookie.
+		// Optional. Default value SameSiteDefaultMode.
+		CookieSameSite http.SameSite `yaml:"cookie_same_site"`
+	}
+
+	// csrfTokenExtractor defines a function that takes `echo.Context` and returns
+	// either a token or an error.
+	csrfTokenExtractor func(echo.Context) (string, error)
+)
+
+var (
+	// DefaultCSRFConfig is the default CSRF middleware config.
+	DefaultCSRFConfig = CSRFConfig{
+		Skipper:        DefaultSkipper,
+		TokenLength:    32,
+		TokenLookup:    "header:" + echo.HeaderXCSRFToken,
+		ContextKey:     "csrf",
+		CookieName:     "_csrf",
+		CookieMaxAge:   86400,
+		CookieSameSite: http.SameSiteDefaultMode,
+	}
+)
+
+// CSRF returns a Cross-Site Request Forgery (CSRF) middleware.
+// See: https://en.wikipedia.org/wiki/Cross-site_request_forgery
+func CSRF() echo.MiddlewareFunc {
+	c := DefaultCSRFConfig
+	return CSRFWithConfig(c)
+}
+
+// CSRFWithConfig returns a CSRF middleware with config.
+// See `CSRF()`.
+func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultCSRFConfig.Skipper
+	}
+	if config.TokenLength == 0 {
+		config.TokenLength = DefaultCSRFConfig.TokenLength
+	}
+	if config.TokenLookup == "" {
+		config.TokenLookup = DefaultCSRFConfig.TokenLookup
+	}
+	if config.ContextKey == "" {
+		config.ContextKey = DefaultCSRFConfig.ContextKey
+	}
+	if config.CookieName == "" {
+		config.CookieName = DefaultCSRFConfig.CookieName
+	}
+	if config.CookieMaxAge == 0 {
+		config.CookieMaxAge = DefaultCSRFConfig.CookieMaxAge
+	}
+	if config.CookieSameSite == http.SameSiteNoneMode {
+		config.CookieSecure = true
+	}
+
+	// Initialize
+	parts := strings.Split(config.TokenLookup, ":")
+	extractor := csrfTokenFromHeader(parts[1])
+	switch parts[0] {
+	case "form":
+		extractor = csrfTokenFromForm(parts[1])
+	case "query":
+		extractor = csrfTokenFromQuery(parts[1])
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			k, err := c.Cookie(config.CookieName)
+			token := ""
+
+			// Generate token
+			if err != nil {
+				token = random.String(config.TokenLength)
+			} else {
+				// Reuse token
+				token = k.Value
+			}
+
+			switch req.Method {
+			case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
+			default:
+				// Validate token only for requests which are not defined as 'safe' by RFC7231
+				clientToken, err := extractor(c)
+				if err != nil {
+					return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+				}
+				if !validateCSRFToken(token, clientToken) {
+					return echo.NewHTTPError(http.StatusForbidden, "invalid csrf token")
+				}
+			}
+
+			// Set CSRF cookie
+			cookie := new(http.Cookie)
+			cookie.Name = config.CookieName
+			cookie.Value = token
+			if config.CookiePath != "" {
+				cookie.Path = config.CookiePath
+			}
+			if config.CookieDomain != "" {
+				cookie.Domain = config.CookieDomain
+			}
+			if config.CookieSameSite != http.SameSiteDefaultMode {
+				cookie.SameSite = config.CookieSameSite
+			}
+			cookie.Expires = time.Now().Add(time.Duration(config.CookieMaxAge) * time.Second)
+			cookie.Secure = config.CookieSecure
+			cookie.HttpOnly = config.CookieHTTPOnly
+			c.SetCookie(cookie)
+
+			// Store token in the context
+			c.Set(config.ContextKey, token)
+
+			// Protect clients from caching the response
+			c.Response().Header().Add(echo.HeaderVary, echo.HeaderCookie)
+
+			return next(c)
+		}
+	}
+}
+
+// csrfTokenFromForm returns a `csrfTokenExtractor` that extracts token from the
+// provided request header.
+func csrfTokenFromHeader(header string) csrfTokenExtractor {
+	return func(c echo.Context) (string, error) {
+		return c.Request().Header.Get(header), nil
+	}
+}
+
+// csrfTokenFromForm returns a `csrfTokenExtractor` that extracts token from the
+// provided form parameter.
+func csrfTokenFromForm(param string) csrfTokenExtractor {
+	return func(c echo.Context) (string, error) {
+		token := c.FormValue(param)
+		if token == "" {
+			return "", errors.New("missing csrf token in the form parameter")
+		}
+		return token, nil
+	}
+}
+
+// csrfTokenFromQuery returns a `csrfTokenExtractor` that extracts token from the
+// provided query parameter.
+func csrfTokenFromQuery(param string) csrfTokenExtractor {
+	return func(c echo.Context) (string, error) {
+		token := c.QueryParam(param)
+		if token == "" {
+			return "", errors.New("missing csrf token in the query string")
+		}
+		return token, nil
+	}
+}
+
+func validateCSRFToken(token, clientToken string) bool {
+	return subtle.ConstantTimeCompare([]byte(token), []byte(clientToken)) == 1
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/decompress.go b/vendor/github.com/labstack/echo/v4/middleware/decompress.go
new file mode 100644
index 000000000..c046359a2
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/decompress.go
@@ -0,0 +1,120 @@
+package middleware
+
+import (
+	"bytes"
+	"compress/gzip"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"sync"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// DecompressConfig defines the config for Decompress middleware.
+	DecompressConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers
+		GzipDecompressPool Decompressor
+	}
+)
+
+//GZIPEncoding content-encoding header if set to "gzip", decompress body contents.
+const GZIPEncoding string = "gzip"
+
+// Decompressor is used to get the sync.Pool used by the middleware to get Gzip readers
+type Decompressor interface {
+	gzipDecompressPool() sync.Pool
+}
+
+var (
+	//DefaultDecompressConfig defines the config for decompress middleware
+	DefaultDecompressConfig = DecompressConfig{
+		Skipper:            DefaultSkipper,
+		GzipDecompressPool: &DefaultGzipDecompressPool{},
+	}
+)
+
+// DefaultGzipDecompressPool is the default implementation of Decompressor interface
+type DefaultGzipDecompressPool struct {
+}
+
+func (d *DefaultGzipDecompressPool) gzipDecompressPool() sync.Pool {
+	return sync.Pool{
+		New: func() interface{} {
+			// create with an empty reader (but with GZIP header)
+			w, err := gzip.NewWriterLevel(ioutil.Discard, gzip.BestSpeed)
+			if err != nil {
+				return err
+			}
+
+			b := new(bytes.Buffer)
+			w.Reset(b)
+			w.Flush()
+			w.Close()
+
+			r, err := gzip.NewReader(bytes.NewReader(b.Bytes()))
+			if err != nil {
+				return err
+			}
+			return r
+		},
+	}
+}
+
+//Decompress decompresses request body based if content encoding type is set to "gzip" with default config
+func Decompress() echo.MiddlewareFunc {
+	return DecompressWithConfig(DefaultDecompressConfig)
+}
+
+//DecompressWithConfig decompresses request body based if content encoding type is set to "gzip" with config
+func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultGzipConfig.Skipper
+	}
+	if config.GzipDecompressPool == nil {
+		config.GzipDecompressPool = DefaultDecompressConfig.GzipDecompressPool
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		pool := config.GzipDecompressPool.gzipDecompressPool()
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+			switch c.Request().Header.Get(echo.HeaderContentEncoding) {
+			case GZIPEncoding:
+				b := c.Request().Body
+
+				i := pool.Get()
+				gr, ok := i.(*gzip.Reader)
+				if !ok {
+					return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error())
+				}
+
+				if err := gr.Reset(b); err != nil {
+					pool.Put(gr)
+					if err == io.EOF { //ignore if body is empty
+						return next(c)
+					}
+					return err
+				}
+				var buf bytes.Buffer
+				io.Copy(&buf, gr)
+
+				gr.Close()
+				pool.Put(gr)
+
+				b.Close() // http.Request.Body is closed by the Server, but because we are replacing it, it must be closed here
+
+				r := ioutil.NopCloser(&buf)
+				c.Request().Body = r
+			}
+			return next(c)
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/jwt.go b/vendor/github.com/labstack/echo/v4/middleware/jwt.go
new file mode 100644
index 000000000..c2e7c06d4
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/jwt.go
@@ -0,0 +1,347 @@
+// +build go1.15
+
+package middleware
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"strings"
+
+	"github.com/golang-jwt/jwt"
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// JWTConfig defines the config for JWT middleware.
+	JWTConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// BeforeFunc defines a function which is executed just before the middleware.
+		BeforeFunc BeforeFunc
+
+		// SuccessHandler defines a function which is executed for a valid token.
+		SuccessHandler JWTSuccessHandler
+
+		// ErrorHandler defines a function which is executed for an invalid token.
+		// It may be used to define a custom JWT error.
+		ErrorHandler JWTErrorHandler
+
+		// ErrorHandlerWithContext is almost identical to ErrorHandler, but it's passed the current context.
+		ErrorHandlerWithContext JWTErrorHandlerWithContext
+
+		// Signing key to validate token.
+		// This is one of the three options to provide a token validation key.
+		// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
+		// Required if neither user-defined KeyFunc nor SigningKeys is provided.
+		SigningKey interface{}
+
+		// Map of signing keys to validate token with kid field usage.
+		// This is one of the three options to provide a token validation key.
+		// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
+		// Required if neither user-defined KeyFunc nor SigningKey is provided.
+		SigningKeys map[string]interface{}
+
+		// Signing method used to check the token's signing algorithm.
+		// Optional. Default value HS256.
+		SigningMethod string
+
+		// Context key to store user information from the token into context.
+		// Optional. Default value "user".
+		ContextKey string
+
+		// Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation.
+		// Not used if custom ParseTokenFunc is set.
+		// Optional. Default value jwt.MapClaims
+		Claims jwt.Claims
+
+		// TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
+		// to extract token from the request.
+		// Optional. Default value "header:Authorization".
+		// Possible values:
+		// - "header:<name>"
+		// - "query:<name>"
+		// - "param:<name>"
+		// - "cookie:<name>"
+		// - "form:<name>"
+		// Multiply sources example:
+		// - "header: Authorization,cookie: myowncookie"
+
+		TokenLookup string
+
+		// AuthScheme to be used in the Authorization header.
+		// Optional. Default value "Bearer".
+		AuthScheme string
+
+		// KeyFunc defines a user-defined function that supplies the public key for a token validation.
+		// The function shall take care of verifying the signing algorithm and selecting the proper key.
+		// A user-defined KeyFunc can be useful if tokens are issued by an external party.
+		// Used by default ParseTokenFunc implementation.
+		//
+		// When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored.
+		// This is one of the three options to provide a token validation key.
+		// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
+		// Required if neither SigningKeys nor SigningKey is provided.
+		// Not used if custom ParseTokenFunc is set.
+		// Default to an internal implementation verifying the signing algorithm and selecting the proper key.
+		KeyFunc jwt.Keyfunc
+
+		// ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token
+		// parsing fails or parsed token is invalid.
+		// Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library
+		ParseTokenFunc func(auth string, c echo.Context) (interface{}, error)
+	}
+
+	// JWTSuccessHandler defines a function which is executed for a valid token.
+	JWTSuccessHandler func(echo.Context)
+
+	// JWTErrorHandler defines a function which is executed for an invalid token.
+	JWTErrorHandler func(error) error
+
+	// JWTErrorHandlerWithContext is almost identical to JWTErrorHandler, but it's passed the current context.
+	JWTErrorHandlerWithContext func(error, echo.Context) error
+
+	jwtExtractor func(echo.Context) (string, error)
+)
+
+// Algorithms
+const (
+	AlgorithmHS256 = "HS256"
+)
+
+// Errors
+var (
+	ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "missing or malformed jwt")
+	ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt")
+)
+
+var (
+	// DefaultJWTConfig is the default JWT auth middleware config.
+	DefaultJWTConfig = JWTConfig{
+		Skipper:       DefaultSkipper,
+		SigningMethod: AlgorithmHS256,
+		ContextKey:    "user",
+		TokenLookup:   "header:" + echo.HeaderAuthorization,
+		AuthScheme:    "Bearer",
+		Claims:        jwt.MapClaims{},
+		KeyFunc:       nil,
+	}
+)
+
+// JWT returns a JSON Web Token (JWT) auth middleware.
+//
+// For valid token, it sets the user in context and calls next handler.
+// For invalid token, it returns "401 - Unauthorized" error.
+// For missing token, it returns "400 - Bad Request" error.
+//
+// See: https://jwt.io/introduction
+// See `JWTConfig.TokenLookup`
+func JWT(key interface{}) echo.MiddlewareFunc {
+	c := DefaultJWTConfig
+	c.SigningKey = key
+	return JWTWithConfig(c)
+}
+
+// JWTWithConfig returns a JWT auth middleware with config.
+// See: `JWT()`.
+func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultJWTConfig.Skipper
+	}
+	if config.SigningKey == nil && len(config.SigningKeys) == 0 && config.KeyFunc == nil && config.ParseTokenFunc == nil {
+		panic("echo: jwt middleware requires signing key")
+	}
+	if config.SigningMethod == "" {
+		config.SigningMethod = DefaultJWTConfig.SigningMethod
+	}
+	if config.ContextKey == "" {
+		config.ContextKey = DefaultJWTConfig.ContextKey
+	}
+	if config.Claims == nil {
+		config.Claims = DefaultJWTConfig.Claims
+	}
+	if config.TokenLookup == "" {
+		config.TokenLookup = DefaultJWTConfig.TokenLookup
+	}
+	if config.AuthScheme == "" {
+		config.AuthScheme = DefaultJWTConfig.AuthScheme
+	}
+	if config.KeyFunc == nil {
+		config.KeyFunc = config.defaultKeyFunc
+	}
+	if config.ParseTokenFunc == nil {
+		config.ParseTokenFunc = config.defaultParseToken
+	}
+
+	// Initialize
+	// Split sources
+	sources := strings.Split(config.TokenLookup, ",")
+	var extractors []jwtExtractor
+	for _, source := range sources {
+		parts := strings.Split(source, ":")
+
+		switch parts[0] {
+		case "query":
+			extractors = append(extractors, jwtFromQuery(parts[1]))
+		case "param":
+			extractors = append(extractors, jwtFromParam(parts[1]))
+		case "cookie":
+			extractors = append(extractors, jwtFromCookie(parts[1]))
+		case "form":
+			extractors = append(extractors, jwtFromForm(parts[1]))
+		case "header":
+			extractors = append(extractors, jwtFromHeader(parts[1], config.AuthScheme))
+		}
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			if config.BeforeFunc != nil {
+				config.BeforeFunc(c)
+			}
+			var auth string
+			var err error
+			for _, extractor := range extractors {
+				// Extract token from extractor, if it's not fail break the loop and
+				// set auth
+				auth, err = extractor(c)
+				if err == nil {
+					break
+				}
+			}
+			// If none of extractor has a token, handle error
+			if err != nil {
+				if config.ErrorHandler != nil {
+					return config.ErrorHandler(err)
+				}
+
+				if config.ErrorHandlerWithContext != nil {
+					return config.ErrorHandlerWithContext(err, c)
+				}
+				return err
+			}
+
+			token, err := config.ParseTokenFunc(auth, c)
+			if err == nil {
+				// Store user information from token into context.
+				c.Set(config.ContextKey, token)
+				if config.SuccessHandler != nil {
+					config.SuccessHandler(c)
+				}
+				return next(c)
+			}
+			if config.ErrorHandler != nil {
+				return config.ErrorHandler(err)
+			}
+			if config.ErrorHandlerWithContext != nil {
+				return config.ErrorHandlerWithContext(err, c)
+			}
+			return &echo.HTTPError{
+				Code:     ErrJWTInvalid.Code,
+				Message:  ErrJWTInvalid.Message,
+				Internal: err,
+			}
+		}
+	}
+}
+
+func (config *JWTConfig) defaultParseToken(auth string, c echo.Context) (interface{}, error) {
+	token := new(jwt.Token)
+	var err error
+	// Issue #647, #656
+	if _, ok := config.Claims.(jwt.MapClaims); ok {
+		token, err = jwt.Parse(auth, config.KeyFunc)
+	} else {
+		t := reflect.ValueOf(config.Claims).Type().Elem()
+		claims := reflect.New(t).Interface().(jwt.Claims)
+		token, err = jwt.ParseWithClaims(auth, claims, config.KeyFunc)
+	}
+	if err != nil {
+		return nil, err
+	}
+	if !token.Valid {
+		return nil, errors.New("invalid token")
+	}
+	return token, nil
+}
+
+// defaultKeyFunc returns a signing key of the given token.
+func (config *JWTConfig) defaultKeyFunc(t *jwt.Token) (interface{}, error) {
+	// Check the signing method
+	if t.Method.Alg() != config.SigningMethod {
+		return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
+	}
+	if len(config.SigningKeys) > 0 {
+		if kid, ok := t.Header["kid"].(string); ok {
+			if key, ok := config.SigningKeys[kid]; ok {
+				return key, nil
+			}
+		}
+		return nil, fmt.Errorf("unexpected jwt key id=%v", t.Header["kid"])
+	}
+
+	return config.SigningKey, nil
+}
+
+// jwtFromHeader returns a `jwtExtractor` that extracts token from the request header.
+func jwtFromHeader(header string, authScheme string) jwtExtractor {
+	return func(c echo.Context) (string, error) {
+		auth := c.Request().Header.Get(header)
+		l := len(authScheme)
+		if len(auth) > l+1 && auth[:l] == authScheme {
+			return auth[l+1:], nil
+		}
+		return "", ErrJWTMissing
+	}
+}
+
+// jwtFromQuery returns a `jwtExtractor` that extracts token from the query string.
+func jwtFromQuery(param string) jwtExtractor {
+	return func(c echo.Context) (string, error) {
+		token := c.QueryParam(param)
+		if token == "" {
+			return "", ErrJWTMissing
+		}
+		return token, nil
+	}
+}
+
+// jwtFromParam returns a `jwtExtractor` that extracts token from the url param string.
+func jwtFromParam(param string) jwtExtractor {
+	return func(c echo.Context) (string, error) {
+		token := c.Param(param)
+		if token == "" {
+			return "", ErrJWTMissing
+		}
+		return token, nil
+	}
+}
+
+// jwtFromCookie returns a `jwtExtractor` that extracts token from the named cookie.
+func jwtFromCookie(name string) jwtExtractor {
+	return func(c echo.Context) (string, error) {
+		cookie, err := c.Cookie(name)
+		if err != nil {
+			return "", ErrJWTMissing
+		}
+		return cookie.Value, nil
+	}
+}
+
+// jwtFromForm returns a `jwtExtractor` that extracts token from the form field.
+func jwtFromForm(name string) jwtExtractor {
+	return func(c echo.Context) (string, error) {
+		field := c.FormValue(name)
+		if field == "" {
+			return "", ErrJWTMissing
+		}
+		return field, nil
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/key_auth.go b/vendor/github.com/labstack/echo/v4/middleware/key_auth.go
new file mode 100644
index 000000000..fd169aa2c
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/key_auth.go
@@ -0,0 +1,166 @@
+package middleware
+
+import (
+	"errors"
+	"net/http"
+	"strings"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// KeyAuthConfig defines the config for KeyAuth middleware.
+	KeyAuthConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// KeyLookup is a string in the form of "<source>:<name>" that is used
+		// to extract key from the request.
+		// Optional. Default value "header:Authorization".
+		// Possible values:
+		// - "header:<name>"
+		// - "query:<name>"
+		// - "form:<name>"
+		KeyLookup string `yaml:"key_lookup"`
+
+		// AuthScheme to be used in the Authorization header.
+		// Optional. Default value "Bearer".
+		AuthScheme string
+
+		// Validator is a function to validate key.
+		// Required.
+		Validator KeyAuthValidator
+
+		// ErrorHandler defines a function which is executed for an invalid key.
+		// It may be used to define a custom error.
+		ErrorHandler KeyAuthErrorHandler
+	}
+
+	// KeyAuthValidator defines a function to validate KeyAuth credentials.
+	KeyAuthValidator func(string, echo.Context) (bool, error)
+
+	keyExtractor func(echo.Context) (string, error)
+
+	// KeyAuthErrorHandler defines a function which is executed for an invalid key.
+	KeyAuthErrorHandler func(error, echo.Context) error
+)
+
+var (
+	// DefaultKeyAuthConfig is the default KeyAuth middleware config.
+	DefaultKeyAuthConfig = KeyAuthConfig{
+		Skipper:    DefaultSkipper,
+		KeyLookup:  "header:" + echo.HeaderAuthorization,
+		AuthScheme: "Bearer",
+	}
+)
+
+// KeyAuth returns an KeyAuth middleware.
+//
+// For valid key it calls the next handler.
+// For invalid key, it sends "401 - Unauthorized" response.
+// For missing key, it sends "400 - Bad Request" response.
+func KeyAuth(fn KeyAuthValidator) echo.MiddlewareFunc {
+	c := DefaultKeyAuthConfig
+	c.Validator = fn
+	return KeyAuthWithConfig(c)
+}
+
+// KeyAuthWithConfig returns an KeyAuth middleware with config.
+// See `KeyAuth()`.
+func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultKeyAuthConfig.Skipper
+	}
+	// Defaults
+	if config.AuthScheme == "" {
+		config.AuthScheme = DefaultKeyAuthConfig.AuthScheme
+	}
+	if config.KeyLookup == "" {
+		config.KeyLookup = DefaultKeyAuthConfig.KeyLookup
+	}
+	if config.Validator == nil {
+		panic("echo: key-auth middleware requires a validator function")
+	}
+
+	// Initialize
+	parts := strings.Split(config.KeyLookup, ":")
+	extractor := keyFromHeader(parts[1], config.AuthScheme)
+	switch parts[0] {
+	case "query":
+		extractor = keyFromQuery(parts[1])
+	case "form":
+		extractor = keyFromForm(parts[1])
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			// Extract and verify key
+			key, err := extractor(c)
+			if err != nil {
+				if config.ErrorHandler != nil {
+					return config.ErrorHandler(err, c)
+				}
+				return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+			}
+			valid, err := config.Validator(key, c)
+			if err != nil {
+				if config.ErrorHandler != nil {
+					return config.ErrorHandler(err, c)
+				}
+				return &echo.HTTPError{
+					Code:     http.StatusUnauthorized,
+					Message:  "invalid key",
+					Internal: err,
+				}
+			} else if valid {
+				return next(c)
+			}
+			return echo.ErrUnauthorized
+		}
+	}
+}
+
+// keyFromHeader returns a `keyExtractor` that extracts key from the request header.
+func keyFromHeader(header string, authScheme string) keyExtractor {
+	return func(c echo.Context) (string, error) {
+		auth := c.Request().Header.Get(header)
+		if auth == "" {
+			return "", errors.New("missing key in request header")
+		}
+		if header == echo.HeaderAuthorization {
+			l := len(authScheme)
+			if len(auth) > l+1 && auth[:l] == authScheme {
+				return auth[l+1:], nil
+			}
+			return "", errors.New("invalid key in the request header")
+		}
+		return auth, nil
+	}
+}
+
+// keyFromQuery returns a `keyExtractor` that extracts key from the query string.
+func keyFromQuery(param string) keyExtractor {
+	return func(c echo.Context) (string, error) {
+		key := c.QueryParam(param)
+		if key == "" {
+			return "", errors.New("missing key in the query string")
+		}
+		return key, nil
+	}
+}
+
+// keyFromForm returns a `keyExtractor` that extracts key from the form.
+func keyFromForm(param string) keyExtractor {
+	return func(c echo.Context) (string, error) {
+		key := c.FormValue(param)
+		if key == "" {
+			return "", errors.New("missing key in the form")
+		}
+		return key, nil
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/logger.go b/vendor/github.com/labstack/echo/v4/middleware/logger.go
new file mode 100644
index 000000000..9baac4769
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/logger.go
@@ -0,0 +1,223 @@
+package middleware
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/color"
+	"github.com/valyala/fasttemplate"
+)
+
+type (
+	// LoggerConfig defines the config for Logger middleware.
+	LoggerConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Tags to construct the logger format.
+		//
+		// - time_unix
+		// - time_unix_nano
+		// - time_rfc3339
+		// - time_rfc3339_nano
+		// - time_custom
+		// - id (Request ID)
+		// - remote_ip
+		// - uri
+		// - host
+		// - method
+		// - path
+		// - protocol
+		// - referer
+		// - user_agent
+		// - status
+		// - error
+		// - latency (In nanoseconds)
+		// - latency_human (Human readable)
+		// - bytes_in (Bytes received)
+		// - bytes_out (Bytes sent)
+		// - header:<NAME>
+		// - query:<NAME>
+		// - form:<NAME>
+		//
+		// Example "${remote_ip} ${status}"
+		//
+		// Optional. Default value DefaultLoggerConfig.Format.
+		Format string `yaml:"format"`
+
+		// Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
+		CustomTimeFormat string `yaml:"custom_time_format"`
+
+		// Output is a writer where logs in JSON format are written.
+		// Optional. Default value os.Stdout.
+		Output io.Writer
+
+		template *fasttemplate.Template
+		colorer  *color.Color
+		pool     *sync.Pool
+	}
+)
+
+var (
+	// DefaultLoggerConfig is the default Logger middleware config.
+	DefaultLoggerConfig = LoggerConfig{
+		Skipper: DefaultSkipper,
+		Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
+			`"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` +
+			`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
+			`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
+		CustomTimeFormat: "2006-01-02 15:04:05.00000",
+		colorer:          color.New(),
+	}
+)
+
+// Logger returns a middleware that logs HTTP requests.
+func Logger() echo.MiddlewareFunc {
+	return LoggerWithConfig(DefaultLoggerConfig)
+}
+
+// LoggerWithConfig returns a Logger middleware with config.
+// See: `Logger()`.
+func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultLoggerConfig.Skipper
+	}
+	if config.Format == "" {
+		config.Format = DefaultLoggerConfig.Format
+	}
+	if config.Output == nil {
+		config.Output = DefaultLoggerConfig.Output
+	}
+
+	config.template = fasttemplate.New(config.Format, "${", "}")
+	config.colorer = color.New()
+	config.colorer.SetOutput(config.Output)
+	config.pool = &sync.Pool{
+		New: func() interface{} {
+			return bytes.NewBuffer(make([]byte, 256))
+		},
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) (err error) {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			res := c.Response()
+			start := time.Now()
+			if err = next(c); err != nil {
+				c.Error(err)
+			}
+			stop := time.Now()
+			buf := config.pool.Get().(*bytes.Buffer)
+			buf.Reset()
+			defer config.pool.Put(buf)
+
+			if _, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
+				switch tag {
+				case "time_unix":
+					return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
+				case "time_unix_nano":
+					return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10))
+				case "time_rfc3339":
+					return buf.WriteString(time.Now().Format(time.RFC3339))
+				case "time_rfc3339_nano":
+					return buf.WriteString(time.Now().Format(time.RFC3339Nano))
+				case "time_custom":
+					return buf.WriteString(time.Now().Format(config.CustomTimeFormat))
+				case "id":
+					id := req.Header.Get(echo.HeaderXRequestID)
+					if id == "" {
+						id = res.Header().Get(echo.HeaderXRequestID)
+					}
+					return buf.WriteString(id)
+				case "remote_ip":
+					return buf.WriteString(c.RealIP())
+				case "host":
+					return buf.WriteString(req.Host)
+				case "uri":
+					return buf.WriteString(req.RequestURI)
+				case "method":
+					return buf.WriteString(req.Method)
+				case "path":
+					p := req.URL.Path
+					if p == "" {
+						p = "/"
+					}
+					return buf.WriteString(p)
+				case "protocol":
+					return buf.WriteString(req.Proto)
+				case "referer":
+					return buf.WriteString(req.Referer())
+				case "user_agent":
+					return buf.WriteString(req.UserAgent())
+				case "status":
+					n := res.Status
+					s := config.colorer.Green(n)
+					switch {
+					case n >= 500:
+						s = config.colorer.Red(n)
+					case n >= 400:
+						s = config.colorer.Yellow(n)
+					case n >= 300:
+						s = config.colorer.Cyan(n)
+					}
+					return buf.WriteString(s)
+				case "error":
+					if err != nil {
+						// Error may contain invalid JSON e.g. `"`
+						b, _ := json.Marshal(err.Error())
+						b = b[1 : len(b)-1]
+						return buf.Write(b)
+					}
+				case "latency":
+					l := stop.Sub(start)
+					return buf.WriteString(strconv.FormatInt(int64(l), 10))
+				case "latency_human":
+					return buf.WriteString(stop.Sub(start).String())
+				case "bytes_in":
+					cl := req.Header.Get(echo.HeaderContentLength)
+					if cl == "" {
+						cl = "0"
+					}
+					return buf.WriteString(cl)
+				case "bytes_out":
+					return buf.WriteString(strconv.FormatInt(res.Size, 10))
+				default:
+					switch {
+					case strings.HasPrefix(tag, "header:"):
+						return buf.Write([]byte(c.Request().Header.Get(tag[7:])))
+					case strings.HasPrefix(tag, "query:"):
+						return buf.Write([]byte(c.QueryParam(tag[6:])))
+					case strings.HasPrefix(tag, "form:"):
+						return buf.Write([]byte(c.FormValue(tag[5:])))
+					case strings.HasPrefix(tag, "cookie:"):
+						cookie, err := c.Cookie(tag[7:])
+						if err == nil {
+							return buf.Write([]byte(cookie.Value))
+						}
+					}
+				}
+				return 0, nil
+			}); err != nil {
+				return
+			}
+
+			if config.Output == nil {
+				_, err = c.Logger().Output().Write(buf.Bytes())
+				return
+			}
+			_, err = config.Output.Write(buf.Bytes())
+			return
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/method_override.go b/vendor/github.com/labstack/echo/v4/middleware/method_override.go
new file mode 100644
index 000000000..92b14d2ed
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/method_override.go
@@ -0,0 +1,92 @@
+package middleware
+
+import (
+	"net/http"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// MethodOverrideConfig defines the config for MethodOverride middleware.
+	MethodOverrideConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Getter is a function that gets overridden method from the request.
+		// Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride).
+		Getter MethodOverrideGetter
+	}
+
+	// MethodOverrideGetter is a function that gets overridden method from the request
+	MethodOverrideGetter func(echo.Context) string
+)
+
+var (
+	// DefaultMethodOverrideConfig is the default MethodOverride middleware config.
+	DefaultMethodOverrideConfig = MethodOverrideConfig{
+		Skipper: DefaultSkipper,
+		Getter:  MethodFromHeader(echo.HeaderXHTTPMethodOverride),
+	}
+)
+
+// MethodOverride returns a MethodOverride middleware.
+// MethodOverride  middleware checks for the overridden method from the request and
+// uses it instead of the original method.
+//
+// For security reasons, only `POST` method can be overridden.
+func MethodOverride() echo.MiddlewareFunc {
+	return MethodOverrideWithConfig(DefaultMethodOverrideConfig)
+}
+
+// MethodOverrideWithConfig returns a MethodOverride middleware with config.
+// See: `MethodOverride()`.
+func MethodOverrideWithConfig(config MethodOverrideConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultMethodOverrideConfig.Skipper
+	}
+	if config.Getter == nil {
+		config.Getter = DefaultMethodOverrideConfig.Getter
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			if req.Method == http.MethodPost {
+				m := config.Getter(c)
+				if m != "" {
+					req.Method = m
+				}
+			}
+			return next(c)
+		}
+	}
+}
+
+// MethodFromHeader is a `MethodOverrideGetter` that gets overridden method from
+// the request header.
+func MethodFromHeader(header string) MethodOverrideGetter {
+	return func(c echo.Context) string {
+		return c.Request().Header.Get(header)
+	}
+}
+
+// MethodFromForm is a `MethodOverrideGetter` that gets overridden method from the
+// form parameter.
+func MethodFromForm(param string) MethodOverrideGetter {
+	return func(c echo.Context) string {
+		return c.FormValue(param)
+	}
+}
+
+// MethodFromQuery is a `MethodOverrideGetter` that gets overridden method from
+// the query parameter.
+func MethodFromQuery(param string) MethodOverrideGetter {
+	return func(c echo.Context) string {
+		return c.QueryParam(param)
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/middleware.go b/vendor/github.com/labstack/echo/v4/middleware/middleware.go
new file mode 100644
index 000000000..a7ad73a5c
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/middleware.go
@@ -0,0 +1,89 @@
+package middleware
+
+import (
+	"net/http"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// Skipper defines a function to skip middleware. Returning true skips processing
+	// the middleware.
+	Skipper func(echo.Context) bool
+
+	// BeforeFunc defines a function which is executed just before the middleware.
+	BeforeFunc func(echo.Context)
+)
+
+func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {
+	groups := pattern.FindAllStringSubmatch(input, -1)
+	if groups == nil {
+		return nil
+	}
+	values := groups[0][1:]
+	replace := make([]string, 2*len(values))
+	for i, v := range values {
+		j := 2 * i
+		replace[j] = "$" + strconv.Itoa(i+1)
+		replace[j+1] = v
+	}
+	return strings.NewReplacer(replace...)
+}
+
+func rewriteRulesRegex(rewrite map[string]string) map[*regexp.Regexp]string {
+	// Initialize
+	rulesRegex := map[*regexp.Regexp]string{}
+	for k, v := range rewrite {
+		k = regexp.QuoteMeta(k)
+		k = strings.Replace(k, `\*`, "(.*?)", -1)
+		if strings.HasPrefix(k, `\^`) {
+			k = strings.Replace(k, `\^`, "^", -1)
+		}
+		k = k + "$"
+		rulesRegex[regexp.MustCompile(k)] = v
+	}
+	return rulesRegex
+}
+
+func rewriteURL(rewriteRegex map[*regexp.Regexp]string, req *http.Request) error {
+	if len(rewriteRegex) == 0 {
+		return nil
+	}
+
+	// Depending how HTTP request is sent RequestURI could contain Scheme://Host/path or be just /path.
+	// We only want to use path part for rewriting and therefore trim prefix if it exists
+	rawURI := req.RequestURI
+	if rawURI != "" && rawURI[0] != '/' {
+		prefix := ""
+		if req.URL.Scheme != "" {
+			prefix = req.URL.Scheme + "://"
+		}
+		if req.URL.Host != "" {
+			prefix += req.URL.Host // host or host:port
+		}
+		if prefix != "" {
+			rawURI = strings.TrimPrefix(rawURI, prefix)
+		}
+	}
+
+	for k, v := range rewriteRegex {
+		if replacer := captureTokens(k, rawURI); replacer != nil {
+			url, err := req.URL.Parse(replacer.Replace(v))
+			if err != nil {
+				return err
+			}
+			req.URL = url
+
+			return nil // rewrite only once
+		}
+	}
+	return nil
+}
+
+// DefaultSkipper returns false which processes the middleware.
+func DefaultSkipper(echo.Context) bool {
+	return false
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/proxy.go b/vendor/github.com/labstack/echo/v4/middleware/proxy.go
new file mode 100644
index 000000000..6cfd6731e
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/proxy.go
@@ -0,0 +1,303 @@
+package middleware
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"math/rand"
+	"net"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+	"regexp"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/labstack/echo/v4"
+)
+
+// TODO: Handle TLS proxy
+
+type (
+	// ProxyConfig defines the config for Proxy middleware.
+	ProxyConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Balancer defines a load balancing technique.
+		// Required.
+		Balancer ProxyBalancer
+
+		// Rewrite defines URL path rewrite rules. The values captured in asterisk can be
+		// retrieved by index e.g. $1, $2 and so on.
+		// Examples:
+		// "/old":              "/new",
+		// "/api/*":            "/$1",
+		// "/js/*":             "/public/javascripts/$1",
+		// "/users/*/orders/*": "/user/$1/order/$2",
+		Rewrite map[string]string
+
+		// RegexRewrite defines rewrite rules using regexp.Rexexp with captures
+		// Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.
+		// Example:
+		// "^/old/[0.9]+/":     "/new",
+		// "^/api/.+?/(.*)":    "/v2/$1",
+		RegexRewrite map[*regexp.Regexp]string
+
+		// Context key to store selected ProxyTarget into context.
+		// Optional. Default value "target".
+		ContextKey string
+
+		// To customize the transport to remote.
+		// Examples: If custom TLS certificates are required.
+		Transport http.RoundTripper
+
+		// ModifyResponse defines function to modify response from ProxyTarget.
+		ModifyResponse func(*http.Response) error
+	}
+
+	// ProxyTarget defines the upstream target.
+	ProxyTarget struct {
+		Name string
+		URL  *url.URL
+		Meta echo.Map
+	}
+
+	// ProxyBalancer defines an interface to implement a load balancing technique.
+	ProxyBalancer interface {
+		AddTarget(*ProxyTarget) bool
+		RemoveTarget(string) bool
+		Next(echo.Context) *ProxyTarget
+	}
+
+	commonBalancer struct {
+		targets []*ProxyTarget
+		mutex   sync.RWMutex
+	}
+
+	// RandomBalancer implements a random load balancing technique.
+	randomBalancer struct {
+		*commonBalancer
+		random *rand.Rand
+	}
+
+	// RoundRobinBalancer implements a round-robin load balancing technique.
+	roundRobinBalancer struct {
+		*commonBalancer
+		i uint32
+	}
+)
+
+var (
+	// DefaultProxyConfig is the default Proxy middleware config.
+	DefaultProxyConfig = ProxyConfig{
+		Skipper:    DefaultSkipper,
+		ContextKey: "target",
+	}
+)
+
+func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		in, _, err := c.Response().Hijack()
+		if err != nil {
+			c.Set("_error", fmt.Sprintf("proxy raw, hijack error=%v, url=%s", t.URL, err))
+			return
+		}
+		defer in.Close()
+
+		out, err := net.Dial("tcp", t.URL.Host)
+		if err != nil {
+			c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", t.URL, err)))
+			return
+		}
+		defer out.Close()
+
+		// Write header
+		err = r.Write(out)
+		if err != nil {
+			c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", t.URL, err)))
+			return
+		}
+
+		errCh := make(chan error, 2)
+		cp := func(dst io.Writer, src io.Reader) {
+			_, err = io.Copy(dst, src)
+			errCh <- err
+		}
+
+		go cp(out, in)
+		go cp(in, out)
+		err = <-errCh
+		if err != nil && err != io.EOF {
+			c.Set("_error", fmt.Errorf("proxy raw, copy body error=%v, url=%s", t.URL, err))
+		}
+	})
+}
+
+// NewRandomBalancer returns a random proxy balancer.
+func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
+	b := &randomBalancer{commonBalancer: new(commonBalancer)}
+	b.targets = targets
+	return b
+}
+
+// NewRoundRobinBalancer returns a round-robin proxy balancer.
+func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {
+	b := &roundRobinBalancer{commonBalancer: new(commonBalancer)}
+	b.targets = targets
+	return b
+}
+
+// AddTarget adds an upstream target to the list.
+func (b *commonBalancer) AddTarget(target *ProxyTarget) bool {
+	for _, t := range b.targets {
+		if t.Name == target.Name {
+			return false
+		}
+	}
+	b.mutex.Lock()
+	defer b.mutex.Unlock()
+	b.targets = append(b.targets, target)
+	return true
+}
+
+// RemoveTarget removes an upstream target from the list.
+func (b *commonBalancer) RemoveTarget(name string) bool {
+	b.mutex.Lock()
+	defer b.mutex.Unlock()
+	for i, t := range b.targets {
+		if t.Name == name {
+			b.targets = append(b.targets[:i], b.targets[i+1:]...)
+			return true
+		}
+	}
+	return false
+}
+
+// Next randomly returns an upstream target.
+func (b *randomBalancer) Next(c echo.Context) *ProxyTarget {
+	if b.random == nil {
+		b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
+	}
+	b.mutex.RLock()
+	defer b.mutex.RUnlock()
+	return b.targets[b.random.Intn(len(b.targets))]
+}
+
+// Next returns an upstream target using round-robin technique.
+func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget {
+	b.i = b.i % uint32(len(b.targets))
+	t := b.targets[b.i]
+	atomic.AddUint32(&b.i, 1)
+	return t
+}
+
+// Proxy returns a Proxy middleware.
+//
+// Proxy middleware forwards the request to upstream server using a configured load balancing technique.
+func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
+	c := DefaultProxyConfig
+	c.Balancer = balancer
+	return ProxyWithConfig(c)
+}
+
+// ProxyWithConfig returns a Proxy middleware with config.
+// See: `Proxy()`
+func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultProxyConfig.Skipper
+	}
+	if config.Balancer == nil {
+		panic("echo: proxy middleware requires balancer")
+	}
+
+	if config.Rewrite != nil {
+		if config.RegexRewrite == nil {
+			config.RegexRewrite = make(map[*regexp.Regexp]string)
+		}
+		for k, v := range rewriteRulesRegex(config.Rewrite) {
+			config.RegexRewrite[k] = v
+		}
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) (err error) {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			res := c.Response()
+			tgt := config.Balancer.Next(c)
+			c.Set(config.ContextKey, tgt)
+
+			if err := rewriteURL(config.RegexRewrite, req); err != nil {
+				return err
+			}
+
+			// Fix header
+			// Basically it's not good practice to unconditionally pass incoming x-real-ip header to upstream.
+			// However, for backward compatibility, legacy behavior is preserved unless you configure Echo#IPExtractor.
+			if req.Header.Get(echo.HeaderXRealIP) == "" || c.Echo().IPExtractor != nil {
+				req.Header.Set(echo.HeaderXRealIP, c.RealIP())
+			}
+			if req.Header.Get(echo.HeaderXForwardedProto) == "" {
+				req.Header.Set(echo.HeaderXForwardedProto, c.Scheme())
+			}
+			if c.IsWebSocket() && req.Header.Get(echo.HeaderXForwardedFor) == "" { // For HTTP, it is automatically set by Go HTTP reverse proxy.
+				req.Header.Set(echo.HeaderXForwardedFor, c.RealIP())
+			}
+
+			// Proxy
+			switch {
+			case c.IsWebSocket():
+				proxyRaw(tgt, c).ServeHTTP(res, req)
+			case req.Header.Get(echo.HeaderAccept) == "text/event-stream":
+			default:
+				proxyHTTP(tgt, c, config).ServeHTTP(res, req)
+			}
+			if e, ok := c.Get("_error").(error); ok {
+				err = e
+			}
+
+			return
+		}
+	}
+}
+
+// StatusCodeContextCanceled is a custom HTTP status code for situations
+// where a client unexpectedly closed the connection to the server.
+// As there is no standard error code for "client closed connection", but
+// various well-known HTTP clients and server implement this HTTP code we use
+// 499 too instead of the more problematic 5xx, which does not allow to detect this situation
+const StatusCodeContextCanceled = 499
+
+func proxyHTTP(tgt *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
+	proxy := httputil.NewSingleHostReverseProxy(tgt.URL)
+	proxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) {
+		desc := tgt.URL.String()
+		if tgt.Name != "" {
+			desc = fmt.Sprintf("%s(%s)", tgt.Name, tgt.URL.String())
+		}
+		// If the client canceled the request (usually by closing the connection), we can report a
+		// client error (4xx) instead of a server error (5xx) to correctly identify the situation.
+		// The Go standard library (at of late 2020) wraps the exported, standard
+		// context.Canceled error with unexported garbage value requiring a substring check, see
+		// https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430
+		if err == context.Canceled || strings.Contains(err.Error(), "operation was canceled") {
+			httpError := echo.NewHTTPError(StatusCodeContextCanceled, fmt.Sprintf("client closed connection: %v", err))
+			httpError.Internal = err
+			c.Set("_error", httpError)
+		} else {
+			httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err))
+			httpError.Internal = err
+			c.Set("_error", httpError)
+		}
+	}
+	proxy.Transport = config.Transport
+	proxy.ModifyResponse = config.ModifyResponse
+	return proxy
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/rate_limiter.go b/vendor/github.com/labstack/echo/v4/middleware/rate_limiter.go
new file mode 100644
index 000000000..0291eb451
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/rate_limiter.go
@@ -0,0 +1,267 @@
+package middleware
+
+import (
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/labstack/echo/v4"
+	"golang.org/x/time/rate"
+)
+
+type (
+	// RateLimiterStore is the interface to be implemented by custom stores.
+	RateLimiterStore interface {
+		// Stores for the rate limiter have to implement the Allow method
+		Allow(identifier string) (bool, error)
+	}
+)
+
+type (
+	// RateLimiterConfig defines the configuration for the rate limiter
+	RateLimiterConfig struct {
+		Skipper    Skipper
+		BeforeFunc BeforeFunc
+		// IdentifierExtractor uses echo.Context to extract the identifier for a visitor
+		IdentifierExtractor Extractor
+		// Store defines a store for the rate limiter
+		Store RateLimiterStore
+		// ErrorHandler provides a handler to be called when IdentifierExtractor returns an error
+		ErrorHandler func(context echo.Context, err error) error
+		// DenyHandler provides a handler to be called when RateLimiter denies access
+		DenyHandler func(context echo.Context, identifier string, err error) error
+	}
+	// Extractor is used to extract data from echo.Context
+	Extractor func(context echo.Context) (string, error)
+)
+
+// errors
+var (
+	// ErrRateLimitExceeded denotes an error raised when rate limit is exceeded
+	ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded")
+	// ErrExtractorError denotes an error raised when extractor function is unsuccessful
+	ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier")
+)
+
+// DefaultRateLimiterConfig defines default values for RateLimiterConfig
+var DefaultRateLimiterConfig = RateLimiterConfig{
+	Skipper: DefaultSkipper,
+	IdentifierExtractor: func(ctx echo.Context) (string, error) {
+		id := ctx.RealIP()
+		return id, nil
+	},
+	ErrorHandler: func(context echo.Context, err error) error {
+		return &echo.HTTPError{
+			Code:     ErrExtractorError.Code,
+			Message:  ErrExtractorError.Message,
+			Internal: err,
+		}
+	},
+	DenyHandler: func(context echo.Context, identifier string, err error) error {
+		return &echo.HTTPError{
+			Code:     ErrRateLimitExceeded.Code,
+			Message:  ErrRateLimitExceeded.Message,
+			Internal: err,
+		}
+	},
+}
+
+/*
+RateLimiter returns a rate limiting middleware
+
+	e := echo.New()
+
+	limiterStore := middleware.NewRateLimiterMemoryStore(20)
+
+	e.GET("/rate-limited", func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	}, RateLimiter(limiterStore))
+*/
+func RateLimiter(store RateLimiterStore) echo.MiddlewareFunc {
+	config := DefaultRateLimiterConfig
+	config.Store = store
+
+	return RateLimiterWithConfig(config)
+}
+
+/*
+RateLimiterWithConfig returns a rate limiting middleware
+
+	e := echo.New()
+
+	config := middleware.RateLimiterConfig{
+		Skipper: DefaultSkipper,
+		Store: middleware.NewRateLimiterMemoryStore(
+			middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute}
+		)
+		IdentifierExtractor: func(ctx echo.Context) (string, error) {
+			id := ctx.RealIP()
+			return id, nil
+		},
+		ErrorHandler: func(context echo.Context, err error) error {
+			return context.JSON(http.StatusTooManyRequests, nil)
+		},
+		DenyHandler: func(context echo.Context, identifier string) error {
+			return context.JSON(http.StatusForbidden, nil)
+		},
+	}
+
+	e.GET("/rate-limited", func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	}, middleware.RateLimiterWithConfig(config))
+*/
+func RateLimiterWithConfig(config RateLimiterConfig) echo.MiddlewareFunc {
+	if config.Skipper == nil {
+		config.Skipper = DefaultRateLimiterConfig.Skipper
+	}
+	if config.IdentifierExtractor == nil {
+		config.IdentifierExtractor = DefaultRateLimiterConfig.IdentifierExtractor
+	}
+	if config.ErrorHandler == nil {
+		config.ErrorHandler = DefaultRateLimiterConfig.ErrorHandler
+	}
+	if config.DenyHandler == nil {
+		config.DenyHandler = DefaultRateLimiterConfig.DenyHandler
+	}
+	if config.Store == nil {
+		panic("Store configuration must be provided")
+	}
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+			if config.BeforeFunc != nil {
+				config.BeforeFunc(c)
+			}
+
+			identifier, err := config.IdentifierExtractor(c)
+			if err != nil {
+				c.Error(config.ErrorHandler(c, err))
+				return nil
+			}
+
+			if allow, err := config.Store.Allow(identifier); !allow {
+				c.Error(config.DenyHandler(c, identifier, err))
+				return nil
+			}
+			return next(c)
+		}
+	}
+}
+
+type (
+	// RateLimiterMemoryStore is the built-in store implementation for RateLimiter
+	RateLimiterMemoryStore struct {
+		visitors    map[string]*Visitor
+		mutex       sync.Mutex
+		rate        rate.Limit
+		burst       int
+		expiresIn   time.Duration
+		lastCleanup time.Time
+	}
+	// Visitor signifies a unique user's limiter details
+	Visitor struct {
+		*rate.Limiter
+		lastSeen time.Time
+	}
+)
+
+/*
+NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with
+the provided rate (as req/s). The provided rate less than 1 will be treated as zero.
+Burst and ExpiresIn will be set to default values.
+
+Example (with 20 requests/sec):
+
+	limiterStore := middleware.NewRateLimiterMemoryStore(20)
+
+*/
+func NewRateLimiterMemoryStore(rate rate.Limit) (store *RateLimiterMemoryStore) {
+	return NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
+		Rate: rate,
+	})
+}
+
+/*
+NewRateLimiterMemoryStoreWithConfig returns an instance of RateLimiterMemoryStore
+with the provided configuration. Rate must be provided. Burst will be set to the value of
+the configured rate if not provided or set to 0.
+
+The build-in memory store is usually capable for modest loads. For higher loads other
+store implementations should be considered.
+
+Characteristics:
+* Concurrency above 100 parallel requests may causes measurable lock contention
+* A high number of different IP addresses (above 16000) may be impacted by the internally used Go map
+* A high number of requests from a single IP address may cause lock contention
+
+Example:
+
+	limiterStore := middleware.NewRateLimiterMemoryStoreWithConfig(
+		middleware.RateLimiterMemoryStoreConfig{Rate: 50, Burst: 200, ExpiresIn: 5 * time.Minutes},
+	)
+*/
+func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (store *RateLimiterMemoryStore) {
+	store = &RateLimiterMemoryStore{}
+
+	store.rate = config.Rate
+	store.burst = config.Burst
+	store.expiresIn = config.ExpiresIn
+	if config.ExpiresIn == 0 {
+		store.expiresIn = DefaultRateLimiterMemoryStoreConfig.ExpiresIn
+	}
+	if config.Burst == 0 {
+		store.burst = int(config.Rate)
+	}
+	store.visitors = make(map[string]*Visitor)
+	store.lastCleanup = now()
+	return
+}
+
+// RateLimiterMemoryStoreConfig represents configuration for RateLimiterMemoryStore
+type RateLimiterMemoryStoreConfig struct {
+	Rate      rate.Limit    // Rate of requests allowed to pass as req/s
+	Burst     int           // Burst additionally allows a number of requests to pass when rate limit is reached
+	ExpiresIn time.Duration // ExpiresIn is the duration after that a rate limiter is cleaned up
+}
+
+// DefaultRateLimiterMemoryStoreConfig provides default configuration values for RateLimiterMemoryStore
+var DefaultRateLimiterMemoryStoreConfig = RateLimiterMemoryStoreConfig{
+	ExpiresIn: 3 * time.Minute,
+}
+
+// Allow implements RateLimiterStore.Allow
+func (store *RateLimiterMemoryStore) Allow(identifier string) (bool, error) {
+	store.mutex.Lock()
+	limiter, exists := store.visitors[identifier]
+	if !exists {
+		limiter = new(Visitor)
+		limiter.Limiter = rate.NewLimiter(store.rate, store.burst)
+		store.visitors[identifier] = limiter
+	}
+	limiter.lastSeen = now()
+	if now().Sub(store.lastCleanup) > store.expiresIn {
+		store.cleanupStaleVisitors()
+	}
+	store.mutex.Unlock()
+	return limiter.AllowN(now(), 1), nil
+}
+
+/*
+cleanupStaleVisitors helps manage the size of the visitors map by removing stale records
+of users who haven't visited again after the configured expiry time has elapsed
+*/
+func (store *RateLimiterMemoryStore) cleanupStaleVisitors() {
+	for id, visitor := range store.visitors {
+		if now().Sub(visitor.lastSeen) > store.expiresIn {
+			delete(store.visitors, id)
+		}
+	}
+	store.lastCleanup = now()
+}
+
+/*
+actual time method which is mocked in test file
+*/
+var now = time.Now
diff --git a/vendor/github.com/labstack/echo/v4/middleware/recover.go b/vendor/github.com/labstack/echo/v4/middleware/recover.go
new file mode 100644
index 000000000..0dbe740da
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/recover.go
@@ -0,0 +1,101 @@
+package middleware
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/log"
+)
+
+type (
+	// RecoverConfig defines the config for Recover middleware.
+	RecoverConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Size of the stack to be printed.
+		// Optional. Default value 4KB.
+		StackSize int `yaml:"stack_size"`
+
+		// DisableStackAll disables formatting stack traces of all other goroutines
+		// into buffer after the trace for the current goroutine.
+		// Optional. Default value false.
+		DisableStackAll bool `yaml:"disable_stack_all"`
+
+		// DisablePrintStack disables printing stack trace.
+		// Optional. Default value as false.
+		DisablePrintStack bool `yaml:"disable_print_stack"`
+
+		// LogLevel is log level to printing stack trace.
+		// Optional. Default value 0 (Print).
+		LogLevel log.Lvl
+	}
+)
+
+var (
+	// DefaultRecoverConfig is the default Recover middleware config.
+	DefaultRecoverConfig = RecoverConfig{
+		Skipper:           DefaultSkipper,
+		StackSize:         4 << 10, // 4 KB
+		DisableStackAll:   false,
+		DisablePrintStack: false,
+		LogLevel:          0,
+	}
+)
+
+// Recover returns a middleware which recovers from panics anywhere in the chain
+// and handles the control to the centralized HTTPErrorHandler.
+func Recover() echo.MiddlewareFunc {
+	return RecoverWithConfig(DefaultRecoverConfig)
+}
+
+// RecoverWithConfig returns a Recover middleware with config.
+// See: `Recover()`.
+func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultRecoverConfig.Skipper
+	}
+	if config.StackSize == 0 {
+		config.StackSize = DefaultRecoverConfig.StackSize
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			defer func() {
+				if r := recover(); r != nil {
+					err, ok := r.(error)
+					if !ok {
+						err = fmt.Errorf("%v", r)
+					}
+					stack := make([]byte, config.StackSize)
+					length := runtime.Stack(stack, !config.DisableStackAll)
+					if !config.DisablePrintStack {
+						msg := fmt.Sprintf("[PANIC RECOVER] %v %s\n", err, stack[:length])
+						switch config.LogLevel {
+						case log.DEBUG:
+							c.Logger().Debug(msg)
+						case log.INFO:
+							c.Logger().Info(msg)
+						case log.WARN:
+							c.Logger().Warn(msg)
+						case log.ERROR:
+							c.Logger().Error(msg)
+						case log.OFF:
+							// None.
+						default:
+							c.Logger().Print(msg)
+						}
+					}
+					c.Error(err)
+				}
+			}()
+			return next(c)
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/redirect.go b/vendor/github.com/labstack/echo/v4/middleware/redirect.go
new file mode 100644
index 000000000..13877db38
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/redirect.go
@@ -0,0 +1,152 @@
+package middleware
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/labstack/echo/v4"
+)
+
+// RedirectConfig defines the config for Redirect middleware.
+type RedirectConfig struct {
+	// Skipper defines a function to skip middleware.
+	Skipper
+
+	// Status code to be used when redirecting the request.
+	// Optional. Default value http.StatusMovedPermanently.
+	Code int `yaml:"code"`
+}
+
+// redirectLogic represents a function that given a scheme, host and uri
+// can both: 1) determine if redirect is needed (will set ok accordingly) and
+// 2) return the appropriate redirect url.
+type redirectLogic func(scheme, host, uri string) (ok bool, url string)
+
+const www = "www."
+
+// DefaultRedirectConfig is the default Redirect middleware config.
+var DefaultRedirectConfig = RedirectConfig{
+	Skipper: DefaultSkipper,
+	Code:    http.StatusMovedPermanently,
+}
+
+// HTTPSRedirect redirects http requests to https.
+// For example, http://labstack.com will be redirect to https://labstack.com.
+//
+// Usage `Echo#Pre(HTTPSRedirect())`
+func HTTPSRedirect() echo.MiddlewareFunc {
+	return HTTPSRedirectWithConfig(DefaultRedirectConfig)
+}
+
+// HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config.
+// See `HTTPSRedirect()`.
+func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if scheme != "https" {
+			return true, "https://" + host + uri
+		}
+		return false, ""
+	})
+}
+
+// HTTPSWWWRedirect redirects http requests to https www.
+// For example, http://labstack.com will be redirect to https://www.labstack.com.
+//
+// Usage `Echo#Pre(HTTPSWWWRedirect())`
+func HTTPSWWWRedirect() echo.MiddlewareFunc {
+	return HTTPSWWWRedirectWithConfig(DefaultRedirectConfig)
+}
+
+// HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
+// See `HTTPSWWWRedirect()`.
+func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if scheme != "https" && !strings.HasPrefix(host, www) {
+			return true, "https://www." + host + uri
+		}
+		return false, ""
+	})
+}
+
+// HTTPSNonWWWRedirect redirects http requests to https non www.
+// For example, http://www.labstack.com will be redirect to https://labstack.com.
+//
+// Usage `Echo#Pre(HTTPSNonWWWRedirect())`
+func HTTPSNonWWWRedirect() echo.MiddlewareFunc {
+	return HTTPSNonWWWRedirectWithConfig(DefaultRedirectConfig)
+}
+
+// HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
+// See `HTTPSNonWWWRedirect()`.
+func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
+	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
+		if scheme != "https" {
+			host = strings.TrimPrefix(host, www)
+			return true, "https://" + host + uri
+		}
+		return false, ""
+	})
+}
+
+// WWWRedirect redirects non www requests to www.
+// For example, http://labstack.com will be redirect to http://www.labstack.com.
+//
+// Usage `Echo#Pre(WWWRedirect())`
+func WWWRedirect() echo.MiddlewareFunc {
+	return WWWRedirectWithConfig(DefaultRedirectConfig)
+}
+
+// WWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
+// See `WWWRedirect()`.
+func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if !strings.HasPrefix(host, www) {
+			return true, scheme + "://www." + host + uri
+		}
+		return false, ""
+	})
+}
+
+// NonWWWRedirect redirects www requests to non www.
+// For example, http://www.labstack.com will be redirect to http://labstack.com.
+//
+// Usage `Echo#Pre(NonWWWRedirect())`
+func NonWWWRedirect() echo.MiddlewareFunc {
+	return NonWWWRedirectWithConfig(DefaultRedirectConfig)
+}
+
+// NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
+// See `NonWWWRedirect()`.
+func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if strings.HasPrefix(host, www) {
+			return true, scheme + "://" + host[4:] + uri
+		}
+		return false, ""
+	})
+}
+
+func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc {
+	if config.Skipper == nil {
+		config.Skipper = DefaultRedirectConfig.Skipper
+	}
+	if config.Code == 0 {
+		config.Code = DefaultRedirectConfig.Code
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req, scheme := c.Request(), c.Scheme()
+			host := req.Host
+			if ok, url := cb(scheme, host, req.RequestURI); ok {
+				return c.Redirect(config.Code, url)
+			}
+
+			return next(c)
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/request_id.go b/vendor/github.com/labstack/echo/v4/middleware/request_id.go
new file mode 100644
index 000000000..b0baeeb2d
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/request_id.go
@@ -0,0 +1,70 @@
+package middleware
+
+import (
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/random"
+)
+
+type (
+	// RequestIDConfig defines the config for RequestID middleware.
+	RequestIDConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Generator defines a function to generate an ID.
+		// Optional. Default value random.String(32).
+		Generator func() string
+
+		// RequestIDHandler defines a function which is executed for a request id.
+		RequestIDHandler func(echo.Context, string)
+	}
+)
+
+var (
+	// DefaultRequestIDConfig is the default RequestID middleware config.
+	DefaultRequestIDConfig = RequestIDConfig{
+		Skipper:   DefaultSkipper,
+		Generator: generator,
+	}
+)
+
+// RequestID returns a X-Request-ID middleware.
+func RequestID() echo.MiddlewareFunc {
+	return RequestIDWithConfig(DefaultRequestIDConfig)
+}
+
+// RequestIDWithConfig returns a X-Request-ID middleware with config.
+func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultRequestIDConfig.Skipper
+	}
+	if config.Generator == nil {
+		config.Generator = generator
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			res := c.Response()
+			rid := req.Header.Get(echo.HeaderXRequestID)
+			if rid == "" {
+				rid = config.Generator()
+			}
+			res.Header().Set(echo.HeaderXRequestID, rid)
+			if config.RequestIDHandler != nil {
+				config.RequestIDHandler(c, rid)
+			}
+
+			return next(c)
+		}
+	}
+}
+
+func generator() string {
+	return random.String(32)
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/rewrite.go b/vendor/github.com/labstack/echo/v4/middleware/rewrite.go
new file mode 100644
index 000000000..e5b0a6b56
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/rewrite.go
@@ -0,0 +1,81 @@
+package middleware
+
+import (
+	"regexp"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// RewriteConfig defines the config for Rewrite middleware.
+	RewriteConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Rules defines the URL path rewrite rules. The values captured in asterisk can be
+		// retrieved by index e.g. $1, $2 and so on.
+		// Example:
+		// "/old":              "/new",
+		// "/api/*":            "/$1",
+		// "/js/*":             "/public/javascripts/$1",
+		// "/users/*/orders/*": "/user/$1/order/$2",
+		// Required.
+		Rules map[string]string `yaml:"rules"`
+
+		// RegexRules defines the URL path rewrite rules using regexp.Rexexp with captures
+		// Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.
+		// Example:
+		// "^/old/[0.9]+/":     "/new",
+		// "^/api/.+?/(.*)":     "/v2/$1",
+		RegexRules map[*regexp.Regexp]string `yaml:"regex_rules"`
+	}
+)
+
+var (
+	// DefaultRewriteConfig is the default Rewrite middleware config.
+	DefaultRewriteConfig = RewriteConfig{
+		Skipper: DefaultSkipper,
+	}
+)
+
+// Rewrite returns a Rewrite middleware.
+//
+// Rewrite middleware rewrites the URL path based on the provided rules.
+func Rewrite(rules map[string]string) echo.MiddlewareFunc {
+	c := DefaultRewriteConfig
+	c.Rules = rules
+	return RewriteWithConfig(c)
+}
+
+// RewriteWithConfig returns a Rewrite middleware with config.
+// See: `Rewrite()`.
+func RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Rules == nil && config.RegexRules == nil {
+		panic("echo: rewrite middleware requires url path rewrite rules or regex rules")
+	}
+
+	if config.Skipper == nil {
+		config.Skipper = DefaultBodyDumpConfig.Skipper
+	}
+
+	if config.RegexRules == nil {
+		config.RegexRules = make(map[*regexp.Regexp]string)
+	}
+	for k, v := range rewriteRulesRegex(config.Rules) {
+		config.RegexRules[k] = v
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) (err error) {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			if err := rewriteURL(config.RegexRules, c.Request()); err != nil {
+				return err
+			}
+			return next(c)
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/secure.go b/vendor/github.com/labstack/echo/v4/middleware/secure.go
new file mode 100644
index 000000000..6c4051723
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/secure.go
@@ -0,0 +1,145 @@
+package middleware
+
+import (
+	"fmt"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// SecureConfig defines the config for Secure middleware.
+	SecureConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// XSSProtection provides protection against cross-site scripting attack (XSS)
+		// by setting the `X-XSS-Protection` header.
+		// Optional. Default value "1; mode=block".
+		XSSProtection string `yaml:"xss_protection"`
+
+		// ContentTypeNosniff provides protection against overriding Content-Type
+		// header by setting the `X-Content-Type-Options` header.
+		// Optional. Default value "nosniff".
+		ContentTypeNosniff string `yaml:"content_type_nosniff"`
+
+		// XFrameOptions can be used to indicate whether or not a browser should
+		// be allowed to render a page in a <frame>, <iframe> or <object> .
+		// Sites can use this to avoid clickjacking attacks, by ensuring that their
+		// content is not embedded into other sites.provides protection against
+		// clickjacking.
+		// Optional. Default value "SAMEORIGIN".
+		// Possible values:
+		// - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself.
+		// - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so.
+		// - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin.
+		XFrameOptions string `yaml:"x_frame_options"`
+
+		// HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how
+		// long (in seconds) browsers should remember that this site is only to
+		// be accessed using HTTPS. This reduces your exposure to some SSL-stripping
+		// man-in-the-middle (MITM) attacks.
+		// Optional. Default value 0.
+		HSTSMaxAge int `yaml:"hsts_max_age"`
+
+		// HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security`
+		// header, excluding all subdomains from security policy. It has no effect
+		// unless HSTSMaxAge is set to a non-zero value.
+		// Optional. Default value false.
+		HSTSExcludeSubdomains bool `yaml:"hsts_exclude_subdomains"`
+
+		// ContentSecurityPolicy sets the `Content-Security-Policy` header providing
+		// security against cross-site scripting (XSS), clickjacking and other code
+		// injection attacks resulting from execution of malicious content in the
+		// trusted web page context.
+		// Optional. Default value "".
+		ContentSecurityPolicy string `yaml:"content_security_policy"`
+
+		// CSPReportOnly would use the `Content-Security-Policy-Report-Only` header instead
+		// of the `Content-Security-Policy` header. This allows iterative updates of the
+		// content security policy by only reporting the violations that would
+		// have occurred instead of blocking the resource.
+		// Optional. Default value false.
+		CSPReportOnly bool `yaml:"csp_report_only"`
+
+		// HSTSPreloadEnabled will add the preload tag in the `Strict Transport Security`
+		// header, which enables the domain to be included in the HSTS preload list
+		// maintained by Chrome (and used by Firefox and Safari): https://hstspreload.org/
+		// Optional.  Default value false.
+		HSTSPreloadEnabled bool `yaml:"hsts_preload_enabled"`
+
+		// ReferrerPolicy sets the `Referrer-Policy` header providing security against
+		// leaking potentially sensitive request paths to third parties.
+		// Optional. Default value "".
+		ReferrerPolicy string `yaml:"referrer_policy"`
+	}
+)
+
+var (
+	// DefaultSecureConfig is the default Secure middleware config.
+	DefaultSecureConfig = SecureConfig{
+		Skipper:            DefaultSkipper,
+		XSSProtection:      "1; mode=block",
+		ContentTypeNosniff: "nosniff",
+		XFrameOptions:      "SAMEORIGIN",
+		HSTSPreloadEnabled: false,
+	}
+)
+
+// Secure returns a Secure middleware.
+// Secure middleware provides protection against cross-site scripting (XSS) attack,
+// content type sniffing, clickjacking, insecure connection and other code injection
+// attacks.
+func Secure() echo.MiddlewareFunc {
+	return SecureWithConfig(DefaultSecureConfig)
+}
+
+// SecureWithConfig returns a Secure middleware with config.
+// See: `Secure()`.
+func SecureWithConfig(config SecureConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultSecureConfig.Skipper
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			res := c.Response()
+
+			if config.XSSProtection != "" {
+				res.Header().Set(echo.HeaderXXSSProtection, config.XSSProtection)
+			}
+			if config.ContentTypeNosniff != "" {
+				res.Header().Set(echo.HeaderXContentTypeOptions, config.ContentTypeNosniff)
+			}
+			if config.XFrameOptions != "" {
+				res.Header().Set(echo.HeaderXFrameOptions, config.XFrameOptions)
+			}
+			if (c.IsTLS() || (req.Header.Get(echo.HeaderXForwardedProto) == "https")) && config.HSTSMaxAge != 0 {
+				subdomains := ""
+				if !config.HSTSExcludeSubdomains {
+					subdomains = "; includeSubdomains"
+				}
+				if config.HSTSPreloadEnabled {
+					subdomains = fmt.Sprintf("%s; preload", subdomains)
+				}
+				res.Header().Set(echo.HeaderStrictTransportSecurity, fmt.Sprintf("max-age=%d%s", config.HSTSMaxAge, subdomains))
+			}
+			if config.ContentSecurityPolicy != "" {
+				if config.CSPReportOnly {
+					res.Header().Set(echo.HeaderContentSecurityPolicyReportOnly, config.ContentSecurityPolicy)
+				} else {
+					res.Header().Set(echo.HeaderContentSecurityPolicy, config.ContentSecurityPolicy)
+				}
+			}
+			if config.ReferrerPolicy != "" {
+				res.Header().Set(echo.HeaderReferrerPolicy, config.ReferrerPolicy)
+			}
+			return next(c)
+		}
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/slash.go b/vendor/github.com/labstack/echo/v4/middleware/slash.go
new file mode 100644
index 000000000..4188675b0
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/slash.go
@@ -0,0 +1,130 @@
+package middleware
+
+import (
+	"strings"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// TrailingSlashConfig defines the config for TrailingSlash middleware.
+	TrailingSlashConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Status code to be used when redirecting the request.
+		// Optional, but when provided the request is redirected using this code.
+		RedirectCode int `yaml:"redirect_code"`
+	}
+)
+
+var (
+	// DefaultTrailingSlashConfig is the default TrailingSlash middleware config.
+	DefaultTrailingSlashConfig = TrailingSlashConfig{
+		Skipper: DefaultSkipper,
+	}
+)
+
+// AddTrailingSlash returns a root level (before router) middleware which adds a
+// trailing slash to the request `URL#Path`.
+//
+// Usage `Echo#Pre(AddTrailingSlash())`
+func AddTrailingSlash() echo.MiddlewareFunc {
+	return AddTrailingSlashWithConfig(DefaultTrailingSlashConfig)
+}
+
+// AddTrailingSlashWithConfig returns a AddTrailingSlash middleware with config.
+// See `AddTrailingSlash()`.
+func AddTrailingSlashWithConfig(config TrailingSlashConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultTrailingSlashConfig.Skipper
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			url := req.URL
+			path := url.Path
+			qs := c.QueryString()
+			if !strings.HasSuffix(path, "/") {
+				path += "/"
+				uri := path
+				if qs != "" {
+					uri += "?" + qs
+				}
+
+				// Redirect
+				if config.RedirectCode != 0 {
+					return c.Redirect(config.RedirectCode, sanitizeURI(uri))
+				}
+
+				// Forward
+				req.RequestURI = uri
+				url.Path = path
+			}
+			return next(c)
+		}
+	}
+}
+
+// RemoveTrailingSlash returns a root level (before router) middleware which removes
+// a trailing slash from the request URI.
+//
+// Usage `Echo#Pre(RemoveTrailingSlash())`
+func RemoveTrailingSlash() echo.MiddlewareFunc {
+	return RemoveTrailingSlashWithConfig(TrailingSlashConfig{})
+}
+
+// RemoveTrailingSlashWithConfig returns a RemoveTrailingSlash middleware with config.
+// See `RemoveTrailingSlash()`.
+func RemoveTrailingSlashWithConfig(config TrailingSlashConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultTrailingSlashConfig.Skipper
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			url := req.URL
+			path := url.Path
+			qs := c.QueryString()
+			l := len(path) - 1
+			if l > 0 && strings.HasSuffix(path, "/") {
+				path = path[:l]
+				uri := path
+				if qs != "" {
+					uri += "?" + qs
+				}
+
+				// Redirect
+				if config.RedirectCode != 0 {
+					return c.Redirect(config.RedirectCode, sanitizeURI(uri))
+				}
+
+				// Forward
+				req.RequestURI = uri
+				url.Path = path
+			}
+			return next(c)
+		}
+	}
+}
+
+func sanitizeURI(uri string) string {
+	// double slash `\\`, `//` or even `\/` is absolute uri for browsers and by redirecting request to that uri
+	// we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash
+	if len(uri) > 1 && (uri[0] == '\\' || uri[0] == '/') && (uri[1] == '\\' || uri[1] == '/') {
+		uri = "/" + strings.TrimLeft(uri, `/\`)
+	}
+	return uri
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/static.go b/vendor/github.com/labstack/echo/v4/middleware/static.go
new file mode 100644
index 000000000..0106f7ce2
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/static.go
@@ -0,0 +1,276 @@
+package middleware
+
+import (
+	"fmt"
+	"html/template"
+	"net/http"
+	"net/url"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/bytes"
+)
+
+type (
+	// StaticConfig defines the config for Static middleware.
+	StaticConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// Root directory from where the static content is served.
+		// Required.
+		Root string `yaml:"root"`
+
+		// Index file for serving a directory.
+		// Optional. Default value "index.html".
+		Index string `yaml:"index"`
+
+		// Enable HTML5 mode by forwarding all not-found requests to root so that
+		// SPA (single-page application) can handle the routing.
+		// Optional. Default value false.
+		HTML5 bool `yaml:"html5"`
+
+		// Enable directory browsing.
+		// Optional. Default value false.
+		Browse bool `yaml:"browse"`
+
+		// Enable ignoring of the base of the URL path.
+		// Example: when assigning a static middleware to a non root path group,
+		// the filesystem path is not doubled
+		// Optional. Default value false.
+		IgnoreBase bool `yaml:"ignoreBase"`
+
+		// Filesystem provides access to the static content.
+		// Optional. Defaults to http.Dir(config.Root)
+		Filesystem http.FileSystem `yaml:"-"`
+	}
+)
+
+const html = `
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta http-equiv="X-UA-Compatible" content="ie=edge">
+  <title>{{ .Name }}</title>
+  <style>
+    body {
+			font-family: Menlo, Consolas, monospace;
+			padding: 48px;
+		}
+		header {
+			padding: 4px 16px;
+			font-size: 24px;
+		}
+    ul {
+			list-style-type: none;
+			margin: 0;
+    	padding: 20px 0 0 0;
+			display: flex;
+			flex-wrap: wrap;
+    }
+    li {
+			width: 300px;
+			padding: 16px;
+		}
+		li a {
+			display: block;
+			overflow: hidden;
+			white-space: nowrap;
+			text-overflow: ellipsis;
+			text-decoration: none;
+			transition: opacity 0.25s;
+		}
+		li span {
+			color: #707070;
+			font-size: 12px;
+		}
+		li a:hover {
+			opacity: 0.50;
+		}
+		.dir {
+			color: #E91E63;
+		}
+		.file {
+			color: #673AB7;
+		}
+  </style>
+</head>
+<body>
+	<header>
+		{{ .Name }}
+	</header>
+	<ul>
+		{{ range .Files }}
+		<li>
+		{{ if .Dir }}
+			{{ $name := print .Name "/" }}
+			<a class="dir" href="{{ $name }}">{{ $name }}</a>
+			{{ else }}
+			<a class="file" href="{{ .Name }}">{{ .Name }}</a>
+			<span>{{ .Size }}</span>
+		{{ end }}
+		</li>
+		{{ end }}
+  </ul>
+</body>
+</html>
+`
+
+var (
+	// DefaultStaticConfig is the default Static middleware config.
+	DefaultStaticConfig = StaticConfig{
+		Skipper: DefaultSkipper,
+		Index:   "index.html",
+	}
+)
+
+// Static returns a Static middleware to serves static content from the provided
+// root directory.
+func Static(root string) echo.MiddlewareFunc {
+	c := DefaultStaticConfig
+	c.Root = root
+	return StaticWithConfig(c)
+}
+
+// StaticWithConfig returns a Static middleware with config.
+// See `Static()`.
+func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Root == "" {
+		config.Root = "." // For security we want to restrict to CWD.
+	}
+	if config.Skipper == nil {
+		config.Skipper = DefaultStaticConfig.Skipper
+	}
+	if config.Index == "" {
+		config.Index = DefaultStaticConfig.Index
+	}
+	if config.Filesystem == nil {
+		config.Filesystem = http.Dir(config.Root)
+		config.Root = "."
+	}
+
+	// Index template
+	t, err := template.New("index").Parse(html)
+	if err != nil {
+		panic(fmt.Sprintf("echo: %v", err))
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) (err error) {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			p := c.Request().URL.Path
+			if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
+				p = c.Param("*")
+			}
+			p, err = url.PathUnescape(p)
+			if err != nil {
+				return
+			}
+			name := filepath.Join(config.Root, filepath.Clean("/"+p)) // "/"+ for security
+
+			if config.IgnoreBase {
+				routePath := path.Base(strings.TrimRight(c.Path(), "/*"))
+				baseURLPath := path.Base(p)
+				if baseURLPath == routePath {
+					i := strings.LastIndex(name, routePath)
+					name = name[:i] + strings.Replace(name[i:], routePath, "", 1)
+				}
+			}
+
+			file, err := openFile(config.Filesystem, name)
+			if err != nil {
+				if !os.IsNotExist(err) {
+					return err
+				}
+
+				if err = next(c); err == nil {
+					return err
+				}
+
+				he, ok := err.(*echo.HTTPError)
+				if !(ok && config.HTML5 && he.Code == http.StatusNotFound) {
+					return err
+				}
+
+				file, err = openFile(config.Filesystem, filepath.Join(config.Root, config.Index))
+				if err != nil {
+					return err
+				}
+			}
+
+			defer file.Close()
+
+			info, err := file.Stat()
+			if err != nil {
+				return err
+			}
+
+			if info.IsDir() {
+				index, err := openFile(config.Filesystem, filepath.Join(name, config.Index))
+				if err != nil {
+					if config.Browse {
+						return listDir(t, name, file, c.Response())
+					}
+
+					if os.IsNotExist(err) {
+						return next(c)
+					}
+				}
+
+				defer index.Close()
+
+				info, err = index.Stat()
+				if err != nil {
+					return err
+				}
+
+				return serveFile(c, index, info)
+			}
+
+			return serveFile(c, file, info)
+		}
+	}
+}
+
+func openFile(fs http.FileSystem, name string) (http.File, error) {
+	pathWithSlashes := filepath.ToSlash(name)
+	return fs.Open(pathWithSlashes)
+}
+
+func serveFile(c echo.Context, file http.File, info os.FileInfo) error {
+	http.ServeContent(c.Response(), c.Request(), info.Name(), info.ModTime(), file)
+	return nil
+}
+
+func listDir(t *template.Template, name string, dir http.File, res *echo.Response) (err error) {
+	files, err := dir.Readdir(-1)
+	if err != nil {
+		return
+	}
+
+	// Create directory index
+	res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
+	data := struct {
+		Name  string
+		Files []interface{}
+	}{
+		Name: name,
+	}
+	for _, f := range files {
+		data.Files = append(data.Files, struct {
+			Name string
+			Dir  bool
+			Size string
+		}{f.Name(), f.IsDir(), bytes.Format(f.Size())})
+	}
+	return t.Execute(res, data)
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/timeout.go b/vendor/github.com/labstack/echo/v4/middleware/timeout.go
new file mode 100644
index 000000000..731136541
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/timeout.go
@@ -0,0 +1,128 @@
+package middleware
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/labstack/echo/v4"
+)
+
+type (
+	// TimeoutConfig defines the config for Timeout middleware.
+	TimeoutConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+
+		// ErrorMessage is written to response on timeout in addition to http.StatusServiceUnavailable (503) status code
+		// It can be used to define a custom timeout error message
+		ErrorMessage string
+
+		// OnTimeoutRouteErrorHandler is an error handler that is executed for error that was returned from wrapped route after
+		// request timeouted and we already had sent the error code (503) and message response to the client.
+		// NB: do not write headers/body inside this handler. The response has already been sent to the client and response writer
+		// will not accept anything no more. If you want to know what actual route middleware timeouted use `c.Path()`
+		OnTimeoutRouteErrorHandler func(err error, c echo.Context)
+
+		// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
+		// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
+		// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
+		// difference over 500microseconds (0.5millisecond) response seems to be reliable
+		Timeout time.Duration
+	}
+)
+
+var (
+	// DefaultTimeoutConfig is the default Timeout middleware config.
+	DefaultTimeoutConfig = TimeoutConfig{
+		Skipper:      DefaultSkipper,
+		Timeout:      0,
+		ErrorMessage: "",
+	}
+)
+
+// Timeout returns a middleware which returns error (503 Service Unavailable error) to client immediately when handler
+// call runs for longer than its time limit. NB: timeout does not stop handler execution.
+func Timeout() echo.MiddlewareFunc {
+	return TimeoutWithConfig(DefaultTimeoutConfig)
+}
+
+// TimeoutWithConfig returns a Timeout middleware with config.
+// See: `Timeout()`.
+func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultTimeoutConfig.Skipper
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) || config.Timeout == 0 {
+				return next(c)
+			}
+
+			handlerWrapper := echoHandlerFuncWrapper{
+				ctx:        c,
+				handler:    next,
+				errChan:    make(chan error, 1),
+				errHandler: config.OnTimeoutRouteErrorHandler,
+			}
+			handler := http.TimeoutHandler(handlerWrapper, config.Timeout, config.ErrorMessage)
+			handler.ServeHTTP(c.Response().Writer, c.Request())
+
+			select {
+			case err := <-handlerWrapper.errChan:
+				return err
+			default:
+				return nil
+			}
+		}
+	}
+}
+
+type echoHandlerFuncWrapper struct {
+	ctx        echo.Context
+	handler    echo.HandlerFunc
+	errHandler func(err error, c echo.Context)
+	errChan    chan error
+}
+
+func (t echoHandlerFuncWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	// replace echo.Context Request with the one provided by TimeoutHandler to let later middlewares/handler on the chain
+	// handle properly it's cancellation
+	t.ctx.SetRequest(r)
+
+	// replace writer with TimeoutHandler custom one. This will guarantee that
+	// `writes by h to its ResponseWriter will return ErrHandlerTimeout.`
+	originalWriter := t.ctx.Response().Writer
+	t.ctx.Response().Writer = rw
+
+	// in case of panic we restore original writer and call panic again
+	// so it could be handled with global middleware Recover()
+	defer func() {
+		if err := recover(); err != nil {
+			t.ctx.Response().Writer = originalWriter
+			panic(err)
+		}
+	}()
+
+	err := t.handler(t.ctx)
+	if ctxErr := r.Context().Err(); ctxErr == context.DeadlineExceeded {
+		if err != nil && t.errHandler != nil {
+			t.errHandler(err, t.ctx)
+		}
+		return // on timeout we can not send handler error to client because `http.TimeoutHandler` has already sent headers
+	}
+	// we restore original writer only for cases we did not timeout. On timeout we have already sent response to client
+	// and should not anymore send additional headers/data
+	// so on timeout writer stays what http.TimeoutHandler uses and prevents writing headers/body
+	t.ctx.Response().Writer = originalWriter
+	if err != nil {
+		// call global error handler to write error to the client. This is needed or `http.TimeoutHandler` will send status code by itself
+		// and after that our tries to write status code will not work anymore
+		t.ctx.Error(err)
+		// we pass error from handler to middlewares up in handler chain to act on it if needed. But this means that
+		// global error handler is probably be called twice as `t.ctx.Error` already does that.
+		t.errChan <- err
+	}
+}
diff --git a/vendor/github.com/labstack/echo/v4/middleware/util.go b/vendor/github.com/labstack/echo/v4/middleware/util.go
new file mode 100644
index 000000000..ab951a0e9
--- /dev/null
+++ b/vendor/github.com/labstack/echo/v4/middleware/util.go
@@ -0,0 +1,54 @@
+package middleware
+
+import (
+	"strings"
+)
+
+func matchScheme(domain, pattern string) bool {
+	didx := strings.Index(domain, ":")
+	pidx := strings.Index(pattern, ":")
+	return didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx]
+}
+
+// matchSubdomain compares authority with wildcard
+func matchSubdomain(domain, pattern string) bool {
+	if !matchScheme(domain, pattern) {
+		return false
+	}
+	didx := strings.Index(domain, "://")
+	pidx := strings.Index(pattern, "://")
+	if didx == -1 || pidx == -1 {
+		return false
+	}
+	domAuth := domain[didx+3:]
+	// to avoid long loop by invalid long domain
+	if len(domAuth) > 253 {
+		return false
+	}
+	patAuth := pattern[pidx+3:]
+
+	domComp := strings.Split(domAuth, ".")
+	patComp := strings.Split(patAuth, ".")
+	for i := len(domComp)/2 - 1; i >= 0; i-- {
+		opp := len(domComp) - 1 - i
+		domComp[i], domComp[opp] = domComp[opp], domComp[i]
+	}
+	for i := len(patComp)/2 - 1; i >= 0; i-- {
+		opp := len(patComp) - 1 - i
+		patComp[i], patComp[opp] = patComp[opp], patComp[i]
+	}
+
+	for i, v := range domComp {
+		if len(patComp) <= i {
+			return false
+		}
+		p := patComp[i]
+		if p == "*" {
+			return true
+		}
+		if p != v {
+			return false
+		}
+	}
+	return false
+}
diff --git a/vendor/github.com/labstack/gommon/bytes/README.md b/vendor/github.com/labstack/gommon/bytes/README.md
new file mode 100644
index 000000000..6e3b0fd3d
--- /dev/null
+++ b/vendor/github.com/labstack/gommon/bytes/README.md
@@ -0,0 +1,29 @@
+# Bytes
+
+- Format bytes integer to human readable bytes string.
+- Parse human readable bytes string to bytes integer.
+
+## Installation
+
+```go
+go get github.com/labstack/gommon/bytes
+```
+
+## [Usage](https://github.com/labstack/gommon/blob/master/bytes/bytes_test.go)
+
+### Format
+
+```go
+println(bytes.Format(13231323))
+```
+
+`12.62MB`
+
+### Parse
+
+```go
+b, _ = Parse("2M")
+println(b)
+```
+
+`2097152`
diff --git a/vendor/github.com/labstack/gommon/bytes/bytes.go b/vendor/github.com/labstack/gommon/bytes/bytes.go
new file mode 100644
index 000000000..2f6bcec6e
--- /dev/null
+++ b/vendor/github.com/labstack/gommon/bytes/bytes.go
@@ -0,0 +1,109 @@
+package bytes
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+type (
+	// Bytes struct
+	Bytes struct{}
+)
+
+const (
+	_ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier
+	KB
+	MB
+	GB
+	TB
+	PB
+	EB
+)
+
+var (
+	pattern = regexp.MustCompile(`(?i)^(-?\d+(?:\.\d+)?)\s?([KMGTPE]B?|B?)$`)
+	global  = New()
+)
+
+// New creates a Bytes instance.
+func New() *Bytes {
+	return &Bytes{}
+}
+
+// Format formats bytes integer to human readable string.
+// For example, 31323 bytes will return 30.59KB.
+func (*Bytes) Format(b int64) string {
+	multiple := ""
+	value := float64(b)
+
+	switch {
+	case b >= EB:
+		value /= EB
+		multiple = "EB"
+	case b >= PB:
+		value /= PB
+		multiple = "PB"
+	case b >= TB:
+		value /= TB
+		multiple = "TB"
+	case b >= GB:
+		value /= GB
+		multiple = "GB"
+	case b >= MB:
+		value /= MB
+		multiple = "MB"
+	case b >= KB:
+		value /= KB
+		multiple = "KB"
+	case b == 0:
+		return "0"
+	default:
+		return strconv.FormatInt(b, 10) + "B"
+	}
+
+	return fmt.Sprintf("%.2f%s", value, multiple)
+}
+
+// Parse parses human readable bytes string to bytes integer.
+// For example, 6GB (6G is also valid) will return 6442450944.
+func (*Bytes) Parse(value string) (i int64, err error) {
+	parts := pattern.FindStringSubmatch(value)
+	if len(parts) < 3 {
+		return 0, fmt.Errorf("error parsing value=%s", value)
+	}
+	bytesString := parts[1]
+	multiple := strings.ToUpper(parts[2])
+	bytes, err := strconv.ParseFloat(bytesString, 64)
+	if err != nil {
+		return
+	}
+
+	switch multiple {
+	default:
+		return int64(bytes), nil
+	case "K", "KB":
+		return int64(bytes * KB), nil
+	case "M", "MB":
+		return int64(bytes * MB), nil
+	case "G", "GB":
+		return int64(bytes * GB), nil
+	case "T", "TB":
+		return int64(bytes * TB), nil
+	case "P", "PB":
+		return int64(bytes * PB), nil
+	case "E", "EB":
+		return int64(bytes * EB), nil
+	}
+}
+
+// Format wraps global Bytes's Format function.
+func Format(b int64) string {
+	return global.Format(b)
+}
+
+// Parse wraps global Bytes's Parse function.
+func Parse(val string) (int64, error) {
+	return global.Parse(val)
+}
diff --git a/vendor/github.com/labstack/gommon/random/random.go b/vendor/github.com/labstack/gommon/random/random.go
new file mode 100644
index 000000000..482d05248
--- /dev/null
+++ b/vendor/github.com/labstack/gommon/random/random.go
@@ -0,0 +1,48 @@
+package random
+
+import (
+	"math/rand"
+	"strings"
+	"time"
+)
+
+type (
+	Random struct {
+	}
+)
+
+// Charsets
+const (
+	Uppercase    = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	Lowercase    = "abcdefghijklmnopqrstuvwxyz"
+	Alphabetic   = Uppercase + Lowercase
+	Numeric      = "0123456789"
+	Alphanumeric = Alphabetic + Numeric
+	Symbols      = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?`
+	Hex          = Numeric + "abcdef"
+)
+
+var (
+	global = New()
+)
+
+func New() *Random {
+	rand.Seed(time.Now().UnixNano())
+	return new(Random)
+}
+
+func (r *Random) String(length uint8, charsets ...string) string {
+	charset := strings.Join(charsets, "")
+	if charset == "" {
+		charset = Alphanumeric
+	}
+	b := make([]byte, length)
+	for i := range b {
+		b[i] = charset[rand.Int63()%int64(len(charset))]
+	}
+	return string(b)
+}
+
+func String(length uint8, charsets ...string) string {
+	return global.String(length, charsets...)
+}
diff --git a/vendor/golang.org/x/time/AUTHORS b/vendor/golang.org/x/time/AUTHORS
new file mode 100644
index 000000000..15167cd74
--- /dev/null
+++ b/vendor/golang.org/x/time/AUTHORS
@@ -0,0 +1,3 @@
+# This source code refers to The Go Authors for copyright purposes.
+# The master list of authors is in the main Go distribution,
+# visible at http://tip.golang.org/AUTHORS.
diff --git a/vendor/golang.org/x/time/CONTRIBUTORS b/vendor/golang.org/x/time/CONTRIBUTORS
new file mode 100644
index 000000000..1c4577e96
--- /dev/null
+++ b/vendor/golang.org/x/time/CONTRIBUTORS
@@ -0,0 +1,3 @@
+# This source code was written by the Go contributors.
+# The master list of contributors is in the main Go distribution,
+# visible at http://tip.golang.org/CONTRIBUTORS.
diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE
new file mode 100644
index 000000000..6a66aea5e
--- /dev/null
+++ b/vendor/golang.org/x/time/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/golang.org/x/time/PATENTS b/vendor/golang.org/x/time/PATENTS
new file mode 100644
index 000000000..733099041
--- /dev/null
+++ b/vendor/golang.org/x/time/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Go project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Go, where such license applies only to those patent
+claims, both currently owned or controlled by Google and acquired in
+the future, licensable by Google that are necessarily infringed by this
+implementation of Go.  This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation.  If you or your agent or exclusive licensee institute or
+order or agree to the institution of patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that this implementation of Go or any code incorporated within this
+implementation of Go constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of Go
+shall terminate as of the date such litigation is filed.
diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go
new file mode 100644
index 000000000..a98fe7782
--- /dev/null
+++ b/vendor/golang.org/x/time/rate/rate.go
@@ -0,0 +1,402 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package rate provides a rate limiter.
+package rate
+
+import (
+	"context"
+	"fmt"
+	"math"
+	"sync"
+	"time"
+)
+
+// Limit defines the maximum frequency of some events.
+// Limit is represented as number of events per second.
+// A zero Limit allows no events.
+type Limit float64
+
+// Inf is the infinite rate limit; it allows all events (even if burst is zero).
+const Inf = Limit(math.MaxFloat64)
+
+// Every converts a minimum time interval between events to a Limit.
+func Every(interval time.Duration) Limit {
+	if interval <= 0 {
+		return Inf
+	}
+	return 1 / Limit(interval.Seconds())
+}
+
+// A Limiter controls how frequently events are allowed to happen.
+// It implements a "token bucket" of size b, initially full and refilled
+// at rate r tokens per second.
+// Informally, in any large enough time interval, the Limiter limits the
+// rate to r tokens per second, with a maximum burst size of b events.
+// As a special case, if r == Inf (the infinite rate), b is ignored.
+// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
+//
+// The zero value is a valid Limiter, but it will reject all events.
+// Use NewLimiter to create non-zero Limiters.
+//
+// Limiter has three main methods, Allow, Reserve, and Wait.
+// Most callers should use Wait.
+//
+// Each of the three methods consumes a single token.
+// They differ in their behavior when no token is available.
+// If no token is available, Allow returns false.
+// If no token is available, Reserve returns a reservation for a future token
+// and the amount of time the caller must wait before using it.
+// If no token is available, Wait blocks until one can be obtained
+// or its associated context.Context is canceled.
+//
+// The methods AllowN, ReserveN, and WaitN consume n tokens.
+type Limiter struct {
+	mu     sync.Mutex
+	limit  Limit
+	burst  int
+	tokens float64
+	// last is the last time the limiter's tokens field was updated
+	last time.Time
+	// lastEvent is the latest time of a rate-limited event (past or future)
+	lastEvent time.Time
+}
+
+// Limit returns the maximum overall event rate.
+func (lim *Limiter) Limit() Limit {
+	lim.mu.Lock()
+	defer lim.mu.Unlock()
+	return lim.limit
+}
+
+// Burst returns the maximum burst size. Burst is the maximum number of tokens
+// that can be consumed in a single call to Allow, Reserve, or Wait, so higher
+// Burst values allow more events to happen at once.
+// A zero Burst allows no events, unless limit == Inf.
+func (lim *Limiter) Burst() int {
+	lim.mu.Lock()
+	defer lim.mu.Unlock()
+	return lim.burst
+}
+
+// NewLimiter returns a new Limiter that allows events up to rate r and permits
+// bursts of at most b tokens.
+func NewLimiter(r Limit, b int) *Limiter {
+	return &Limiter{
+		limit: r,
+		burst: b,
+	}
+}
+
+// Allow is shorthand for AllowN(time.Now(), 1).
+func (lim *Limiter) Allow() bool {
+	return lim.AllowN(time.Now(), 1)
+}
+
+// AllowN reports whether n events may happen at time now.
+// Use this method if you intend to drop / skip events that exceed the rate limit.
+// Otherwise use Reserve or Wait.
+func (lim *Limiter) AllowN(now time.Time, n int) bool {
+	return lim.reserveN(now, n, 0).ok
+}
+
+// A Reservation holds information about events that are permitted by a Limiter to happen after a delay.
+// A Reservation may be canceled, which may enable the Limiter to permit additional events.
+type Reservation struct {
+	ok        bool
+	lim       *Limiter
+	tokens    int
+	timeToAct time.Time
+	// This is the Limit at reservation time, it can change later.
+	limit Limit
+}
+
+// OK returns whether the limiter can provide the requested number of tokens
+// within the maximum wait time.  If OK is false, Delay returns InfDuration, and
+// Cancel does nothing.
+func (r *Reservation) OK() bool {
+	return r.ok
+}
+
+// Delay is shorthand for DelayFrom(time.Now()).
+func (r *Reservation) Delay() time.Duration {
+	return r.DelayFrom(time.Now())
+}
+
+// InfDuration is the duration returned by Delay when a Reservation is not OK.
+const InfDuration = time.Duration(1<<63 - 1)
+
+// DelayFrom returns the duration for which the reservation holder must wait
+// before taking the reserved action.  Zero duration means act immediately.
+// InfDuration means the limiter cannot grant the tokens requested in this
+// Reservation within the maximum wait time.
+func (r *Reservation) DelayFrom(now time.Time) time.Duration {
+	if !r.ok {
+		return InfDuration
+	}
+	delay := r.timeToAct.Sub(now)
+	if delay < 0 {
+		return 0
+	}
+	return delay
+}
+
+// Cancel is shorthand for CancelAt(time.Now()).
+func (r *Reservation) Cancel() {
+	r.CancelAt(time.Now())
+	return
+}
+
+// CancelAt indicates that the reservation holder will not perform the reserved action
+// and reverses the effects of this Reservation on the rate limit as much as possible,
+// considering that other reservations may have already been made.
+func (r *Reservation) CancelAt(now time.Time) {
+	if !r.ok {
+		return
+	}
+
+	r.lim.mu.Lock()
+	defer r.lim.mu.Unlock()
+
+	if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(now) {
+		return
+	}
+
+	// calculate tokens to restore
+	// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
+	// after r was obtained. These tokens should not be restored.
+	restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct))
+	if restoreTokens <= 0 {
+		return
+	}
+	// advance time to now
+	now, _, tokens := r.lim.advance(now)
+	// calculate new number of tokens
+	tokens += restoreTokens
+	if burst := float64(r.lim.burst); tokens > burst {
+		tokens = burst
+	}
+	// update state
+	r.lim.last = now
+	r.lim.tokens = tokens
+	if r.timeToAct == r.lim.lastEvent {
+		prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens)))
+		if !prevEvent.Before(now) {
+			r.lim.lastEvent = prevEvent
+		}
+	}
+
+	return
+}
+
+// Reserve is shorthand for ReserveN(time.Now(), 1).
+func (lim *Limiter) Reserve() *Reservation {
+	return lim.ReserveN(time.Now(), 1)
+}
+
+// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
+// The Limiter takes this Reservation into account when allowing future events.
+// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size.
+// Usage example:
+//   r := lim.ReserveN(time.Now(), 1)
+//   if !r.OK() {
+//     // Not allowed to act! Did you remember to set lim.burst to be > 0 ?
+//     return
+//   }
+//   time.Sleep(r.Delay())
+//   Act()
+// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
+// If you need to respect a deadline or cancel the delay, use Wait instead.
+// To drop or skip events exceeding rate limit, use Allow instead.
+func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation {
+	r := lim.reserveN(now, n, InfDuration)
+	return &r
+}
+
+// Wait is shorthand for WaitN(ctx, 1).
+func (lim *Limiter) Wait(ctx context.Context) (err error) {
+	return lim.WaitN(ctx, 1)
+}
+
+// WaitN blocks until lim permits n events to happen.
+// It returns an error if n exceeds the Limiter's burst size, the Context is
+// canceled, or the expected wait time exceeds the Context's Deadline.
+// The burst limit is ignored if the rate limit is Inf.
+func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
+	lim.mu.Lock()
+	burst := lim.burst
+	limit := lim.limit
+	lim.mu.Unlock()
+
+	if n > burst && limit != Inf {
+		return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, burst)
+	}
+	// Check if ctx is already cancelled
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+	}
+	// Determine wait limit
+	now := time.Now()
+	waitLimit := InfDuration
+	if deadline, ok := ctx.Deadline(); ok {
+		waitLimit = deadline.Sub(now)
+	}
+	// Reserve
+	r := lim.reserveN(now, n, waitLimit)
+	if !r.ok {
+		return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n)
+	}
+	// Wait if necessary
+	delay := r.DelayFrom(now)
+	if delay == 0 {
+		return nil
+	}
+	t := time.NewTimer(delay)
+	defer t.Stop()
+	select {
+	case <-t.C:
+		// We can proceed.
+		return nil
+	case <-ctx.Done():
+		// Context was canceled before we could proceed.  Cancel the
+		// reservation, which may permit other events to proceed sooner.
+		r.Cancel()
+		return ctx.Err()
+	}
+}
+
+// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit).
+func (lim *Limiter) SetLimit(newLimit Limit) {
+	lim.SetLimitAt(time.Now(), newLimit)
+}
+
+// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated
+// or underutilized by those which reserved (using Reserve or Wait) but did not yet act
+// before SetLimitAt was called.
+func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit) {
+	lim.mu.Lock()
+	defer lim.mu.Unlock()
+
+	now, _, tokens := lim.advance(now)
+
+	lim.last = now
+	lim.tokens = tokens
+	lim.limit = newLimit
+}
+
+// SetBurst is shorthand for SetBurstAt(time.Now(), newBurst).
+func (lim *Limiter) SetBurst(newBurst int) {
+	lim.SetBurstAt(time.Now(), newBurst)
+}
+
+// SetBurstAt sets a new burst size for the limiter.
+func (lim *Limiter) SetBurstAt(now time.Time, newBurst int) {
+	lim.mu.Lock()
+	defer lim.mu.Unlock()
+
+	now, _, tokens := lim.advance(now)
+
+	lim.last = now
+	lim.tokens = tokens
+	lim.burst = newBurst
+}
+
+// reserveN is a helper method for AllowN, ReserveN, and WaitN.
+// maxFutureReserve specifies the maximum reservation wait duration allowed.
+// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.
+func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
+	lim.mu.Lock()
+
+	if lim.limit == Inf {
+		lim.mu.Unlock()
+		return Reservation{
+			ok:        true,
+			lim:       lim,
+			tokens:    n,
+			timeToAct: now,
+		}
+	}
+
+	now, last, tokens := lim.advance(now)
+
+	// Calculate the remaining number of tokens resulting from the request.
+	tokens -= float64(n)
+
+	// Calculate the wait duration
+	var waitDuration time.Duration
+	if tokens < 0 {
+		waitDuration = lim.limit.durationFromTokens(-tokens)
+	}
+
+	// Decide result
+	ok := n <= lim.burst && waitDuration <= maxFutureReserve
+
+	// Prepare reservation
+	r := Reservation{
+		ok:    ok,
+		lim:   lim,
+		limit: lim.limit,
+	}
+	if ok {
+		r.tokens = n
+		r.timeToAct = now.Add(waitDuration)
+	}
+
+	// Update state
+	if ok {
+		lim.last = now
+		lim.tokens = tokens
+		lim.lastEvent = r.timeToAct
+	} else {
+		lim.last = last
+	}
+
+	lim.mu.Unlock()
+	return r
+}
+
+// advance calculates and returns an updated state for lim resulting from the passage of time.
+// lim is not changed.
+// advance requires that lim.mu is held.
+func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
+	last := lim.last
+	if now.Before(last) {
+		last = now
+	}
+
+	// Avoid making delta overflow below when last is very old.
+	maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens)
+	elapsed := now.Sub(last)
+	if elapsed > maxElapsed {
+		elapsed = maxElapsed
+	}
+
+	// Calculate the new number of tokens, due to time that passed.
+	delta := lim.limit.tokensFromDuration(elapsed)
+	tokens := lim.tokens + delta
+	if burst := float64(lim.burst); tokens > burst {
+		tokens = burst
+	}
+
+	return now, last, tokens
+}
+
+// durationFromTokens is a unit conversion function from the number of tokens to the duration
+// of time it takes to accumulate them at a rate of limit tokens per second.
+func (limit Limit) durationFromTokens(tokens float64) time.Duration {
+	seconds := tokens / float64(limit)
+	return time.Nanosecond * time.Duration(1e9*seconds)
+}
+
+// tokensFromDuration is a unit conversion function from a time duration to the number of tokens
+// which could be accumulated during that duration at a rate of limit tokens per second.
+func (limit Limit) tokensFromDuration(d time.Duration) float64 {
+	// Split the integer and fractional parts ourself to minimize rounding errors.
+	// See golang.org/issues/34861.
+	sec := float64(d/time.Second) * float64(limit)
+	nsec := float64(d%time.Second) * float64(limit)
+	return sec + nsec/1e9
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 98f342954..4614e9157 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -19,7 +19,7 @@ cloud.google.com/go/storage/internal/apiv2
 # github.com/Azure/azure-pipeline-go v0.2.3
 ## explicit
 github.com/Azure/azure-pipeline-go/pipeline
-# github.com/Azure/azure-sdk-for-go v57.2.0+incompatible
+# github.com/Azure/azure-sdk-for-go v57.4.0+incompatible
 ## explicit
 github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/compute/mgmt/compute
 github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources
@@ -252,9 +252,12 @@ github.com/kr/text
 # github.com/labstack/echo/v4 v4.5.0
 ## explicit
 github.com/labstack/echo/v4
+github.com/labstack/echo/v4/middleware
 # github.com/labstack/gommon v0.3.0
+github.com/labstack/gommon/bytes
 github.com/labstack/gommon/color
 github.com/labstack/gommon/log
+github.com/labstack/gommon/random
 # github.com/mattn/go-colorable v0.1.8
 github.com/mattn/go-colorable
 # github.com/mattn/go-ieproxy v0.0.1
@@ -433,6 +436,8 @@ golang.org/x/text/transform
 golang.org/x/text/unicode/bidi
 golang.org/x/text/unicode/norm
 golang.org/x/text/width
+# golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
+golang.org/x/time/rate
 # google.golang.org/api v0.56.0
 ## explicit
 google.golang.org/api/compute/v1


Test patch:
diff --git a/test/cases/ostree-ng-og.sh b/test/cases/ostree-ng-og.sh
new file mode 100644
index 000000000..975f5947e
--- /dev/null
+++ b/test/cases/ostree-ng-og.sh
@@ -0,0 +1,648 @@
+#!/bin/bash
+set -euo pipefail
+
+# Provision the software under test.
+/usr/libexec/osbuild-composer-test/provision.sh
+
+# Get OS data.
+source /etc/os-release
+ARCH=$(uname -m)
+
+# Colorful output.
+function greenprint {
+    echo -e "\033[1;32m${1}\033[0m"
+}
+
+# Start libvirtd and test it.
+greenprint "🚀 Starting libvirt daemon"
+sudo systemctl start libvirtd
+sudo virsh list --all > /dev/null
+
+# Set a customized dnsmasq configuration for libvirt so we always get the
+# same address on bootup.
+sudo tee /tmp/integration.xml > /dev/null << EOF
+<network>
+  <name>integration</name>
+  <uuid>1c8fe98c-b53a-4ca4-bbdb-deb0f26b3579</uuid>
+  <forward mode='nat'>
+    <nat>
+      <port start='1024' end='65535'/>
+    </nat>
+  </forward>
+  <bridge name='integration' stp='on' delay='0'/>
+  <mac address='52:54:00:36:46:ef'/>
+  <ip address='192.168.100.1' netmask='255.255.255.0'>
+    <dhcp>
+      <range start='192.168.100.2' end='192.168.100.254'/>
+      <host mac='34:49:22:B0:83:30' name='vm-bios' ip='192.168.100.50'/>
+      <host mac='34:49:22:B0:83:31' name='vm-uefi' ip='192.168.100.51'/>
+    </dhcp>
+  </ip>
+</network>
+EOF
+if ! sudo virsh net-info integration > /dev/null 2>&1; then
+    sudo virsh net-define /tmp/integration.xml
+fi
+if [[ $(sudo virsh net-info integration | grep 'Active' | awk '{print $2}') == 'no' ]]; then
+    sudo virsh net-start integration
+fi
+
+# Allow anyone in the wheel group to talk to libvirt.
+greenprint "🚪 Allowing users in wheel group to talk to libvirt"
+sudo tee /etc/polkit-1/rules.d/50-libvirt.rules > /dev/null << EOF
+polkit.addRule(function(action, subject) {
+    if (action.id == "org.libvirt.unix.manage" &&
+        subject.isInGroup("adm")) {
+            return polkit.Result.YES;
+    }
+});
+EOF
+
+# Set up variables.
+OSTREE_REF="test/rhel/9/${ARCH}/edge"
+OS_VARIANT="rhel9-unknown"
+TEST_UUID=$(uuidgen)
+IMAGE_KEY="osbuild-composer-ostree-test-${TEST_UUID}"
+BIOS_GUEST_ADDRESS=192.168.100.50
+UEFI_GUEST_ADDRESS=192.168.100.51
+PROD_REPO_URL=http://192.168.100.1/repo
+PROD_REPO=/var/www/html/repo
+STAGE_REPO_ADDRESS=192.168.200.1
+STAGE_REPO_URL="http://${STAGE_REPO_ADDRESS}/repo/"
+QUAY_REPO_URL="docker://quay.io/osbuild/testing-rhel-edge-push"
+QUAY_REPO_TAG=$(tr -dc a-z0-9 < /dev/urandom | head -c 4 ; echo '')
+ARTIFACTS="ci-artifacts"
+mkdir -p "${ARTIFACTS}"
+
+# Set up temporary files.
+TEMPDIR=$(mktemp -d)
+BLUEPRINT_FILE=${TEMPDIR}/blueprint.toml
+KS_FILE=${TEMPDIR}/ks.cfg
+COMPOSE_START=${TEMPDIR}/compose-start-${IMAGE_KEY}.json
+COMPOSE_INFO=${TEMPDIR}/compose-info-${IMAGE_KEY}.json
+
+# SSH setup.
+SSH_OPTIONS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5)
+SSH_DATA_DIR=$(/usr/libexec/osbuild-composer-test/gen-ssh.sh)
+SSH_KEY=${SSH_DATA_DIR}/id_rsa
+SSH_KEY_PUB=$(cat "${SSH_KEY}".pub)
+
+if [[ "${ID}-${VERSION_ID}" != "rhel-9.0" ]]; then
+        echo "unsupported distro: ${ID}-${VERSION_ID}"
+        exit 1
+fi
+
+CONTAINER_TYPE=edge-container
+CONTAINER_FILENAME=container.tar
+INSTALLER_TYPE=edge-installer
+INSTALLER_FILENAME=installer.iso
+
+
+# Get the compose log.
+get_compose_log () {
+    COMPOSE_ID=$1
+    LOG_FILE=${ARTIFACTS}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.log
+
+    # Download the logs.
+    sudo composer-cli compose log "$COMPOSE_ID" | tee "$LOG_FILE" > /dev/null
+}
+
+# Get the compose metadata.
+get_compose_metadata () {
+    COMPOSE_ID=$1
+    METADATA_FILE=${ARTIFACTS}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.json
+
+    # Download the metadata.
+    sudo composer-cli compose metadata "$COMPOSE_ID" > /dev/null
+
+    # Find the tarball and extract it.
+    TARBALL=$(basename "$(find . -maxdepth 1 -type f -name "*-metadata.tar")")
+    tar -xf "$TARBALL" -C "${TEMPDIR}"
+    rm -f "$TARBALL"
+
+    # Move the JSON file into place.
+    cat "${TEMPDIR}"/"${COMPOSE_ID}".json | jq -M '.' | tee "$METADATA_FILE" > /dev/null
+}
+
+# Build ostree image.
+build_image() {
+    blueprint_name=$1
+    image_type=$2
+
+    # Get worker unit file so we can watch the journal.
+    WORKER_UNIT=$(sudo systemctl list-units | grep -o -E "osbuild.*worker.*\.service")
+    sudo journalctl -af -n 1 -u "${WORKER_UNIT}" &
+    WORKER_JOURNAL_PID=$!
+    # Stop watching the worker journal when exiting.
+    trap 'sudo pkill -P ${WORKER_JOURNAL_PID}' EXIT
+
+    # Start the compose.
+    greenprint "🚀 Starting compose"
+    if [ $# -eq 3 ]; then
+        repo_url=$3
+        sudo composer-cli --json compose start-ostree --ref "$OSTREE_REF" --url "$repo_url" "$blueprint_name" "$image_type" | tee "$COMPOSE_START"
+    else
+        sudo composer-cli --json compose start-ostree --ref "$OSTREE_REF" "$blueprint_name" "$image_type" | tee "$COMPOSE_START"
+    fi
+    COMPOSE_ID=$(jq -r '.body.build_id' "$COMPOSE_START")
+
+    # Wait for the compose to finish.
+    greenprint "⏱ Waiting for compose to finish: ${COMPOSE_ID}"
+    while true; do
+        sudo composer-cli --json compose info "${COMPOSE_ID}" | tee "$COMPOSE_INFO" > /dev/null
+        COMPOSE_STATUS=$(jq -r '.body.queue_status' "$COMPOSE_INFO")
+
+        # Is the compose finished?
+        if [[ $COMPOSE_STATUS != RUNNING ]] && [[ $COMPOSE_STATUS != WAITING ]]; then
+            break
+        fi
+
+        # Wait 30 seconds and try again.
+        sleep 5
+    done
+
+    # Capture the compose logs from osbuild.
+    greenprint "💬 Getting compose log and metadata"
+    get_compose_log "$COMPOSE_ID"
+    get_compose_metadata "$COMPOSE_ID"
+
+    # Kill the journal monitor immediately and remove the trap
+    sudo pkill -P ${WORKER_JOURNAL_PID}
+    trap - EXIT
+
+    # Did the compose finish with success?
+    if [[ $COMPOSE_STATUS != FINISHED ]]; then
+        echo "Something went wrong with the compose. 😢"
+        exit 1
+    fi
+}
+
+# Wait for the ssh server up to be.
+wait_for_ssh_up () {
+    SSH_STATUS=$(sudo ssh "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" admin@"${1}" '/bin/bash -c "echo -n READY"')
+    if [[ $SSH_STATUS == READY ]]; then
+        echo 1
+    else
+        echo 0
+    fi
+}
+
+# Clean up our mess.
+clean_up () {
+    greenprint "🧼 Cleaning up"
+    # Remove tag from quay.io repo
+    skopeo delete --creds "${QUAY_USERNAME}:${QUAY_PASSWORD}" "${QUAY_REPO_URL}:${QUAY_REPO_TAG}"
+
+    # Clear vm
+    if [[ $(sudo virsh domstate "${IMAGE_KEY}-uefi") == "running" ]]; then
+        sudo virsh destroy "${IMAGE_KEY}-uefi"
+    fi
+    sudo virsh undefine "${IMAGE_KEY}-uefi" --nvram
+    # Remove qcow2 file.
+    sudo rm -f "$LIBVIRT_UEFI_IMAGE_PATH"
+
+    # Remove any status containers if exist
+    sudo podman ps -a -q --format "{{.ID}}" | sudo xargs --no-run-if-empty podman rm -f
+    # Remove all images
+    sudo podman rmi -f -a
+
+    # Remove prod repo
+    sudo rm -rf "$PROD_REPO"
+
+    # Remomve tmp dir.
+    sudo rm -rf "$TEMPDIR"
+
+    # Stop prod repo http service
+    sudo systemctl disable --now httpd
+}
+
+# Test result checking
+check_result () {
+    greenprint "🎏 Checking for test result"
+    if [[ $RESULTS == 1 ]]; then
+        greenprint "💚 Success"
+    else
+        greenprint "❌ Failed"
+        clean_up
+        exit 1
+    fi
+}
+
+###########################################################
+##
+## Prepare edge prod and stage repo
+##
+###########################################################
+greenprint "🔧 Prepare edge prod repo"
+# Start prod repo web service
+# osbuild-composer-tests have mod_ssl as a dependency. The package installs
+# an example configuration which automatically enabled httpd on port 443, but
+# that one is already in use. Remove the default configuration as it is useless
+# anyway.
+sudo rm -f /etc/httpd/conf.d/ssl.conf
+sudo systemctl enable --now httpd.service
+
+# Have a clean prod repo
+sudo rm -rf "$PROD_REPO"
+sudo mkdir -p "$PROD_REPO"
+sudo ostree --repo="$PROD_REPO" init --mode=archive
+sudo ostree --repo="$PROD_REPO" remote add --no-gpg-verify edge-stage "$STAGE_REPO_URL"
+
+# Prepare stage repo network
+greenprint "🔧 Prepare stage repo network"
+sudo podman network inspect edge >/dev/null 2>&1 || sudo podman network create --driver=bridge --subnet=192.168.200.0/24 --ip-range=192.168.200.0/24 --gateway=192.168.200.254 edge
+
+##########################################################
+##
+## edge container image for building installer image
+##
+##########################################################
+
+# Write a blueprint for ostree image.
+tee "$BLUEPRINT_FILE" > /dev/null << EOF
+name = "container"
+description = "A base rhel-edge container image"
+version = "0.0.1"
+modules = []
+groups = []
+
+[[packages]]
+name = "python3"
+version = "*"
+
+[[customizations.user]]
+name = "admin"
+description = "Administrator account"
+password = "\$6\$GRmb7S0p8vsYmXzH\$o0E020S.9JQGaHkszoog4ha4AQVs3sk8q0DvLjSMxoxHBKnB2FBXGQ/OkwZQfW/76ktHd0NX5nls2LPxPuUdl."
+key = "${SSH_KEY_PUB}"
+home = "/home/admin/"
+groups = ["wheel"]
+EOF
+
+greenprint "📄 container blueprint"
+cat "$BLUEPRINT_FILE"
+
+# Prepare the blueprint for the compose.
+greenprint "📋 Preparing container blueprint"
+sudo composer-cli blueprints push "$BLUEPRINT_FILE"
+sudo composer-cli blueprints depsolve container
+
+# Build container image.
+build_image container "${CONTAINER_TYPE}"
+
+# Download the image
+greenprint "📥 Downloading the container image"
+sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
+
+# Clear stage repo running env
+greenprint "🧹 Clearing stage repo running env"
+# Remove any status containers if exist
+sudo podman ps -a -q --format "{{.ID}}" | sudo xargs --no-run-if-empty podman rm -f
+# Remove all images
+sudo podman rmi -f -a
+
+# Deal with stage repo image
+greenprint "🗜 Pushing image to quay.io"
+IMAGE_FILENAME="${COMPOSE_ID}-${CONTAINER_FILENAME}"
+skopeo copy --dest-creds "${QUAY_USERNAME}:${QUAY_PASSWORD}" "oci-archive:${IMAGE_FILENAME}" "${QUAY_REPO_URL}:${QUAY_REPO_TAG}"
+greenprint "Downloading image from quay.io"
+sudo podman login quay.io --username "${QUAY_USERNAME}" --password "${QUAY_PASSWORD}"
+sudo podman pull "${QUAY_REPO_URL}:${QUAY_REPO_TAG}"
+sudo podman images
+greenprint "🗜 Running the image"
+sudo podman run -d --name rhel-edge --network edge --ip "$STAGE_REPO_ADDRESS" "${QUAY_REPO_URL}:${QUAY_REPO_TAG}"
+# Clear image file
+sudo rm -f "$IMAGE_FILENAME"
+
+# Wait for container to be running
+until [ "$(sudo podman inspect -f '{{.State.Running}}' rhel-edge)" == "true" ]; do
+    sleep 1;
+done;
+
+# Sync installer edge content
+greenprint "📡 Sync installer content from stage repo"
+sudo ostree --repo="$PROD_REPO" pull --mirror edge-stage "$OSTREE_REF"
+
+# Clean compose and blueprints.
+greenprint "🧽 Clean up container blueprint and compose"
+sudo composer-cli compose delete "${COMPOSE_ID}" > /dev/null
+sudo composer-cli blueprints delete container > /dev/null
+
+########################################################
+##
+## rhel-edge installer image building from container image
+##
+########################################################
+
+# Write a blueprint for installer image.
+tee "$BLUEPRINT_FILE" > /dev/null << EOF
+name = "installer"
+description = "A rhel-edge installer image"
+version = "0.0.1"
+modules = []
+groups = []
+EOF
+
+greenprint "📄 installer blueprint"
+cat "$BLUEPRINT_FILE"
+
+# Prepare the blueprint for the compose.
+greenprint "📋 Preparing installer blueprint"
+sudo composer-cli blueprints push "$BLUEPRINT_FILE"
+sudo composer-cli blueprints depsolve installer
+
+# Build installer image.
+# Test --url arg following by URL with tailling slash for bz#1942029
+build_image installer "${INSTALLER_TYPE}" "${PROD_REPO_URL}/"
+
+# Download the image
+greenprint "📥 Downloading the installer image"
+sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
+ISO_FILENAME="${COMPOSE_ID}-${INSTALLER_FILENAME}"
+sudo mv "${ISO_FILENAME}" /var/lib/libvirt/images
+
+# Clean compose and blueprints.
+greenprint "🧹 Clean up installer blueprint and compose"
+sudo composer-cli compose delete "${COMPOSE_ID}" > /dev/null
+sudo composer-cli blueprints delete installer > /dev/null
+
+########################################################
+##
+## install rhel-edge image with installer(ISO)
+##
+########################################################
+
+# Ensure SELinux is happy with our new images.
+greenprint "👿 Running restorecon on image directory"
+sudo restorecon -Rv /var/lib/libvirt/images/
+
+# Create qcow2 file for virt install.
+greenprint "🖥 Create qcow2 file for virt install"
+LIBVIRT_BIOS_IMAGE_PATH=/var/lib/libvirt/images/${IMAGE_KEY}-bios.qcow2
+LIBVIRT_UEFI_IMAGE_PATH=/var/lib/libvirt/images/${IMAGE_KEY}-uefi.qcow2
+sudo qemu-img create -f qcow2 "${LIBVIRT_BIOS_IMAGE_PATH}" 20G
+sudo qemu-img create -f qcow2 "${LIBVIRT_UEFI_IMAGE_PATH}" 20G
+
+# Write kickstart file for ostree image installation.
+greenprint "📑 Generate kickstart file"
+tee "$KS_FILE" > /dev/null << STOPHERE
+text
+network --bootproto=dhcp --device=link --activate --onboot=on
+
+zerombr
+clearpart --all --initlabel --disklabel=msdos
+autopart --nohome --noswap --type=plain
+ostreesetup --nogpg --osname=rhel-edge --remote=rhel-edge --url=file:///ostree/repo --ref=${OSTREE_REF}
+poweroff
+
+%post --log=/var/log/anaconda/post-install.log --erroronfail
+
+# no sudo password for user admin
+echo -e 'admin\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
+
+# delete local ostree repo and add external prod edge repo
+ostree remote delete rhel-edge
+ostree remote add --no-gpg-verify --no-sign-verify rhel-edge ${PROD_REPO_URL}
+%end
+STOPHERE
+
+##################################################
+##
+## Install and test Edge image on BIOS VM
+##
+##################################################
+# Install ostree image via anaconda.
+greenprint "💿 Install ostree image via installer(ISO) on BIOS VM"
+sudo virt-install  --initrd-inject="${KS_FILE}" \
+                   --extra-args="inst.ks=file:/ks.cfg console=ttyS0,115200" \
+                   --name="${IMAGE_KEY}-bios" \
+                   --disk path="${LIBVIRT_BIOS_IMAGE_PATH}",format=qcow2 \
+                   --ram 3072 \
+                   --vcpus 2 \
+                   --network network=integration,mac=34:49:22:B0:83:30 \
+                   --os-type linux \
+                   --os-variant ${OS_VARIANT} \
+                   --location "/var/lib/libvirt/images/${ISO_FILENAME}" \
+                   --nographics \
+                   --noautoconsole \
+                   --wait=-1 \
+                   --noreboot
+
+# Start VM.
+greenprint "📟 Start BIOS VM"
+sudo virsh start "${IMAGE_KEY}-bios"
+
+# Check for ssh ready to go.
+greenprint "🛃 Checking for SSH is ready to go"
+for LOOP_COUNTER in $(seq 0 30); do
+    RESULTS="$(wait_for_ssh_up $BIOS_GUEST_ADDRESS)"
+    if [[ $RESULTS == 1 ]]; then
+        echo "SSH is ready now! 🥳"
+        break
+    fi
+    sleep 10
+done
+
+# Check image installation result
+check_result
+
+# Get ostree commit value.
+greenprint "🕹 Get ostree install commit value"
+INSTALL_HASH=$(curl "${PROD_REPO_URL}/refs/heads/${OSTREE_REF}")
+
+# Run Edge test on BIOS VM
+# Add instance IP address into /etc/ansible/hosts
+sudo tee "${TEMPDIR}"/inventory > /dev/null << EOF
+[ostree_guest]
+${BIOS_GUEST_ADDRESS}
+
+[ostree_guest:vars]
+ansible_python_interpreter=/usr/bin/python3
+ansible_user=admin
+ansible_private_key_file=${SSH_KEY}
+ansible_ssh_common_args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+EOF
+
+# Test IoT/Edge OS
+greenprint "📼 Run Edge tests on BIOS VM"
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=rhel-edge -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+check_result
+
+# Clean up BIOS VM
+greenprint "🧹 Clean up BIOS VM"
+if [[ $(sudo virsh domstate "${IMAGE_KEY}-bios") == "running" ]]; then
+    sudo virsh destroy "${IMAGE_KEY}-bios"
+fi
+sudo virsh undefine "${IMAGE_KEY}-bios"
+sudo rm -f "$LIBVIRT_BIOS_IMAGE_PATH"
+
+##################################################
+##
+## Install, upgrade and test Edge image on UEFI VM
+##
+##################################################
+# Install ostree image via anaconda.
+greenprint "💿 Install ostree image via installer(ISO) on UEFI VM"
+sudo virt-install  --initrd-inject="${KS_FILE}" \
+                   --extra-args="inst.ks=file:/ks.cfg console=ttyS0,115200" \
+                   --name="${IMAGE_KEY}-uefi"\
+                   --disk path="${LIBVIRT_UEFI_IMAGE_PATH}",format=qcow2 \
+                   --ram 3072 \
+                   --vcpus 2 \
+                   --network network=integration,mac=34:49:22:B0:83:31 \
+                   --os-type linux \
+                   --os-variant ${OS_VARIANT} \
+                   --location "/var/lib/libvirt/images/${ISO_FILENAME}" \
+                   --boot uefi,loader_ro=yes,loader_type=pflash,nvram_template=/usr/share/edk2/ovmf/OVMF_VARS.fd,loader_secure=no \
+                   --nographics \
+                   --noautoconsole \
+                   --wait=-1 \
+                   --noreboot
+
+# Start VM.
+greenprint "💻 Start UEFI VM"
+sudo virsh start "${IMAGE_KEY}-uefi"
+
+# Check for ssh ready to go.
+greenprint "🛃 Checking for SSH is ready to go"
+for LOOP_COUNTER in $(seq 0 30); do
+    RESULTS="$(wait_for_ssh_up $UEFI_GUEST_ADDRESS)"
+    if [[ $RESULTS == 1 ]]; then
+        echo "SSH is ready now! 🥳"
+        break
+    fi
+    sleep 10
+done
+
+# Check image installation result
+check_result
+
+##################################################
+##
+## upgrade rhel-edge with new upgrade commit
+##
+##################################################
+
+# Write a blueprint for ostree image.
+tee "$BLUEPRINT_FILE" > /dev/null << EOF
+name = "upgrade"
+description = "An upgrade rhel-edge container image"
+version = "0.0.2"
+modules = []
+groups = []
+
+[[packages]]
+name = "python3"
+version = "*"
+
+[[packages]]
+name = "wget"
+version = "*"
+
+[[customizations.user]]
+name = "admin"
+description = "Administrator account"
+password = "\$6\$GRmb7S0p8vsYmXzH\$o0E020S.9JQGaHkszoog4ha4AQVs3sk8q0DvLjSMxoxHBKnB2FBXGQ/OkwZQfW/76ktHd0NX5nls2LPxPuUdl."
+key = "${SSH_KEY_PUB}"
+home = "/home/admin/"
+groups = ["wheel"]
+EOF
+
+greenprint "📄 upgrade blueprint"
+cat "$BLUEPRINT_FILE"
+
+# Prepare the blueprint for the compose.
+greenprint "📋 Preparing upgrade blueprint"
+sudo composer-cli blueprints push "$BLUEPRINT_FILE"
+sudo composer-cli blueprints depsolve upgrade
+
+# Build upgrade image.
+build_image upgrade  "${CONTAINER_TYPE}" "$PROD_REPO_URL"
+
+# Download the image
+greenprint "📥 Downloading the upgrade image"
+sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
+
+# Clear stage repo running env
+greenprint "🧹 Clearing stage repo running env"
+# Remove any status containers if exist
+sudo podman ps -a -q --format "{{.ID}}" | sudo xargs --no-run-if-empty podman rm -f
+# Remove all images
+sudo podman rmi -f -a
+
+# Deal with stage repo container
+greenprint "🗜 Extracting image"
+IMAGE_FILENAME="${COMPOSE_ID}-${CONTAINER_FILENAME}"
+sudo podman pull "oci-archive:${IMAGE_FILENAME}"
+sudo podman images
+# Clear image file
+sudo rm -f "$IMAGE_FILENAME"
+
+# Run edge stage repo
+greenprint "🛰 Running edge stage repo"
+# Get image id to run image
+EDGE_IMAGE_ID=$(sudo podman images --filter "dangling=true" --format "{{.ID}}")
+sudo podman run -d --name rhel-edge --network edge --ip "$STAGE_REPO_ADDRESS" "$EDGE_IMAGE_ID"
+# Wait for container to be running
+until [ "$(sudo podman inspect -f '{{.State.Running}}' rhel-edge)" == "true" ]; do
+    sleep 1;
+done;
+
+# Pull upgrade to prod mirror
+greenprint "⛓ Pull upgrade to prod mirror"
+sudo ostree --repo="$PROD_REPO" pull --mirror edge-stage "$OSTREE_REF"
+sudo ostree --repo="$PROD_REPO" static-delta generate "$OSTREE_REF"
+sudo ostree --repo="$PROD_REPO" summary -u
+
+# Get ostree commit value.
+greenprint "🕹 Get ostree upgrade commit value"
+UPGRADE_HASH=$(curl "${PROD_REPO_URL}/refs/heads/${OSTREE_REF}")
+
+# Clean compose and blueprints.
+greenprint "🧽 Clean up upgrade blueprint and compose"
+sudo composer-cli compose delete "${COMPOSE_ID}" > /dev/null
+sudo composer-cli blueprints delete upgrade > /dev/null
+
+# Upgrade image/commit.
+greenprint "🗳 Upgrade ostree image/commit"
+sudo ssh "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" admin@${UEFI_GUEST_ADDRESS} 'sudo rpm-ostree upgrade'
+sudo ssh "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" admin@${UEFI_GUEST_ADDRESS} 'nohup sudo systemctl reboot &>/dev/null & exit'
+
+# Sleep 10 seconds here to make sure vm restarted already
+sleep 10
+
+# Check for ssh ready to go.
+greenprint "🛃 Checking for SSH is ready to go"
+# shellcheck disable=SC2034  # Unused variables left for readability
+for LOOP_COUNTER in $(seq 0 30); do
+    RESULTS="$(wait_for_ssh_up $UEFI_GUEST_ADDRESS)"
+    if [[ $RESULTS == 1 ]]; then
+        echo "SSH is ready now! 🥳"
+        break
+    fi
+    sleep 10
+done
+
+# Check ostree upgrade result
+check_result
+
+# Add instance IP address into /etc/ansible/hosts
+sudo tee "${TEMPDIR}"/inventory > /dev/null << EOF
+[ostree_guest]
+${UEFI_GUEST_ADDRESS}
+
+[ostree_guest:vars]
+ansible_python_interpreter=/usr/bin/python3
+ansible_user=admin
+ansible_private_key_file=${SSH_KEY}
+ansible_ssh_common_args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+EOF
+
+# Test IoT/Edge OS
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=rhel-edge -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree_nort.yaml || RESULTS=0
+check_result
+
+# Final success clean up
+clean_up
+
+exit 0
diff --git a/test/cases/ostree-ng.sh b/test/cases/ostree-ng.sh
index 96f6966c3..48da470a0 100755
--- a/test/cases/ostree-ng.sh
+++ b/test/cases/ostree-ng.sh
@@ -500,7 +500,7 @@ EOF
 
 # Test IoT/Edge OS
 greenprint "📼 Run Edge tests on BIOS VM"
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=rhel-edge -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=rhel-edge -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 # Clean up BIOS VM
@@ -682,7 +682,7 @@ ansible_ssh_common_args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/
 EOF
 
 # Test IoT/Edge OS
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=rhel-edge -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=rhel-edge -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 # Final success clean up
diff --git a/test/cases/ostree-raw-image.sh b/test/cases/ostree-raw-image.sh
index df16b2a6e..e5863cdc9 100755
--- a/test/cases/ostree-raw-image.sh
+++ b/test/cases/ostree-raw-image.sh
@@ -439,7 +439,7 @@ ansible_become_pass=${EDGE_USER_PASSWORD}
 EOF
 
 # Test IoT/Edge OS
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 # Clean up BIOS VM
@@ -512,7 +512,7 @@ ansible_become_pass=${EDGE_USER_PASSWORD}
 EOF
 
 # Test IoT/Edge OS
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 ##################################################################
@@ -648,7 +648,7 @@ ansible_become_pass=${EDGE_USER_PASSWORD}
 EOF
 
 # Test IoT/Edge OS
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 # Final success clean up
diff --git a/test/cases/ostree-simplified-installer.sh b/test/cases/ostree-simplified-installer.sh
index 86f4da90f..9b42f89ad 100755
--- a/test/cases/ostree-simplified-installer.sh
+++ b/test/cases/ostree-simplified-installer.sh
@@ -463,7 +463,7 @@ ansible_become_pass=${EDGE_USER_PASSWORD}
 EOF
 
 # Test IoT/Edge OS
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 # Clean up BIOS VM
@@ -540,7 +540,7 @@ ansible_become_pass=${EDGE_USER_PASSWORD}
 EOF
 
 # Test IoT/Edge OS
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${INSTALL_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 ##################################################################
@@ -675,7 +675,7 @@ ansible_become_pass=${EDGE_USER_PASSWORD}
 EOF
 
 # Test IoT/Edge OS
-sudo ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=redhat -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
 check_result
 
 # Final success clean up
diff --git a/test/cases/ostree.sh b/test/cases/ostree.sh
index e20301993..14527c028 100755
--- a/test/cases/ostree.sh
+++ b/test/cases/ostree.sh
@@ -53,6 +53,15 @@ function greenprint {
     echo -e "\033[1;32m${1}\033[0m"
 }
 
+function get_build_info() {
+    key="$1"
+    fname="$2"
+    if rpm -q --quiet weldr-client; then
+        key=".body${key}"
+    fi
+    jq -r "${key}" "${fname}"
+}
+
 # Start libvirtd and test it.
 greenprint "🚀 Starting libvirt daemon"
 sudo systemctl start libvirtd
@@ -170,13 +179,13 @@ build_image() {
     else
         sudo composer-cli --json compose start "$blueprint_name" $IMAGE_TYPE | tee "$COMPOSE_START"
     fi
-    COMPOSE_ID=$(jq -r '.build_id' "$COMPOSE_START")
+    COMPOSE_ID=$(get_build_info ".build_id" "$COMPOSE_START")
 
     # Wait for the compose to finish.
     greenprint "⏱ Waiting for compose to finish: ${COMPOSE_ID}"
     while true; do
         sudo composer-cli --json compose info "${COMPOSE_ID}" | tee "$COMPOSE_INFO" > /dev/null
-        COMPOSE_STATUS=$(jq -r '.queue_status' "$COMPOSE_INFO")
+        COMPOSE_STATUS=$(get_build_info ".queue_status" "$COMPOSE_INFO")
 
         # Is the compose finished?
         if [[ $COMPOSE_STATUS != RUNNING ]] && [[ $COMPOSE_STATUS != WAITING ]]; then
@@ -261,7 +270,7 @@ modules = []
 groups = []
 
 [[packages]]
-name = "python36"
+name = "python3"
 version = "*"
 EOF
 
@@ -426,7 +435,7 @@ modules = []
 groups = []
 
 [[packages]]
-name = "python36"
+name = "python3"
 version = "*"
 
 [[packages]]
diff --git a/test/data/ansible/check_ostree_nort.yaml b/test/data/ansible/check_ostree_nort.yaml
new file mode 100644
index 000000000..cb0803fcb
--- /dev/null
+++ b/test/data/ansible/check_ostree_nort.yaml
@@ -0,0 +1,530 @@
+---
+- hosts: ostree_guest
+  become: no
+  vars:
+    workspace: "{{ lookup('env', 'WORKSPACE') }}"
+    total_counter: "0"
+    failed_counter: "0"
+
+  tasks:
+    # current target host's IP address
+    - debug: var=ansible_all_ipv4_addresses
+
+    # default kernel or rt kernel
+    - name: check installed kernel
+      command: uname -r
+      register: result_kernel
+
+    # first installed or upgraded
+    # first installed has one commit, but upgraded has two
+    - name: determin which stage the checking is running on
+      shell: rpm-ostree status --json | jq '.deployments | length'
+      register: result_stage
+
+    - set_fact:
+        checking_stage: "{{ result_stage.stdout }}"
+
+    # case: check ostree commit correctly updated
+    - name: get deployed ostree commit
+      command: rpm-ostree status --json
+      register: result_commit
+
+    - name: make a json result
+      set_fact:
+        deploy_commit: "{{ result_commit.stdout | from_json | json_query('deployments[0].checksum') }}"
+
+    - name: check commit deployed and built
+      block:
+        - assert:
+            that:
+              - deploy_commit == ostree_commit
+            fail_msg: "deployed ostree commit is not commit built by osbuild-composer"
+            success_msg: "successful building and deployment"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case from bug: https://bugzilla.redhat.com/show_bug.cgi?id=1848453
+    - name: check ostree-remount status
+      command: systemctl is-active ostree-remount.service
+      register: result_remount
+
+    - name: ostree-remount should be started
+      block:
+        - assert:
+            that:
+              - result_remount.stdout == "active"
+            fail_msg: "ostree-remount is not started by default"
+            success_msg: "starting ostree-remount successful"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # bios or uefi
+    - name: bios or uefi
+      stat:
+        path: /sys/firmware/efi
+      register: result_uefi
+
+    - set_fact:
+        device_name: /dev/vda2
+
+    - set_fact:
+        device_name: /dev/vda3
+      when: result_uefi.stat.exists
+
+    # for edge-simplified-installer and edge-raw-image, /sysroot is mounted at /dev/vda4, need to set device_name to /dev/vda4
+    - name: check if it is simplified-installer or raw-image
+      command: df -h
+      register: result_df
+    
+    - set_fact:
+        device_name: /dev/vda4
+      when: "'/dev/vda4' in result_df.stdout"
+
+    # case: check /sysroot moutn point
+    - name: check /sysroot mount point
+      command: findmnt -r -o SOURCE -n /sysroot
+      register: result_sysroot_mount_point
+
+    - name: "/sysroot should be mounted on {{ device_name }}"
+      block:
+        - assert:
+            that:
+              - result_sysroot_mount_point.stdout == "{{ device_name }}"
+            fail_msg: "/var does not mount on {{ device_name }}"
+            success_msg: "/var mounts on {{ device_name }}"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check /sysroot mount status
+    - name: check /sysroot mount status
+      shell: findmnt -r -o OPTIONS -n /sysroot | awk -F "," '{print $1}'
+      register: result_sysroot_mount_status
+
+    - name: /sysroot should be mount with rw permission
+      block:
+        - assert:
+            that:
+              - result_sysroot_mount_status.stdout == "rw"
+            fail_msg: "/sysroot is not mounted with rw permission"
+            success_msg: "/sysroot is mounted with rw permission"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check /var mount point
+    - name: check /var mount point
+      command: findmnt -r -o SOURCE -n /var
+      register: result_var_mount_point
+
+    - name: "/var should be mounted on {{ device_name }}[/ostree/deploy/{{ image_type }}/var]"
+      block:
+        - assert:
+            that:
+              - result_var_mount_point.stdout == "{{ device_name }}[/ostree/deploy/{{ image_type }}/var]"
+            fail_msg: "/var does not mount on {{ device_name }}[/ostree/deploy/{{ image_type }}/var]"
+            success_msg: "/var mounts on {{ device_name }}[/ostree/deploy/{{ image_type }}/var]"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check /var mount status
+    - name: check /var mount status
+      shell: findmnt -r -o OPTIONS -n /var | awk -F "," '{print $1}'
+      register: result_var_mount_status
+
+    - name: /var should be mount with rw permission
+      block:
+        - assert:
+            that:
+              - result_var_mount_status.stdout == "rw"
+            fail_msg: "/var is not mounted with rw permission"
+            success_msg: "/var is mounted with rw permission"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check /usr mount point
+    - name: check /usr mount point
+      command: findmnt -r -o SOURCE -n /usr
+      register: result_usr_mount_point
+
+    - name: "/usr should be mounted on {{ device_name }}[/ostree/deploy/{{ image_type }}/deploy/{{ deploy_commit }}.0/usr]"
+      block:
+        - assert:
+            that:
+              - result_usr_mount_point.stdout == "{{ device_name }}[/ostree/deploy/{{ image_type }}/deploy/{{ deploy_commit }}.0/usr]"
+            fail_msg: "/usr does not mount on {{ device_name }}[/ostree/deploy/{{ image_type }}/deploy/{{ deploy_commit }}.0/usr]"
+            success_msg: "/usr mounts on {{ device_name }}[/ostree/deploy/{{ image_type }}/deploy/{{ deploy_commit }}.0/usr]"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check /usr mount status
+    - name: check /usr mount status
+      shell: findmnt -r -o OPTIONS -n /usr | awk -F "," '{print $1}'
+      register: result_usr_mount_status
+
+    - name: /usr should be mount with rw permission
+      block:
+        - assert:
+            that:
+              - result_usr_mount_status.stdout == "ro"
+            fail_msg: "/usr is not mounted with ro permission"
+            success_msg: "/usr is mounted with ro permission"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    - name: get the first 10 chars in commit hash
+      set_fact:
+        commit_log: "{{ deploy_commit[:11] }}"
+
+    # case: check wget installed after upgrade
+    - name: check installed package
+      shell: rpm -qa | sort
+      register: result_packages
+
+    - name: check wget installed
+      block:
+        - assert:
+            that:
+              - "'wget' in result_packages.stdout"
+            fail_msg: "wget not installed, ostree upgrade might be failed"
+            success_msg: "wget installed in ostree upgrade"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+      when: checking_stage == "2"
+
+    - name: save installed package to log file
+      copy:
+        content: "{{ result_packages.stdout }}"
+        dest: "{{ workspace }}/{{ commit_log }}.installed.ostree.log"
+      delegate_to: localhost
+
+    # case: check ostree-remount mount log
+    - name: check ostree-remount mount log
+      command: journalctl -u ostree-remount
+      register: result_remount_jounalctl
+
+    - name: ostree-remount should remount /var and /sysroot
+      block:
+        - assert:
+            that:
+              - "'/sysroot' in result_remount_jounalctl.stdout"
+              - "'/var' in result_remount_jounalctl.stdout"
+            fail_msg: "/sysroot or /var are not remounted by ostree-remount"
+            success_msg: "/sysroot and /var are remount"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check dmesg error and failed log
+    - name: check dmesg output
+      command: dmesg
+      register: result_dmesg
+
+    - name: save dmesg output to log file
+      copy:
+        content: "{{ result_dmesg.stdout }}"
+        dest: "{{ workspace }}/{{ commit_log }}.dmesg.ostree.log"
+      delegate_to: localhost
+
+    - name: check dmesg error and fail log
+      shell: dmesg --notime | grep -i "error\|fail" || true
+      register: result_dmesg_error
+
+    # case: check running container with podman
+    - name: run ubi8 image
+      command: podman run registry.access.redhat.com/ubi8/ubi-minimal:latest cat /etc/redhat-release
+      register: podman_result
+      become: yes
+      ignore_errors: yes  # due to https://bugzilla.redhat.com/show_bug.cgi?id=1903983
+
+    - name: run container test
+      block:
+        - assert:
+            that:
+              - podman_result is succeeded
+              - "'Red Hat Enterprise Linux release' in podman_result.stdout"
+            fail_msg: "failed run container with podman"
+            success_msg: "running container with podman successed"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check dnf package and it should not be installed
+    # https://github.com/osbuild/osbuild-composer/blob/master/internal/distro/rhel8/distro.go#L642
+    - name: dnf should not be installed
+      block:
+        - name: dnf should not be installed
+          shell: rpm -qa | grep dnf || echo -n PASS
+          register: result_dnf
+
+        - assert:
+            that:
+              - result_dnf.stdout == "PASS"
+            fail_msg: "dnf is installed"
+            success_msg: "No dnf installed"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+      when: ansible_facts['distribution'] != 'RedHat' and ansible_facts ['distribution_version'] != '8.5'
+
+    # case: check installed greenboot packages
+    # https://github.com/osbuild/osbuild-composer/blob/master/internal/distro/rhel8/distro.go#L634
+    - name: greenboot and it's related packages should be installed
+      block:
+        - name: greenboot and it's related packages should be installed
+          shell: rpm -qa | grep greenboot
+          register: result_greenboot_packages
+
+        - assert:
+            that:
+              - "'greenboot-0' in result_greenboot_packages.stdout"
+              - "'greenboot-grub2' in result_greenboot_packages.stdout"
+              - "'greenboot-rpm-ostree-grub2' in result_greenboot_packages.stdout"
+              - "'greenboot-reboot' in result_greenboot_packages.stdout"
+              - "'greenboot-status' in result_greenboot_packages.stdout"
+            fail_msg: "Some of greenboot and its related packages are not installed"
+            success_msg: "All greenboot and its related packages are installed"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check greenboot* services
+    - name: a list of greenboot* service should be enabled
+      block:
+        - name: a list of greenboot* service should be enabled
+          command: systemctl is-enabled greenboot-grub2-set-counter greenboot-grub2-set-success greenboot-healthcheck greenboot-rpm-ostree-grub2-check-fallback greenboot-status greenboot-task-runner redboot-auto-reboot redboot-task-runner
+          register: result_greenboot_service
+
+        - assert:
+            that:
+              - result_greenboot_service.stdout == 'enabled\nenabled\nenabled\nenabled\nenabled\nenabled\nenabled\nenabled'
+            fail_msg: "Some of greenboot* services are not enabled"
+            success_msg: "All greenboot* services are enabled"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check greenboot* services log
+    - name: all greenboot* service should run without error
+      block:
+        - name: all greenboot* service should run without error
+          command: journalctl -b -0 -u boot-complete.target -u greenboot -u greenboot-healthcheck -u greenboot-rpm-ostree-grub2-check-fallback -u greenboot-grub2-set-counter -u greenboot-grub2-set-success -u greenboot-status -u redboot -u redboot-auto-reboot -u redboot.target
+          register: result_greenboot_log
+
+        - assert:
+            that:
+              - "'Script \\'00_required_scripts_start.sh\\' SUCCESS' in result_greenboot_log.stdout"
+              - "'Script \\'00_wanted_scripts_start.sh\\' SUCCESS' in result_greenboot_log.stdout"
+              - "'greenboot Health Checks Runner' in result_greenboot_log.stdout"
+              - "'Reached target Boot Completion Check' in result_greenboot_log.stdout"
+              - "'Mark boot as successful in grubenv' in result_greenboot_log.stdout"
+              - "'Boot Status is GREEN - Health Check SUCCESS' in result_greenboot_log.stdout"
+              - "'greenboot MotD Generator' in result_greenboot_log.stdout"
+            fail_msg: "Some errors happened in service boot"
+            success_msg: "All greenboot services booted success"
+
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check grubenv variables
+    - name: grubenv variables should contain boot_success=1
+      block:
+        - name: grubenv variables should contain boot_success=1
+          command: grub2-editenv list
+          register: result_grubenv
+          become: yes
+
+        - assert:
+            that:
+              - "'boot_success=1' in result_grubenv.stdout"
+            fail_msg: "Not found boot_success=1"
+            success_msg: "Found boot_success=1"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check rollback function if boot error found
+    - name: install sanely failing health check unit to test red boot status behavior
+      block:
+        - name: install sanely failing health check unit to test red boot status behavior
+          command: rpm-ostree install http://file-server-virt-qe-3rd.apps.ocp4.prod.psi.redhat.com/greenboot-failing-unit-1.0-1.el8.noarch.rpm
+          become: yes
+
+        - name: reboot to deploy new ostree commit
+          reboot:
+          become: yes
+
+        - name: waits until instance is reachable
+          wait_for:
+            host: "{{ ansible_all_ipv4_addresses[0] }}"
+            port: 22
+            search_regex: OpenSSH
+            delay: 10
+          register: result_rollback
+
+        - assert:
+            that:
+              - result_rollback is succeeded
+            fail_msg: "Rollback failed"
+            success_msg: "Rollback success"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+
+    # case: check ostree commit after rollback
+    - name: check ostree commit after rollback
+      block:
+        - name: check ostree commit after rollback
+          command: rpm-ostree status --json
+          register: result_commit
+
+        - name: make a json result
+          set_fact:
+            deploy_commit: "{{ result_commit.stdout | from_json | json_query('deployments[0].checksum') }}"
+
+        - assert:
+            that:
+              - deploy_commit == ostree_commit
+            fail_msg: "Not rollback to last commit"
+            success_msg: "Rollback success"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+      when: result_rollback is succeeded
+
+    # case: check greenboot* services log again
+    - name: fallback log should be found here
+      block:
+        - name: fallback log should be found here
+          command: journalctl -b -0 -u boot-complete.target -u greenboot -u greenboot-healthcheck -u greenboot-rpm-ostree-grub2-check-fallback -u greenboot-grub2-set-counter -u greenboot-grub2-set-success -u greenboot-status -u redboot -u redboot-auto-reboot -u redboot.target
+          register: result_greenboot_log
+
+        - assert:
+            that:
+              - "'FALLBACK BOOT DETECTED! Default rpm-ostree deployment has been rolled back' in result_greenboot_log.stdout"
+              - "'Script \\'00_required_scripts_start.sh\\' SUCCESS' in result_greenboot_log.stdout"
+              - "'Script \\'00_wanted_scripts_start.sh\\' SUCCESS' in result_greenboot_log.stdout"
+              - "'greenboot Health Checks Runner' in result_greenboot_log.stdout"
+              - "'Reached target Boot Completion Check' in result_greenboot_log.stdout"
+              - "'Mark boot as successful in grubenv' in result_greenboot_log.stdout"
+              - "'Boot Status is GREEN - Health Check SUCCESS' in result_greenboot_log.stdout"
+              - "'greenboot MotD Generator' in result_greenboot_log.stdout"
+            fail_msg: "Fallback log not found"
+            success_msg: "Found fallback log"
+
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+      when: result_rollback is succeeded
+
+    # case: check grubenv variables again
+    - name: grubenv variables should contain boot_success=1
+      block:
+        - name: grubenv variables should contain boot_success=1
+          command: grub2-editenv list
+          register: result_grubenv
+          become: yes
+
+        - assert:
+            that:
+              - "'boot_success=1' in result_grubenv.stdout"
+            fail_msg: "Not found boot_success=1"
+            success_msg: "Found boot_success=1"
+      always:
+        - set_fact:
+            total_counter: "{{ total_counter | int + 1 }}"
+      rescue:
+        - name: failed count + 1
+          set_fact:
+            failed_counter: "{{ failed_counter | int + 1 }}"
+      when: result_rollback is succeeded
+
+    - assert:
+        that:
+          - failed_counter == "0"
+        fail_msg: "Run {{ total_counter }} tests, but {{ failed_counter }} of them failed"
+        success_msg: "Totally {{ total_counter }} test passed"
diff --git a/test/data/manifests/rhel_90-aarch64-edge_commit-boot.json b/test/data/manifests/rhel_90-aarch64-edge_commit-boot.json
index 2885847d6..b932d3ddb 100644
--- a/test/data/manifests/rhel_90-aarch64-edge_commit-boot.json
+++ b/test/data/manifests/rhel_90-aarch64-edge_commit-boot.json
@@ -782,7 +782,15 @@
               "enabled_services": [
                 "NetworkManager.service",
                 "firewalld.service",
-                "sshd.service"
+                "sshd.service",
+                "greenboot-grub2-set-counter",
+                "greenboot-grub2-set-success",
+                "greenboot-healthcheck",
+                "greenboot-rpm-ostree-grub2-check-fallback",
+                "greenboot-status",
+                "greenboot-task-runner",
+                "redboot-auto-reboot",
+                "redboot-task-runner"
               ]
             }
           },
@@ -8729,6 +8737,31 @@
           "actual": "system_u:object_r:mnt_t:s0",
           "expected": "system_u:object_r:default_t:s0",
           "filename": "/proc"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:container_runtime_exec_t:s0",
+          "filename": "/usr/bin/crun"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:container_runtime_exec_t:s0",
+          "filename": "/usr/bin/podman"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:container_runtime_exec_t:s0",
+          "filename": "/usr/bin/runc"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:usbguard_unit_file_t:s0",
+          "filename": "/usr/lib/systemd/system/usbguard.service"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:usbguard_exec_t:s0",
+          "filename": "/usr/sbin/usbguard-daemon"
         }
       ],
       "policy": {
@@ -8746,12 +8779,6 @@
       "exit.target",
       "fstrim.timer",
       "fwupd-refresh.timer",
-      "greenboot-grub2-set-counter.service",
-      "greenboot-grub2-set-success.service",
-      "greenboot-healthcheck.service",
-      "greenboot-rpm-ostree-grub2-check-fallback.service",
-      "greenboot-status.service",
-      "greenboot-task-runner.service",
       "halt.target",
       "kexec.target",
       "mdcheck_continue.timer",
@@ -8768,8 +8795,6 @@
       "proc-sys-fs-binfmt_misc.mount",
       "raid-check.timer",
       "rdisc.service",
-      "redboot-auto-reboot.service",
-      "redboot-task-runner.service",
       "remote-cryptsetup.target",
       "remote-veritysetup.target",
       "rpm-ostree-bootstatus.service",
@@ -8805,6 +8830,12 @@
       "dm-event.socket",
       "firewalld.service",
       "getty@.service",
+      "greenboot-grub2-set-counter.service",
+      "greenboot-grub2-set-success.service",
+      "greenboot-healthcheck.service",
+      "greenboot-rpm-ostree-grub2-check-fallback.service",
+      "greenboot-status.service",
+      "greenboot-task-runner.service",
       "import-state.service",
       "loadmodules.service",
       "lvm2-lvmpolld.socket",
@@ -8813,6 +8844,8 @@
       "nis-domainname.service",
       "ostree-remount.service",
       "reboot.target",
+      "redboot-auto-reboot.service",
+      "redboot-task-runner.service",
       "remote-fs.target",
       "runlevel6.target",
       "selinux-autorelabel-mark.service",
diff --git a/test/data/manifests/rhel_90-aarch64-edge_container-boot.json b/test/data/manifests/rhel_90-aarch64-edge_container-boot.json
index 190608e13..5771f1111 100644
--- a/test/data/manifests/rhel_90-aarch64-edge_container-boot.json
+++ b/test/data/manifests/rhel_90-aarch64-edge_container-boot.json
@@ -615,6 +615,7 @@
                   "sha256:48f54283e14169425e68b87180435123af1906bd12c6631895d48106f5e841c9",
                   "sha256:7f3874f19f5af6867d7d9ff0a34442f4d92c153c139a1c0a058c7e3ae4ce1e7a",
                   "sha256:8a46cffbaee57de4881ef8c746195cb4da090ae0544bf90cf346dcfe3f322f4d",
+                  "sha256:d12649d69a0a7088ee0663735ce9aa6f05ae710aefe1391004cb1cb424dbdf40",
                   "sha256:785dba3e8f6c225e5a6eb5ae410c03e19c890573d010a368b754bb83e606ce94",
                   "sha256:2a399fa929245800ed68c8febee80ca2e1c20a08e92f9cd8c73e2cdd3aa04490",
                   "sha256:0f72fb02c99abc767b209829f3ebba2fe62c908a5d58317d3243f17ebd183709",
@@ -781,7 +782,15 @@
               "enabled_services": [
                 "NetworkManager.service",
                 "firewalld.service",
-                "sshd.service"
+                "sshd.service",
+                "greenboot-grub2-set-counter",
+                "greenboot-grub2-set-success",
+                "greenboot-healthcheck",
+                "greenboot-rpm-ostree-grub2-check-fallback",
+                "greenboot-status",
+                "greenboot-task-runner",
+                "redboot-auto-reboot",
+                "redboot-task-runner"
               ]
             }
           },
@@ -2019,6 +2028,9 @@
           "sha256:d0b15c7dbe30b7890b86d77dcde6034409c9604809ff103ab54597f4a09aff28": {
             "url": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.0-20210827/Packages/libblockdev-fs-2.25-7.el9.aarch64.rpm"
           },
+          "sha256:d12649d69a0a7088ee0663735ce9aa6f05ae710aefe1391004cb1cb424dbdf40": {
+            "url": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.0-20210827/Packages/nss-altfiles-2.18.1-20.el9.aarch64.rpm"
+          },
           "sha256:d165d14d89664d4e60cadc3625d5a76735a8c47043d9da3e3808ed44a90aeeeb": {
             "url": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-baseos-n9.0-20210827/Packages/python3-libs-3.9.6-6.el9.aarch64.rpm"
           },
@@ -9027,6 +9039,15 @@
         "remote_location": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.0-20210827/Packages/nss-3.67.0-8.el9.aarch64.rpm",
         "checksum": "sha256:8a46cffbaee57de4881ef8c746195cb4da090ae0544bf90cf346dcfe3f322f4d"
       },
+      {
+        "name": "nss-altfiles",
+        "epoch": 0,
+        "version": "2.18.1",
+        "release": "20.el9",
+        "arch": "aarch64",
+        "remote_location": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.0-20210827/Packages/nss-altfiles-2.18.1-20.el9.aarch64.rpm",
+        "checksum": "sha256:d12649d69a0a7088ee0663735ce9aa6f05ae710aefe1391004cb1cb424dbdf40"
+      },
       {
         "name": "nss-softokn",
         "epoch": 0,
diff --git a/test/data/manifests/rhel_90-x86_64-edge_commit-boot.json b/test/data/manifests/rhel_90-x86_64-edge_commit-boot.json
index 2b0f4874b..b028104b0 100644
--- a/test/data/manifests/rhel_90-x86_64-edge_commit-boot.json
+++ b/test/data/manifests/rhel_90-x86_64-edge_commit-boot.json
@@ -817,7 +817,15 @@
               "enabled_services": [
                 "NetworkManager.service",
                 "firewalld.service",
-                "sshd.service"
+                "sshd.service",
+                "greenboot-grub2-set-counter",
+                "greenboot-grub2-set-success",
+                "greenboot-healthcheck",
+                "greenboot-rpm-ostree-grub2-check-fallback",
+                "greenboot-status",
+                "greenboot-task-runner",
+                "redboot-auto-reboot",
+                "redboot-task-runner"
               ]
             }
           },
@@ -9085,6 +9093,16 @@
           "actual": "system_u:object_r:mnt_t:s0",
           "expected": "system_u:object_r:default_t:s0",
           "filename": "/proc"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:usbguard_unit_file_t:s0",
+          "filename": "/usr/lib/systemd/system/usbguard.service"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:usbguard_exec_t:s0",
+          "filename": "/usr/sbin/usbguard-daemon"
         }
       ],
       "policy": {
@@ -9102,12 +9120,6 @@
       "exit.target",
       "fstrim.timer",
       "fwupd-refresh.timer",
-      "greenboot-grub2-set-counter.service",
-      "greenboot-grub2-set-success.service",
-      "greenboot-healthcheck.service",
-      "greenboot-rpm-ostree-grub2-check-fallback.service",
-      "greenboot-status.service",
-      "greenboot-task-runner.service",
       "halt.target",
       "kexec.target",
       "mdcheck_continue.timer",
@@ -9124,8 +9136,6 @@
       "proc-sys-fs-binfmt_misc.mount",
       "raid-check.timer",
       "rdisc.service",
-      "redboot-auto-reboot.service",
-      "redboot-task-runner.service",
       "remote-cryptsetup.target",
       "remote-veritysetup.target",
       "rpm-ostree-bootstatus.service",
@@ -9161,6 +9171,12 @@
       "dm-event.socket",
       "firewalld.service",
       "getty@.service",
+      "greenboot-grub2-set-counter.service",
+      "greenboot-grub2-set-success.service",
+      "greenboot-healthcheck.service",
+      "greenboot-rpm-ostree-grub2-check-fallback.service",
+      "greenboot-status.service",
+      "greenboot-task-runner.service",
       "import-state.service",
       "loadmodules.service",
       "lvm2-lvmpolld.socket",
@@ -9170,6 +9186,8 @@
       "nis-domainname.service",
       "ostree-remount.service",
       "reboot.target",
+      "redboot-auto-reboot.service",
+      "redboot-task-runner.service",
       "remote-fs.target",
       "runlevel6.target",
       "selinux-autorelabel-mark.service",
diff --git a/test/data/manifests/rhel_90-x86_64-edge_commit_rt-boot.json b/test/data/manifests/rhel_90-x86_64-edge_commit_rt-boot.json
index 4e2dac923..9a1becf58 100644
--- a/test/data/manifests/rhel_90-x86_64-edge_commit_rt-boot.json
+++ b/test/data/manifests/rhel_90-x86_64-edge_commit_rt-boot.json
@@ -1025,7 +1025,15 @@
               "enabled_services": [
                 "NetworkManager.service",
                 "firewalld.service",
-                "sshd.service"
+                "sshd.service",
+                "greenboot-grub2-set-counter",
+                "greenboot-grub2-set-success",
+                "greenboot-healthcheck",
+                "greenboot-rpm-ostree-grub2-check-fallback",
+                "greenboot-status",
+                "greenboot-task-runner",
+                "redboot-auto-reboot",
+                "redboot-task-runner"
               ]
             }
           },
@@ -11322,6 +11330,16 @@
           "actual": "system_u:object_r:mnt_t:s0",
           "expected": "system_u:object_r:default_t:s0",
           "filename": "/proc"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:usbguard_unit_file_t:s0",
+          "filename": "/usr/lib/systemd/system/usbguard.service"
+        },
+        {
+          "actual": "system_u:object_r:unlabeled_t:s0",
+          "expected": "system_u:object_r:usbguard_exec_t:s0",
+          "filename": "/usr/sbin/usbguard-daemon"
         }
       ],
       "policy": {
@@ -11340,12 +11358,6 @@
       "exit.target",
       "fstrim.timer",
       "fwupd-refresh.timer",
-      "greenboot-grub2-set-counter.service",
-      "greenboot-grub2-set-success.service",
-      "greenboot-healthcheck.service",
-      "greenboot-rpm-ostree-grub2-check-fallback.service",
-      "greenboot-status.service",
-      "greenboot-task-runner.service",
       "halt.target",
       "kexec.target",
       "kvm_stat.service",
@@ -11365,8 +11377,6 @@
       "rdisc.service",
       "realtime-entsk.service",
       "realtime-setup.service",
-      "redboot-auto-reboot.service",
-      "redboot-task-runner.service",
       "remote-cryptsetup.target",
       "remote-veritysetup.target",
       "rhsm-facts.service",
@@ -11405,6 +11415,12 @@
       "dnf-makecache.timer",
       "firewalld.service",
       "getty@.service",
+      "greenboot-grub2-set-counter.service",
+      "greenboot-grub2-set-success.service",
+      "greenboot-healthcheck.service",
+      "greenboot-rpm-ostree-grub2-check-fallback.service",
+      "greenboot-status.service",
+      "greenboot-task-runner.service",
       "import-state.service",
       "kdump.service",
       "loadmodules.service",
@@ -11415,6 +11431,8 @@
       "nis-domainname.service",
       "ostree-remount.service",
       "reboot.target",
+      "redboot-auto-reboot.service",
+      "redboot-task-runner.service",
       "remote-fs.target",
       "rhsmcertd.service",
       "runlevel6.target",
diff --git a/test/data/manifests/rhel_90-x86_64-edge_container-boot.json b/test/data/manifests/rhel_90-x86_64-edge_container-boot.json
index 56e4e2018..42b433657 100644
--- a/test/data/manifests/rhel_90-x86_64-edge_container-boot.json
+++ b/test/data/manifests/rhel_90-x86_64-edge_container-boot.json
@@ -650,6 +650,7 @@
                   "sha256:731b91b14eb7bc79961d02940db4fe2f8644c6c5d5005bd3c62cc3830443b692",
                   "sha256:22bee502caf00522d7e3733699502e9d109d1a884c3006ee212e900101b07231",
                   "sha256:ad91a2693eae9b8159810a59526a22e0f9518d79d4845f81531331dd6cbac36d",
+                  "sha256:0178538e8a7935d1418e78f74c58880239ad3e8eab94638af562a128ad133e20",
                   "sha256:f31d17d77b9b35e6907b1803e47cede1d3d552d5cb45d46d940d4bd57d28dac5",
                   "sha256:925973b321b7f87ddba9ca957f75ba6585d95a1b2604fe880a05ef1d3621c50a",
                   "sha256:6e5a2b51da0bfecbbc0f11fc023eb17f4fcaf70f23ada1294ba4be8b33bda125",
@@ -816,7 +817,15 @@
               "enabled_services": [
                 "NetworkManager.service",
                 "firewalld.service",
-                "sshd.service"
+                "sshd.service",
+                "greenboot-grub2-set-counter",
+                "greenboot-grub2-set-success",
+                "greenboot-healthcheck",
+                "greenboot-rpm-ostree-grub2-check-fallback",
+                "greenboot-status",
+                "greenboot-task-runner",
+                "redboot-auto-reboot",
+                "redboot-task-runner"
               ]
             }
           },
@@ -1077,6 +1086,9 @@
           "sha256:01619e0b96181bff3b990016a5857b9d8859b7a4887d8c120b6f60c688bac526": {
             "url": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.0-20210827/Packages/npth-1.6-8.el9.x86_64.rpm"
           },
+          "sha256:0178538e8a7935d1418e78f74c58880239ad3e8eab94638af562a128ad133e20": {
+            "url": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.0-20210827/Packages/nss-altfiles-2.18.1-20.el9.x86_64.rpm"
+          },
           "sha256:02c6622480af52421fc2e54be42b8dd1ce6502e237c9a4e8231ae587b49767e3": {
             "url": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.0-20210827/Packages/zlib-1.2.11-31.el9.x86_64.rpm"
           },
@@ -9378,6 +9390,15 @@
         "remote_location": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.0-20210827/Packages/nss-3.67.0-8.el9.x86_64.rpm",
         "checksum": "sha256:ad91a2693eae9b8159810a59526a22e0f9518d79d4845f81531331dd6cbac36d"
       },
+      {
+        "name": "nss-altfiles",
+        "epoch": 0,
+        "version": "2.18.1",
+        "release": "20.el9",
+        "arch": "x86_64",
+        "remote_location": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.0-20210827/Packages/nss-altfiles-2.18.1-20.el9.x86_64.rpm",
+        "checksum": "sha256:0178538e8a7935d1418e78f74c58880239ad3e8eab94638af562a128ad133e20"
+      },
       {
         "name": "nss-softokn",
         "epoch": 0,
@@ -9649,6 +9670,5 @@
         "checksum": "sha256:b6b91777247881481f67ca222ca83e18c5649194e00f40fca8a6297d191bb261"
       }
     ]
-  },
-  "image-info": {}
+  }
 }