From ccfcaccea9bbb4723a0847238c819107e044cace Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sat, 16 Sep 2023 21:26:30 +0200
Subject: [PATCH 01/20] Rename variable

---
 cmd/utils.go | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/cmd/utils.go b/cmd/utils.go
index bc8115d1..90cdd1c4 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -44,7 +44,9 @@ var (
 )
 
 // generateConfig generates a config file of the given type.
-func generateConfig(cmd *cobra.Command, fileType configFileType, configFile string, force bool) {
+func generateConfig(
+	cmd *cobra.Command, fileType configFileType, configFile string, forceRewriteFile bool,
+) {
 	logger := log.New(cmd.OutOrStdout(), "", 0)
 
 	// Create a new config object and load the defaults.
@@ -71,7 +73,7 @@ func generateConfig(cmd *cobra.Command, fileType configFileType, configFile stri
 
 	// Check if the config file already exists and if we should overwrite it.
 	exists := false
-	if _, err := os.Stat(configFile); err == nil && !force {
+	if _, err := os.Stat(configFile); err == nil && !forceRewriteFile {
 		logger.Fatal(
 			"Config file already exists. Use --force to overwrite or choose a different filename.")
 	} else if err == nil {
@@ -84,7 +86,7 @@ func generateConfig(cmd *cobra.Command, fileType configFileType, configFile stri
 	}
 
 	verb := "created"
-	if exists && force {
+	if exists && forceRewriteFile {
 		verb = "overwritten"
 	}
 	logger.Printf("Config file '%s' was %s successfully.", configFile, verb)

From ee575e254ddc1598185792bc955212c284a0eaf5 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sat, 16 Sep 2023 21:30:39 +0200
Subject: [PATCH 02/20] Update all dependencies

---
 go.mod | 28 ++++++++++++++--------------
 go.sum | 58 ++++++++++++++++++++++++++++------------------------------
 2 files changed, 42 insertions(+), 44 deletions(-)

diff --git a/go.mod b/go.mod
index 3498d55d..a0f702e8 100644
--- a/go.mod
+++ b/go.mod
@@ -8,17 +8,17 @@ require (
 	github.com/codingsince1985/checksum v1.3.0
 	github.com/envoyproxy/protoc-gen-validate v1.0.2
 	github.com/gatewayd-io/gatewayd-plugin-sdk v0.1.1
-	github.com/getsentry/sentry-go v0.24.0
+	github.com/getsentry/sentry-go v0.24.1
 	github.com/go-co-op/gocron v1.33.1
 	github.com/google/go-cmp v0.5.9
 	github.com/google/go-github/v53 v53.2.0
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0
 	github.com/hashicorp/go-hclog v1.5.0
 	github.com/hashicorp/go-plugin v1.5.1
 	github.com/invopop/jsonschema v0.8.0
 	github.com/knadh/koanf v1.5.0
 	github.com/mitchellh/mapstructure v1.5.0
-	github.com/panjf2000/gnet/v2 v2.3.1
+	github.com/panjf2000/gnet/v2 v2.3.2
 	github.com/prometheus/client_golang v1.16.0
 	github.com/prometheus/client_model v0.4.0
 	github.com/prometheus/common v0.44.0
@@ -27,14 +27,14 @@ require (
 	github.com/spf13/cobra v1.7.0
 	github.com/stretchr/testify v1.8.4
 	github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04
-	go.opentelemetry.io/otel v1.17.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0
-	go.opentelemetry.io/otel/sdk v1.17.0
-	go.opentelemetry.io/otel/trace v1.17.0
+	go.opentelemetry.io/otel v1.18.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0
+	go.opentelemetry.io/otel/sdk v1.18.0
+	go.opentelemetry.io/otel/trace v1.18.0
 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9
-	google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d
-	google.golang.org/grpc v1.58.0
+	google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb
+	google.golang.org/grpc v1.58.1
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -71,11 +71,11 @@ require (
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	go.opentelemetry.io/otel/metric v1.17.0 // indirect
+	go.opentelemetry.io/otel/metric v1.18.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	go.uber.org/zap v1.25.0 // indirect
+	go.uber.org/zap v1.26.0 // indirect
 	golang.org/x/crypto v0.13.0 // indirect
 	golang.org/x/net v0.15.0 // indirect
 	golang.org/x/oauth2 v0.12.0 // indirect
@@ -83,6 +83,6 @@ require (
 	golang.org/x/sys v0.12.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
+	google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect
 )
diff --git a/go.sum b/go.sum
index a65e999f..a731b08c 100644
--- a/go.sum
+++ b/go.sum
@@ -27,8 +27,6 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72H
 github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
 github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
 github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
-github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -78,8 +76,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/gatewayd-io/gatewayd-plugin-sdk v0.1.1 h1:ujlh6TSDFmG2e9VtY6hZk8BW0p7i0AEQL3BpMMLzxwc=
 github.com/gatewayd-io/gatewayd-plugin-sdk v0.1.1/go.mod h1:B4oWVHf7NeSCs7szN8nrlIO6tkznV1F3ZMqE9VxDtKY=
-github.com/getsentry/sentry-go v0.24.0 h1:02b7qEmJ56EHGe9KFgjArjU/vG/aywm7Efgu+iPc01Y=
-github.com/getsentry/sentry-go v0.24.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
+github.com/getsentry/sentry-go v0.24.1 h1:W6/0GyTy8J6ge6lVCc94WB6Gx2ZuLrgopnn9w8Hiwuk=
+github.com/getsentry/sentry-go v0.24.1/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-co-op/gocron v1.33.1 h1:wjX+Dg6Ae29a/f9BSQjY1Rl+jflTpW9aDyMqseCj78c=
 github.com/go-co-op/gocron v1.33.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no=
@@ -146,8 +144,8 @@ github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
 github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1 h1:LSsiG61v9IzzxMkqEr6nrix4miJI62xlRjwT7BYD2SM=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1/go.mod h1:Hbb13e3/WtqQ8U5hLGkek9gJvBLasHuPFI0UEGfnQ10=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk=
 github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
 github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -278,8 +276,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
 github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
 github.com/panjf2000/ants/v2 v2.8.1 h1:C+n/f++aiW8kHCExKlpX6X+okmxKXP7DWLutxuAPuwQ=
 github.com/panjf2000/ants/v2 v2.8.1/go.mod h1:KIBmYG9QQX5U2qzFP/yQJaq/nSb6rahS9iEHkrCMgM8=
-github.com/panjf2000/gnet/v2 v2.3.1 h1:J7vHkNxwsevVIw3u/6LCXgcnpGBk5iKqhQ2RMblGodc=
-github.com/panjf2000/gnet/v2 v2.3.1/go.mod h1:Ik5lTy2nmBg9Uvjfcf2KRYs+EXVNOLyxPHpFOFlqu+M=
+github.com/panjf2000/gnet/v2 v2.3.2 h1:cwzq4S2fZbHvBaGriMlZTDtiFL/EzaaVny2V03wOuj0=
+github.com/panjf2000/gnet/v2 v2.3.2/go.mod h1:jQ0+i/ZSs4wxUKl06sgjWE0bL/yWI1d5LkNdqulZXFc=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
@@ -372,18 +370,18 @@ github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQ
 go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
 go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
-go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM=
-go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 h1:U5GYackKpVKlPrd/5gKMlrTlP2dCESAAFU682VCpieY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0/go.mod h1:aFsJfCEnLzEu9vRRAcUiB/cpRTbVsNdF3OHSPpdjxZQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 h1:iGeIsSYwpYSvh5UGzWrJfTDJvPjrXtxl3GUppj6IXQU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0/go.mod h1:1j3H3G1SBYpZFti6OI4P0uRQCW20MXkG5v4UWXppLLE=
-go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc=
-go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o=
-go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE=
-go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ=
-go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ=
-go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY=
+go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs=
+go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0 h1:yE32ay7mJG2leczfREEhoW3VfSZIvHaB+gvVo1o8DQ8=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0/go.mod h1:G17FHPDLt74bCI7tJ4CMitEk4BXTYG4FW6XUpkPBXa4=
+go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ=
+go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k=
+go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY=
+go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M=
+go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10=
+go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0=
 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
 go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -396,8 +394,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
-go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
@@ -552,12 +550,12 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
-google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
-google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
-google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
+google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA=
+google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
+google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI=
+google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb h1:Isk1sSH7bovx8Rti2wZK0UZF6oraBDK74uoyLEEVFN0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
 google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
@@ -566,8 +564,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o=
-google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/grpc v1.58.1 h1:OL+Vz23DTtrrldqHK49FUOPHyY75rvFqJfXC84NYW58=
+google.golang.org/grpc v1.58.1/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

From b9af48bbd4be51cdc6095c2d61ebb800071536a6 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sat, 16 Sep 2023 21:51:48 +0200
Subject: [PATCH 03/20] Add test for ticker

---
 network/network_helpers_test.go | 4 +++-
 network/server_test.go          | 1 +
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/network/network_helpers_test.go b/network/network_helpers_test.go
index 6cc09526..766c0315 100644
--- a/network/network_helpers_test.go
+++ b/network/network_helpers_test.go
@@ -112,7 +112,7 @@ func CollectAndComparePrometheusMetrics(t *testing.T) {
 			gatewayd_bytes_sent_to_server_sum 282
 			gatewayd_bytes_sent_to_server_count 5
 			gatewayd_client_connections 1
-			gatewayd_plugin_hooks_executed_total 10
+			gatewayd_plugin_hooks_executed_total 11
 			gatewayd_plugin_hooks_registered_total 0
 			gatewayd_plugins_loaded_total 0
 			gatewayd_proxied_connections 1
@@ -122,6 +122,7 @@ func CollectAndComparePrometheusMetrics(t *testing.T) {
 			gatewayd_server_connections 5
 			gatewayd_traffic_bytes_sum 182
 			gatewayd_traffic_bytes_count 4
+			gatewayd_server_ticks_fired_total 1
 		`
 
 		metrics = []string{
@@ -139,6 +140,7 @@ func CollectAndComparePrometheusMetrics(t *testing.T) {
 			"gatewayd_proxy_passthroughs_total",
 			"gatewayd_server_connections",
 			"gatewayd_traffic_bytes",
+			"gatewayd_server_ticks_fired_total",
 		}
 	)
 	assert.NoError(t,
diff --git a/network/server_test.go b/network/server_test.go
index 3c2f62b4..79da4e57 100644
--- a/network/server_test.go
+++ b/network/server_test.go
@@ -170,6 +170,7 @@ func TestRunServer(t *testing.T) {
 			gnet.WithMulticore(false),
 			gnet.WithReuseAddr(true),
 			gnet.WithReusePort(true),
+			gnet.WithTicker(true), // Enable ticker.
 		},
 		proxy,
 		logger,

From 0a588174f3c29c940b1882a099068cff18e5504a Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sat, 16 Sep 2023 22:40:52 +0200
Subject: [PATCH 04/20] Check for contents of logs

---
 network/server_test.go | 35 ++++++++++++++++++++++++++++++++---
 1 file changed, 32 insertions(+), 3 deletions(-)

diff --git a/network/server_test.go b/network/server_test.go
index 79da4e57..86d4c6e9 100644
--- a/network/server_test.go
+++ b/network/server_test.go
@@ -1,8 +1,11 @@
 package network
 
 import (
+	"bufio"
 	"context"
 	"errors"
+	"io"
+	"os"
 	"testing"
 
 	v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1"
@@ -21,11 +24,15 @@ func TestRunServer(t *testing.T) {
 	errs := make(chan error)
 
 	logger := logging.NewLogger(context.Background(), logging.LoggerConfig{
-		Output:            []config.LogOutput{config.Console},
+		Output: []config.LogOutput{
+			config.Console,
+			config.File,
+		},
 		TimeFormat:        zerolog.TimeFormatUnix,
 		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
-		Level:             zerolog.ErrorLevel,
+		Level:             zerolog.DebugLevel,
 		NoColor:           true,
+		FileName:          "server_test.log",
 	})
 
 	pluginRegistry := plugin.NewRegistry(
@@ -54,7 +61,8 @@ func TestRunServer(t *testing.T) {
 		} else {
 			errs <- errors.New("request is not a []byte") //nolint:goerr113
 		}
-		assert.Empty(t, paramsMap["error"])
+		assert.Empty(t, paramsMap["error"], "The error MUST be empty.")
+
 		return params, nil
 	}
 	pluginRegistry.AddHook(v1.HookName_HOOK_NAME_ON_TRAFFIC_FROM_CLIENT, 1, onTrafficFromClient)
@@ -184,6 +192,27 @@ func TestRunServer(t *testing.T) {
 			errs <- err
 		}
 		close(errs)
+
+		// Read the log file and check if the log file contains the expected log messages.
+		if _, err := os.Stat("server_test.log"); err == nil {
+			logFile, err := os.Open("server_test.log")
+			assert.NoError(t, err)
+			defer logFile.Close()
+
+			reader := bufio.NewReader(logFile)
+			assert.NotNil(t, reader)
+
+			buffer, err := io.ReadAll(reader)
+			assert.NoError(t, err)
+			assert.Greater(t, len(buffer), 0) // The log file should not be empty.
+
+			logLines := string(buffer)
+			assert.Contains(t, logLines, "GatewayD is running", "GatewayD should be running")
+			assert.Contains(t, logLines, "GatewayD is ticking...", "GatewayD should be ticking")
+			assert.Contains(t, logLines, "Ingress traffic", "Ingress traffic should be logged")
+			assert.Contains(t, logLines, "Egress traffic", "Egress traffic should be logged")
+			assert.Contains(t, logLines, "GatewayD is shutting down...", "GatewayD should be shutting down")
+		}
 	}(server, errs)
 
 	//nolint:thelper

From 3a2b1e8f8c2c09926c5ff8c86046d7feaeb547d7 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sat, 16 Sep 2023 22:53:08 +0200
Subject: [PATCH 05/20] Fix time in console output

---
 logging/hclog_adapter_test.go  | 3 ++-
 logging/logger_test.go         | 3 ++-
 metrics/merger_test.go         | 2 +-
 network/client_test.go         | 3 ++-
 network/proxy_test.go          | 5 +++--
 network/server_test.go         | 3 ++-
 network/utils_test.go          | 5 +++--
 plugin/plugin_registry_test.go | 3 ++-
 8 files changed, 17 insertions(+), 10 deletions(-)

diff --git a/logging/hclog_adapter_test.go b/logging/hclog_adapter_test.go
index 7ac2c78d..48fc1b4e 100644
--- a/logging/hclog_adapter_test.go
+++ b/logging/hclog_adapter_test.go
@@ -3,6 +3,7 @@ package logging
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/gatewayd-io/gatewayd/config"
 	"github.com/hashicorp/go-hclog"
@@ -20,7 +21,7 @@ func TestNewHcLogAdapter(t *testing.T) {
 				Output:            []config.LogOutput{config.Console},
 				Level:             zerolog.TraceLevel,
 				TimeFormat:        zerolog.TimeFormatUnix,
-				ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+				ConsoleTimeFormat: time.RFC3339,
 				NoColor:           true,
 			},
 		)
diff --git a/logging/logger_test.go b/logging/logger_test.go
index ea9444ca..1e6ab381 100644
--- a/logging/logger_test.go
+++ b/logging/logger_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"os"
 	"testing"
+	"time"
 
 	"github.com/gatewayd-io/gatewayd/config"
 	"github.com/rs/zerolog"
@@ -40,7 +41,7 @@ func TestNewLogger_File(t *testing.T) {
 		LoggerConfig{
 			Output:            []config.LogOutput{config.File},
 			FileName:          "gatewayd.log",
-			ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+			ConsoleTimeFormat: time.RFC3339,
 			MaxSize:           config.DefaultMaxSize,
 			MaxBackups:        config.DefaultMaxBackups,
 			MaxAge:            config.DefaultMaxAge,
diff --git a/metrics/merger_test.go b/metrics/merger_test.go
index 8edb17d8..a792ecdf 100644
--- a/metrics/merger_test.go
+++ b/metrics/merger_test.go
@@ -22,7 +22,7 @@ func TestMerger(t *testing.T) {
 		logging.LoggerConfig{
 			Output:            []config.LogOutput{config.Console},
 			TimeFormat:        zerolog.TimeFormatUnix,
-			ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+			ConsoleTimeFormat: time.RFC3339,
 			Level:             zerolog.InfoLevel,
 			NoColor:           true,
 		},
diff --git a/network/client_test.go b/network/client_test.go
index 2ae1d18a..e82917ba 100644
--- a/network/client_test.go
+++ b/network/client_test.go
@@ -3,6 +3,7 @@ package network
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/gatewayd-io/gatewayd/config"
 	"github.com/gatewayd-io/gatewayd/logging"
@@ -17,7 +18,7 @@ func CreateNewClient(t *testing.T) *Client {
 	logger := logging.NewLogger(context.Background(), logging.LoggerConfig{
 		Output:            []config.LogOutput{config.Console},
 		TimeFormat:        zerolog.TimeFormatUnix,
-		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+		ConsoleTimeFormat: time.RFC3339,
 		Level:             zerolog.DebugLevel,
 		NoColor:           true,
 	})
diff --git a/network/proxy_test.go b/network/proxy_test.go
index e1603e83..b7a50c29 100644
--- a/network/proxy_test.go
+++ b/network/proxy_test.go
@@ -3,6 +3,7 @@ package network
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/gatewayd-io/gatewayd/config"
 	"github.com/gatewayd-io/gatewayd/logging"
@@ -17,7 +18,7 @@ func TestNewProxy(t *testing.T) {
 	logger := logging.NewLogger(context.Background(), logging.LoggerConfig{
 		Output:            []config.LogOutput{config.Console},
 		TimeFormat:        zerolog.TimeFormatUnix,
-		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+		ConsoleTimeFormat: time.RFC3339,
 		Level:             zerolog.DebugLevel,
 		NoColor:           true,
 	})
@@ -80,7 +81,7 @@ func TestNewProxyElastic(t *testing.T) {
 	logger := logging.NewLogger(context.Background(), logging.LoggerConfig{
 		Output:            []config.LogOutput{config.Console},
 		TimeFormat:        zerolog.TimeFormatUnix,
-		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+		ConsoleTimeFormat: time.RFC3339,
 		Level:             zerolog.DebugLevel,
 		NoColor:           true,
 	})
diff --git a/network/server_test.go b/network/server_test.go
index 86d4c6e9..9665469e 100644
--- a/network/server_test.go
+++ b/network/server_test.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"os"
 	"testing"
+	"time"
 
 	v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1"
 	"github.com/gatewayd-io/gatewayd/config"
@@ -29,7 +30,7 @@ func TestRunServer(t *testing.T) {
 			config.File,
 		},
 		TimeFormat:        zerolog.TimeFormatUnix,
-		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+		ConsoleTimeFormat: time.RFC3339,
 		Level:             zerolog.DebugLevel,
 		NoColor:           true,
 		FileName:          "server_test.log",
diff --git a/network/utils_test.go b/network/utils_test.go
index 4fb8b374..6fe5e595 100644
--- a/network/utils_test.go
+++ b/network/utils_test.go
@@ -3,6 +3,7 @@ package network
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/gatewayd-io/gatewayd/config"
 	"github.com/gatewayd-io/gatewayd/logging"
@@ -15,7 +16,7 @@ func TestGetID(t *testing.T) {
 	cfg := logging.LoggerConfig{
 		Output:            []config.LogOutput{config.Console},
 		TimeFormat:        zerolog.TimeFormatUnix,
-		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+		ConsoleTimeFormat: time.RFC3339,
 		Level:             zerolog.DebugLevel,
 		NoColor:           true,
 	}
@@ -30,7 +31,7 @@ func TestResolve(t *testing.T) {
 	cfg := logging.LoggerConfig{
 		Output:            []config.LogOutput{config.Console},
 		TimeFormat:        zerolog.TimeFormatUnix,
-		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+		ConsoleTimeFormat: time.RFC3339,
 		Level:             zerolog.DebugLevel,
 		NoColor:           true,
 	}
diff --git a/plugin/plugin_registry_test.go b/plugin/plugin_registry_test.go
index 19c78ec5..ab73342b 100644
--- a/plugin/plugin_registry_test.go
+++ b/plugin/plugin_registry_test.go
@@ -3,6 +3,7 @@ package plugin
 import (
 	"context"
 	"testing"
+	"time"
 
 	sdkPlugin "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin"
 	v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1"
@@ -19,7 +20,7 @@ func NewPluginRegistry(t *testing.T) *Registry {
 	cfg := logging.LoggerConfig{
 		Output:            []config.LogOutput{config.Console},
 		TimeFormat:        zerolog.TimeFormatUnix,
-		ConsoleTimeFormat: config.DefaultConsoleTimeFormat,
+		ConsoleTimeFormat: time.RFC3339,
 		Level:             zerolog.DebugLevel,
 		NoColor:           true,
 	}

From 2a749cb3a28b19a1113fa2b13416c719686455d9 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 00:13:32 +0200
Subject: [PATCH 06/20] Pause for gracefull start and stop of the server

---
 network/server_test.go | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/network/server_test.go b/network/server_test.go
index 9665469e..c0c28055 100644
--- a/network/server_test.go
+++ b/network/server_test.go
@@ -220,6 +220,9 @@ func TestRunServer(t *testing.T) {
 	go func(t *testing.T, server *Server, proxy *Proxy) {
 		for {
 			if server.IsRunning() {
+				// Pause for a while to allow the server to start.
+				time.Sleep(500 * time.Millisecond)
+
 				client := NewClient(
 					context.Background(),
 					&config.Client{
@@ -262,8 +265,10 @@ func TestRunServer(t *testing.T) {
 				CollectAndComparePrometheusMetrics(t)
 
 				// Clean up.
-				server.Shutdown()
 				client.Close()
+				// Pause for a while to allow the server to disconnect and shutdown.
+				time.Sleep(500 * time.Millisecond)
+				server.Shutdown()
 				break
 			}
 		}

From 2fe03b246481601d5accb58276bab337651336b9 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 00:08:22 +0200
Subject: [PATCH 07/20] Add more tests to HcLogAdapter

---
 logging/hclog_adapter_test.go | 55 +++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/logging/hclog_adapter_test.go b/logging/hclog_adapter_test.go
index 48fc1b4e..b34235ea 100644
--- a/logging/hclog_adapter_test.go
+++ b/logging/hclog_adapter_test.go
@@ -84,3 +84,58 @@ func TestNewHcLogAdapter_LogLevel_Difference(t *testing.T) {
 	assert.Contains(t, consoleOutput, "ERR This is an error message")
 	assert.NotContains(t, consoleOutput, "DBG This is a log message, but it should not be logged")
 }
+
+// TestNewHcLogAdapter_Log tests the HcLogAdapter.Log method.
+func TestNewHcLogAdapter_Log(t *testing.T) {
+	consoleOutput := capturer.CaptureStdout(func() {
+		logger := NewLogger(
+			context.Background(),
+			LoggerConfig{
+				Output:     []config.LogOutput{config.Console},
+				Level:      zerolog.TraceLevel,
+				TimeFormat: zerolog.TimeFormatUnix,
+				NoColor:    true,
+			},
+		)
+
+		hcLogAdapter := NewHcLogAdapter(&logger, "test")
+		hcLogAdapter.SetLevel(hclog.Trace)
+
+		hcLogAdapter.Log(hclog.Off, "This is a message")
+		hcLogAdapter.Log(hclog.NoLevel, "This is yet another message")
+		hcLogAdapter.Log(hclog.Trace, "This is a trace message")
+		hcLogAdapter.Log(hclog.Debug, "This is a debug message")
+		hcLogAdapter.Log(hclog.Info, "This is an info message")
+		hcLogAdapter.Log(hclog.Warn, "This is a warn message")
+		hcLogAdapter.Log(hclog.Error, "This is an error message")
+	})
+
+	assert.NotContains(t, consoleOutput, "This is a message")
+	assert.NotContains(t, consoleOutput, "This is yet another message")
+	assert.Contains(t, consoleOutput, "TRC This is a trace message")
+	assert.Contains(t, consoleOutput, "DBG This is a debug message")
+	assert.Contains(t, consoleOutput, "INF This is an info message")
+	assert.Contains(t, consoleOutput, "WRN This is a warn message")
+	assert.Contains(t, consoleOutput, "ERR This is an error message")
+}
+
+func TestNewHcLogAdapter_GetLevel(t *testing.T) {
+	logger := NewLogger(
+		context.Background(),
+		LoggerConfig{
+			Output:     []config.LogOutput{config.Console},
+			Level:      zerolog.TraceLevel,
+			TimeFormat: zerolog.TimeFormatUnix,
+			NoColor:    true,
+		},
+	)
+
+	hcLogAdapter := NewHcLogAdapter(&logger, "test")
+	hcLogAdapter.SetLevel(hclog.Trace)
+	assert.Equal(t, hclog.Trace, hcLogAdapter.GetLevel())
+
+	hcLogAdapter.SetLevel(hclog.Debug)
+	assert.Equal(t, hclog.Debug, hcLogAdapter.GetLevel())
+	assert.NotEqual(t, zerolog.DebugLevel, logger.GetLevel(),
+		"The logger should not be affected by the hclog adapter's level")
+}

From 5c27893af935fe1cf4504956e79d1a1cf81c41d7 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 00:17:29 +0200
Subject: [PATCH 08/20] Add test for pool.Cap

---
 pool/pool_test.go | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/pool/pool_test.go b/pool/pool_test.go
index b36efc4d..e406d11e 100644
--- a/pool/pool_test.go
+++ b/pool/pool_test.go
@@ -206,3 +206,21 @@ func TestPool_GetClientIDs(t *testing.T) {
 	assert.Contains(t, ids, "client2.ID")
 	pool.Clear()
 }
+
+func TestPool_Cap(t *testing.T) {
+	pool := NewPool(context.Background(), 1)
+	assert.NotNil(t, pool)
+	assert.NotNil(t, pool.Pool())
+	assert.Equal(t, 0, pool.Size())
+	assert.Equal(t, 1, pool.Cap())
+	err := pool.Put("client1.ID", "client1")
+	assert.Nil(t, err)
+	assert.Equal(t, 1, pool.Size())
+	err = pool.Put("client2.ID", "client2")
+	assert.NotNil(t, err)
+	assert.Equal(t, 1, pool.Size())
+	assert.Equal(t, 1, pool.Cap())
+	pool.Clear()
+	assert.Equal(t, 0, pool.Size())
+	assert.Equal(t, 1, pool.Cap())
+}

From f1d1ea766bacc6f75ad79585baa9f7e52051427d Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 00:52:33 +0200
Subject: [PATCH 09/20] Convert nested array to primitive types

Add tests for CastToPrimitiveTypes and NewCommand
---
 plugin/utils.go      | 14 +++++++++++++
 plugin/utils_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 63 insertions(+)

diff --git a/plugin/utils.go b/plugin/utils.go
index e7771f4d..52e9e667 100644
--- a/plugin/utils.go
+++ b/plugin/utils.go
@@ -34,10 +34,24 @@ func CastToPrimitiveTypes(args map[string]interface{}) map[string]interface{} {
 	for key, value := range args {
 		switch value := value.(type) {
 		case time.Duration:
+			// Cast time.Duration to string.
 			args[key] = value.String()
 		case map[string]interface{}:
 			// Recursively cast nested maps.
 			args[key] = CastToPrimitiveTypes(value)
+		case []interface{}:
+			// Recursively cast nested arrays.
+			array := make([]interface{}, len(value))
+			for idx, v := range value {
+				result := v
+				if v, ok := v.(time.Duration); ok {
+					// Cast time.Duration to string.
+					array[idx] = v.String()
+				} else {
+					array[idx] = result
+				}
+			}
+			args[key] = array
 		// TODO: Add more types here as needed.
 		default:
 			args[key] = value
diff --git a/plugin/utils_test.go b/plugin/utils_test.go
index 0e3e695a..5c0d12ea 100644
--- a/plugin/utils_test.go
+++ b/plugin/utils_test.go
@@ -2,6 +2,7 @@ package plugin
 
 import (
 	"testing"
+	"time"
 
 	v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1"
 	"github.com/stretchr/testify/assert"
@@ -73,3 +74,51 @@ func Test_Verify_fail(t *testing.T) {
 func Test_Verify_nil(t *testing.T) {
 	assert.True(t, Verify(nil, nil))
 }
+
+func Test_NewCommand(t *testing.T) {
+	cmd := NewCommand("/test", []string{"--test"}, []string{"test=123"})
+	assert.NotNil(t, cmd)
+	assert.Equal(t, "/test", cmd.Path)
+	// Command.Args[0] is always set to the command name itself.
+	assert.Equal(t, []string{"/test", "--test"}, cmd.Args)
+	assert.Equal(t, []string{"test=123"}, cmd.Env)
+}
+
+// Test_CastToPrimitiveTypes tests the CastToPrimitiveTypes function.
+func Test_CastToPrimitiveTypes(t *testing.T) {
+	actual := map[string]interface{}{
+		"string":   "test",
+		"int":      123,
+		"bool":     true,
+		"map":      map[string]interface{}{"test": "test"},
+		"duration": time.Duration(123),
+		"array": []interface{}{
+			"test",
+			123,
+			true,
+			map[string]interface{}{
+				"test": "test",
+			},
+			time.Duration(123),
+		},
+	}
+	expected := map[string]interface{}{
+		"string":   "test",
+		"int":      123,
+		"bool":     true,
+		"map":      map[string]interface{}{"test": "test"},
+		"duration": "123ns", // time.Duration is casted to string.
+		"array": []interface{}{
+			"test",
+			123,
+			true,
+			map[string]interface{}{
+				"test": "test",
+			},
+			"123ns", // time.Duration is casted to string.
+		},
+	}
+
+	casted := CastToPrimitiveTypes(actual)
+	assert.Equal(t, expected, casted)
+}

From 36d68947287692d503dc185b6084424b7859dcdf Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 15:05:43 +0200
Subject: [PATCH 10/20] Use cobra to print from the commands

---
 cmd/utils.go   | 26 +++++++++++++-------------
 cmd/version.go |  6 ++----
 2 files changed, 15 insertions(+), 17 deletions(-)

diff --git a/cmd/utils.go b/cmd/utils.go
index 90cdd1c4..52129295 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -89,7 +89,7 @@ func generateConfig(
 	if exists && forceRewriteFile {
 		verb = "overwritten"
 	}
-	logger.Printf("Config file '%s' was %s successfully.", configFile, verb)
+	cmd.Printf("Config file '%s' was %s successfully.", configFile, verb)
 }
 
 func lintConfig(cmd *cobra.Command, fileType configFileType, configFile string) {
@@ -163,12 +163,10 @@ func lintConfig(cmd *cobra.Command, fileType configFileType, configFile string)
 		logger.Fatalf("Error validating %s config: %s\n", string(fileType), err)
 	}
 
-	logger.Printf("%s config is valid\n", fileType)
+	cmd.Printf("%s config is valid\n", fileType)
 }
 
 func listPlugins(cmd *cobra.Command, pluginConfigFile string, onlyEnabled bool) {
-	logger := log.New(cmd.OutOrStdout(), "", 0)
-
 	// Load the plugin config file.
 	conf := config.NewConfig(context.TODO(), "", pluginConfigFile)
 	conf.LoadDefaults(context.TODO())
@@ -176,8 +174,10 @@ func listPlugins(cmd *cobra.Command, pluginConfigFile string, onlyEnabled bool)
 	conf.UnmarshalPluginConfig(context.TODO())
 
 	if len(conf.Plugin.Plugins) != 0 {
-		logger.Printf("Total plugins: %d\n", len(conf.Plugin.Plugins))
-		logger.Println("Plugins:")
+		cmd.Printf("Total plugins: %d\n", len(conf.Plugin.Plugins))
+		cmd.Println("Plugins:")
+	} else {
+		cmd.Println("No plugins found")
 	}
 
 	// Print the list of plugins.
@@ -185,15 +185,15 @@ func listPlugins(cmd *cobra.Command, pluginConfigFile string, onlyEnabled bool)
 		if onlyEnabled && !plugin.Enabled {
 			continue
 		}
-		logger.Printf("  Name: %s\n", plugin.Name)
-		logger.Printf("  Enabled: %t\n", plugin.Enabled)
-		logger.Printf("  Path: %s\n", plugin.LocalPath)
-		logger.Printf("  Args: %s\n", strings.Join(plugin.Args, " "))
-		logger.Println("  Env:")
+		cmd.Printf("  Name: %s\n", plugin.Name)
+		cmd.Printf("  Enabled: %t\n", plugin.Enabled)
+		cmd.Printf("  Path: %s\n", plugin.LocalPath)
+		cmd.Printf("  Args: %s\n", strings.Join(plugin.Args, " "))
+		cmd.Println("  Env:")
 		for _, env := range plugin.Env {
-			logger.Printf("    %s\n", env)
+			cmd.Printf("    %s\n", env)
 		}
-		logger.Printf("  Checksum: %s\n", plugin.Checksum)
+		cmd.Printf("  Checksum: %s\n", plugin.Checksum)
 	}
 }
 
diff --git a/cmd/version.go b/cmd/version.go
index 7331f6c1..524725de 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -1,8 +1,6 @@
 package cmd
 
 import (
-	"fmt"
-
 	"github.com/gatewayd-io/gatewayd/config"
 	"github.com/spf13/cobra"
 )
@@ -11,8 +9,8 @@ import (
 var versionCmd = &cobra.Command{
 	Use:   "version",
 	Short: "Show version information",
-	Run: func(_ *cobra.Command, _ []string) {
-		fmt.Println(config.VersionInfo()) //nolint:forbidigo
+	Run: func(cmd *cobra.Command, _ []string) {
+		cmd.Println(config.VersionInfo()) //nolint:forbidigo
 	},
 }
 

From 285212fdae43ad65423f67f45c5e6da330b8bc86 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 15:06:56 +0200
Subject: [PATCH 11/20] Add tests for all commands except `run` and `plugin
 install`

---
 .golangci.yaml          |  1 +
 cmd/cmd_helpers_test.go | 20 +++++++++++++
 cmd/config_init_test.go | 36 +++++++++++++++++++++++
 cmd/config_lint_test.go | 36 +++++++++++++++++++++++
 cmd/config_test.go      | 31 ++++++++++++++++++++
 cmd/plugin_init_test.go | 25 ++++++++++++++++
 cmd/plugin_lint_test.go | 18 ++++++++++++
 cmd/plugin_list_test.go | 64 +++++++++++++++++++++++++++++++++++++++++
 cmd/plugin_test.go      | 32 +++++++++++++++++++++
 cmd/root_test.go        | 34 ++++++++++++++++++++++
 cmd/version.go          |  2 +-
 cmd/version_test.go     | 23 +++++++++++++++
 12 files changed, 321 insertions(+), 1 deletion(-)
 create mode 100644 cmd/cmd_helpers_test.go
 create mode 100644 cmd/config_init_test.go
 create mode 100644 cmd/config_lint_test.go
 create mode 100644 cmd/config_test.go
 create mode 100644 cmd/plugin_init_test.go
 create mode 100644 cmd/plugin_lint_test.go
 create mode 100644 cmd/plugin_list_test.go
 create mode 100644 cmd/plugin_test.go
 create mode 100644 cmd/root_test.go
 create mode 100644 cmd/version_test.go

diff --git a/.golangci.yaml b/.golangci.yaml
index 3fd336f2..1995214e 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -73,6 +73,7 @@ linters-settings:
           - "github.com/prometheus/client_model/go"
           - "github.com/prometheus/common/expfmt"
           - "github.com/panjf2000/gnet/v2"
+          - "github.com/spf13/cobra"
   tagalign:
     align: false
     sort: false
diff --git a/cmd/cmd_helpers_test.go b/cmd/cmd_helpers_test.go
new file mode 100644
index 00000000..e8ed00bc
--- /dev/null
+++ b/cmd/cmd_helpers_test.go
@@ -0,0 +1,20 @@
+package cmd
+
+import (
+	"bytes"
+
+	"github.com/spf13/cobra"
+)
+
+// executeCommandC executes a cobra command and returns the command, output, and error.
+// Taken from https://github.com/spf13/cobra/blob/0c72800b8dba637092b57a955ecee75949e79a73/command_test.go#L48.
+func executeCommandC(root *cobra.Command, args ...string) (string, error) {
+	buf := new(bytes.Buffer)
+	root.SetOut(buf)
+	root.SetErr(buf)
+	root.SetArgs(args)
+
+	_, err := root.ExecuteC()
+
+	return buf.String(), err
+}
diff --git a/cmd/config_init_test.go b/cmd/config_init_test.go
new file mode 100644
index 00000000..1c737590
--- /dev/null
+++ b/cmd/config_init_test.go
@@ -0,0 +1,36 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_configInitCmd(t *testing.T) {
+	// Reset globalConfigFile to avoid conflicts with other tests.
+	globalConfigFile = "./test_config.yaml"
+
+	// Test configInitCmd.
+	output, err := executeCommandC(rootCmd, "config", "init", "-c", globalConfigFile)
+	assert.NoError(t, err, "configInitCmd should not return an error")
+	assert.Equal(t,
+		fmt.Sprintf("Config file '%s' was created successfully.", globalConfigFile),
+		output,
+		"configInitCmd should print the correct output")
+	// Check that the config file was created.
+	assert.FileExists(t, globalConfigFile, "configInitCmd should create a config file")
+
+	// Test configInitCmd with the --force flag to overwrite the config file.
+	output, err = executeCommandC(rootCmd, "config", "init", "--force")
+	assert.NoError(t, err, "configInitCmd should not return an error")
+	assert.Equal(t,
+		fmt.Sprintf("Config file '%s' was overwritten successfully.", globalConfigFile),
+		output,
+		"configInitCmd should print the correct output")
+
+	// Clean up.
+	err = os.Remove(globalConfigFile)
+	assert.NoError(t, err)
+}
diff --git a/cmd/config_lint_test.go b/cmd/config_lint_test.go
new file mode 100644
index 00000000..c046925e
--- /dev/null
+++ b/cmd/config_lint_test.go
@@ -0,0 +1,36 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_configLintCmd(t *testing.T) {
+	// Reset globalConfigFile to avoid conflicts with other tests.
+	globalConfigFile = "./test_config.yaml"
+
+	// Test configInitCmd.
+	output, err := executeCommandC(rootCmd, "config", "init", "-c", globalConfigFile)
+	assert.NoError(t, err, "configInitCmd should not return an error")
+	assert.Equal(t,
+		fmt.Sprintf("Config file '%s' was created successfully.", globalConfigFile),
+		output,
+		"configInitCmd should print the correct output")
+	// Check that the config file was created.
+	assert.FileExists(t, globalConfigFile, "configInitCmd should create a config file")
+
+	// Test configLintCmd.
+	output, err = executeCommandC(rootCmd, "config", "lint", "-c", globalConfigFile)
+	assert.NoError(t, err, "configLintCmd should not return an error")
+	assert.Equal(t,
+		"global config is valid\n",
+		output,
+		"configLintCmd should print the correct output")
+
+	// Clean up.
+	err = os.Remove(globalConfigFile)
+	assert.NoError(t, err)
+}
diff --git a/cmd/config_test.go b/cmd/config_test.go
new file mode 100644
index 00000000..62870b55
--- /dev/null
+++ b/cmd/config_test.go
@@ -0,0 +1,31 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_configCmd(t *testing.T) {
+	// Test configCmd with no arguments.
+	output, err := executeCommandC(rootCmd, "config")
+	assert.NoError(t, err, "configCmd should not return an error")
+	assert.Equal(t,
+		`Manage GatewayD global configuration
+
+Usage:
+  gatewayd config [flags]
+  gatewayd config [command]
+
+Available Commands:
+  init        Create or overwrite the GatewayD global config
+  lint        Lint the GatewayD global config
+
+Flags:
+  -h, --help   help for config
+
+Use "gatewayd config [command] --help" for more information about a command.
+`,
+		output,
+		"configCmd should print the correct output")
+}
diff --git a/cmd/plugin_init_test.go b/cmd/plugin_init_test.go
new file mode 100644
index 00000000..b05a077e
--- /dev/null
+++ b/cmd/plugin_init_test.go
@@ -0,0 +1,25 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_pluginInitCmd(t *testing.T) {
+	// Test plugin init command.
+	pluginConfigFile := "./test.yaml"
+	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin init command should not have returned an error")
+	assert.Equal(t,
+		fmt.Sprintf("Config file '%s' was created successfully.", pluginConfigFile),
+		output,
+		"plugin init command should have returned the correct output")
+	assert.FileExists(t, pluginConfigFile, "plugin init command should have created a config file")
+
+	// Clean up.
+	err = os.Remove(pluginConfigFile)
+	assert.NoError(t, err)
+}
diff --git a/cmd/plugin_lint_test.go b/cmd/plugin_lint_test.go
new file mode 100644
index 00000000..88574b23
--- /dev/null
+++ b/cmd/plugin_lint_test.go
@@ -0,0 +1,18 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_pluginLintCmd(t *testing.T) {
+	// Test plugin lint command.
+	pluginConfigFile := "../gatewayd_plugins.yaml"
+	output, err := executeCommandC(rootCmd, "plugin", "lint", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin lint command should not have returned an error")
+	assert.Equal(t,
+		"plugins config is valid\n",
+		output,
+		"plugin lint command should have returned the correct output")
+}
diff --git a/cmd/plugin_list_test.go b/cmd/plugin_list_test.go
new file mode 100644
index 00000000..ea85318d
--- /dev/null
+++ b/cmd/plugin_list_test.go
@@ -0,0 +1,64 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_pluginListCmd(t *testing.T) {
+	// Test plugin list command.
+	pluginConfigFile := "./test.yaml"
+	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin init command should not have returned an error")
+	assert.Equal(t,
+		fmt.Sprintf("Config file '%s' was created successfully.", pluginConfigFile),
+		output,
+		"plugin init command should have returned the correct output")
+	assert.FileExists(t, pluginConfigFile, "plugin init command should have created a config file")
+
+	output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin list command should not have returned an error")
+	assert.Equal(t,
+		"No plugins found\n",
+		output,
+		"plugin list command should have returned empty output")
+
+	// Clean up.
+	err = os.Remove(pluginConfigFile)
+	assert.NoError(t, err)
+}
+
+func Test_pluginListCmdWithPlugins(t *testing.T) {
+	// Test plugin list command.
+	// Read the plugin config file from the root directory.
+	pluginConfigFile := "../gatewayd_plugins.yaml"
+	output, err := executeCommandC(rootCmd, "plugin", "list", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin list command should not have returned an error")
+	assert.Equal(t, `Total plugins: 1
+Plugins:
+  Name: gatewayd-plugin-cache
+  Enabled: true
+  Path: ../gatewayd-plugin-cache/gatewayd-plugin-cache
+  Args: --log-level debug
+  Env:
+    MAGIC_COOKIE_KEY=GATEWAYD_PLUGIN
+    MAGIC_COOKIE_VALUE=5712b87aa5d7e9f9e9ab643e6603181c5b796015cb1c09d6f5ada882bf2a1872
+    REDIS_URL=redis://localhost:6379/0
+    EXPIRY=1h
+    METRICS_ENABLED=True
+    METRICS_UNIX_DOMAIN_SOCKET=/tmp/gatewayd-plugin-cache.sock
+    METRICS_PATH=/metrics
+    PERIODIC_INVALIDATOR_ENABLED=True
+    PERIODIC_INVALIDATOR_INTERVAL=1m
+    PERIODIC_INVALIDATOR_START_DELAY=1m
+    API_ADDRESS=localhost:18080
+    EXIT_ON_STARTUP_ERROR=False
+    SENTRY_DSN=https://70eb1abcd32e41acbdfc17bc3407a543@o4504550475038720.ingest.sentry.io/4505342961123328
+  Checksum: 054e7dba9c1e3e3910f4928a000d35c8a6199719fad505c66527f3e9b1993833
+`,
+		output,
+		"plugin list command should have returned the correct output")
+}
diff --git a/cmd/plugin_test.go b/cmd/plugin_test.go
new file mode 100644
index 00000000..4f8233a3
--- /dev/null
+++ b/cmd/plugin_test.go
@@ -0,0 +1,32 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_pluginCmd(t *testing.T) {
+	// Test pluginCmd with no arguments.
+	output, err := executeCommandC(rootCmd, "plugin")
+	assert.NoError(t, err, "pluginCmd should not return an error")
+	assert.Equal(t, `Manage plugins and their configuration
+
+Usage:
+  gatewayd plugin [flags]
+  gatewayd plugin [command]
+
+Available Commands:
+  init        Create or overwrite the GatewayD plugins config
+  install     Install a plugin from a local archive or a GitHub repository
+  lint        Lint the GatewayD plugins config
+  list        List the GatewayD plugins
+
+Flags:
+  -h, --help   help for plugin
+
+Use "gatewayd plugin [command] --help" for more information about a command.
+`,
+		output,
+		"pluginCmd should print the correct output")
+}
diff --git a/cmd/root_test.go b/cmd/root_test.go
new file mode 100644
index 00000000..ea2ee268
--- /dev/null
+++ b/cmd/root_test.go
@@ -0,0 +1,34 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_rootCmd(t *testing.T) {
+	output, err := executeCommandC(rootCmd)
+	assert.NoError(t, err, "rootCmd should not return an error")
+	//nolint:lll
+	assert.Equal(t,
+		`GatewayD is a cloud-native database gateway and framework for building data-driven applications. It sits between your database servers and clients and proxies all their communication.
+
+Usage:
+  gatewayd [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  config      Manage GatewayD global configuration
+  help        Help about any command
+  plugin      Manage plugins and their configuration
+  run         Run a GatewayD instance
+  version     Show version information
+
+Flags:
+  -h, --help   help for gatewayd
+
+Use "gatewayd [command] --help" for more information about a command.
+`,
+		output,
+		"rootCmd should print the correct output")
+}
diff --git a/cmd/version.go b/cmd/version.go
index 524725de..94c44cba 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -10,7 +10,7 @@ var versionCmd = &cobra.Command{
 	Use:   "version",
 	Short: "Show version information",
 	Run: func(cmd *cobra.Command, _ []string) {
-		cmd.Println(config.VersionInfo()) //nolint:forbidigo
+		cmd.Println(config.VersionInfo())
 	},
 }
 
diff --git a/cmd/version_test.go b/cmd/version_test.go
new file mode 100644
index 00000000..c648cad2
--- /dev/null
+++ b/cmd/version_test.go
@@ -0,0 +1,23 @@
+package cmd
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/gatewayd-io/gatewayd/config"
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_versionCmd(t *testing.T) {
+	// Test versionCmd with no arguments.
+	config.Version = "SEMVER"
+	config.VersionDetails = "COMMIT-HASH"
+	output, err := executeCommandC(rootCmd, "version")
+	assert.NoError(t, err, "versionCmd should not return an error")
+	assert.Regexp(t,
+		// The regexp matches something like the following output:
+		// GatewayD v0.7.7 (2023-09-16T19:27:38+0000/038f75b, go1.21.0, linux/amd64)
+		regexp.MustCompile(`^GatewayD SEMVER \(COMMIT-HASH, go\d+\.\d+\.\d+, \w+/\w+\)\n$`),
+		output,
+		"versionCmd should print the correct output")
+}

From a60f3c3ca0491dc8ef20c47877309d23f4a21633 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 16:04:21 +0200
Subject: [PATCH 12/20] Move prints out of downloadFile function

Use command to print
---
 cmd/plugin_install.go | 14 +++++++++-----
 cmd/utils.go          |  4 ----
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/cmd/plugin_install.go b/cmd/plugin_install.go
index 548289e3..3cc56eb5 100644
--- a/cmd/plugin_install.go
+++ b/cmd/plugin_install.go
@@ -85,7 +85,7 @@ var pluginInstallCmd = &cobra.Command{
 			splittedURL := strings.Split(args[0], "@")
 			// If the version is not specified, use the latest version.
 			if len(splittedURL) < NumParts {
-				log.Println("Version not specified. Using latest version")
+				cmd.Println("Version not specified. Using latest version")
 			}
 			if len(splittedURL) >= NumParts {
 				pluginVersion = splittedURL[1]
@@ -138,7 +138,9 @@ var pluginInstallCmd = &cobra.Command{
 					strings.Contains(name, archiveExt)
 			})
 			if downloadURL != "" && releaseID != 0 {
+				cmd.Println("Downloading", downloadURL)
 				downloadFile(client, account, pluginName, downloadURL, releaseID, pluginFilename)
+				cmd.Println("Download completed successfully")
 			} else {
 				log.Panic("The plugin file could not be found in the release assets")
 			}
@@ -148,7 +150,9 @@ var pluginInstallCmd = &cobra.Command{
 				return strings.Contains(name, "checksums.txt")
 			})
 			if checksumsFilename != "" && downloadURL != "" && releaseID != 0 {
+				cmd.Println("Downloading", downloadURL)
 				downloadFile(client, account, pluginName, downloadURL, releaseID, checksumsFilename)
+				cmd.Println("Download completed successfully")
 			} else {
 				log.Panic("The checksum file could not be found in the release assets")
 			}
@@ -174,13 +178,13 @@ var pluginInstallCmd = &cobra.Command{
 						log.Panic("Checksum verification failed")
 					}
 
-					log.Println("Checksum verification passed")
+					cmd.Println("Checksum verification passed")
 					break
 				}
 			}
 
 			if pullOnly {
-				log.Println("Plugin binary downloaded to", pluginFilename)
+				cmd.Println("Plugin binary downloaded to", pluginFilename)
 				return
 			}
 		} else {
@@ -204,7 +208,7 @@ var pluginInstallCmd = &cobra.Command{
 		pluginFileSum := ""
 		for _, filename := range filenames {
 			if strings.Contains(filename, pluginName) {
-				log.Println("Plugin binary extracted to", filename)
+				cmd.Println("Plugin binary extracted to", filename)
 				localPath = filename
 				// Get the checksum for the extracted plugin binary.
 				// TODO: Should we verify the checksum using the checksum.txt file instead?
@@ -303,7 +307,7 @@ var pluginInstallCmd = &cobra.Command{
 		}
 
 		// TODO: Add a rollback mechanism.
-		log.Println("Plugin installed successfully")
+		cmd.Println("Plugin installed successfully")
 	},
 }
 
diff --git a/cmd/utils.go b/cmd/utils.go
index 52129295..ea4d4598 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -393,8 +393,6 @@ func downloadFile(
 	client *github.Client, account, pluginName, downloadURL string,
 	releaseID int64, filename string,
 ) {
-	log.Println("Downloading", downloadURL)
-
 	// Download the plugin.
 	readCloser, redirectURL, err := client.Repositories.DownloadReleaseAsset(
 		context.Background(), account, pluginName, releaseID, http.DefaultClient)
@@ -447,6 +445,4 @@ func downloadFile(
 	if err != nil {
 		log.Panic("There was an error downloading the plugin: ", err)
 	}
-
-	log.Println("Download completed successfully")
 }

From 9b781a7cf1d0f336658b946ac4198e1689f482e8 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 16:29:09 +0200
Subject: [PATCH 13/20] Add test for plugin install command from GitHub

---
 cmd/plugin_install_test.go | 45 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)
 create mode 100644 cmd/plugin_install_test.go

diff --git a/cmd/plugin_install_test.go b/cmd/plugin_install_test.go
new file mode 100644
index 00000000..d8e97a63
--- /dev/null
+++ b/cmd/plugin_install_test.go
@@ -0,0 +1,45 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_pluginInstallCmd(t *testing.T) {
+	// Create a test config file.
+	pluginConfigFile := "./test.yaml"
+	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin init should not return an error")
+	assert.Equal(t,
+		fmt.Sprintf("Config file '%s' was created successfully.", pluginConfigFile),
+		output,
+		"plugin init command should have returned the correct output")
+	assert.FileExists(t, pluginConfigFile, "plugin init command should have created a config file")
+
+	// Test plugin install command.
+	output, err = executeCommandC(
+		rootCmd, "plugin", "install",
+		"github.com/gatewayd-io/gatewayd-plugin-cache@v0.2.4", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin install should not return an error")
+	fmt.Println(output)
+	assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz") //nolint:lll
+	assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/checksums.txt")                                   //nolint:lll
+	assert.Contains(t, output, "Download completed successfully")
+	assert.Contains(t, output, "Checksum verification passed")
+	assert.Contains(t, output, "Plugin binary extracted to plugins/gatewayd-plugin-cache")
+	assert.Contains(t, output, "Plugin installed successfully")
+
+	// See if the plugin was actually installed.
+	output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginConfigFile)
+	assert.NoError(t, err, "plugin list should not return an error")
+	assert.Contains(t, output, "Name: gatewayd-plugin-cache")
+
+	// Clean up.
+	assert.NoError(t, os.RemoveAll("plugins/"))
+	assert.NoError(t, os.Remove("checksums.txt"))
+	assert.NoError(t, os.Remove("gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz"))
+	assert.NoError(t, os.Remove(pluginConfigFile))
+}

From 43948071fdc500abb2f6054edd39ba2d4f522269 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 16:36:59 +0200
Subject: [PATCH 14/20] Fix linter errors

---
 cmd/cmd_helpers_test.go    |  2 ++
 cmd/plugin_init_test.go    |  9 ++++-----
 cmd/plugin_install.go      |  4 ++--
 cmd/plugin_install_test.go | 14 ++++++--------
 cmd/plugin_lint_test.go    |  3 +--
 cmd/plugin_list_test.go    | 15 +++++++--------
 cmd/utils.go               |  3 +--
 7 files changed, 23 insertions(+), 27 deletions(-)

diff --git a/cmd/cmd_helpers_test.go b/cmd/cmd_helpers_test.go
index e8ed00bc..5e33166a 100644
--- a/cmd/cmd_helpers_test.go
+++ b/cmd/cmd_helpers_test.go
@@ -6,6 +6,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
+var pluginTestConfigFile = "./test.yaml"
+
 // executeCommandC executes a cobra command and returns the command, output, and error.
 // Taken from https://github.com/spf13/cobra/blob/0c72800b8dba637092b57a955ecee75949e79a73/command_test.go#L48.
 func executeCommandC(root *cobra.Command, args ...string) (string, error) {
diff --git a/cmd/plugin_init_test.go b/cmd/plugin_init_test.go
index b05a077e..9d06df61 100644
--- a/cmd/plugin_init_test.go
+++ b/cmd/plugin_init_test.go
@@ -10,16 +10,15 @@ import (
 
 func Test_pluginInitCmd(t *testing.T) {
 	// Test plugin init command.
-	pluginConfigFile := "./test.yaml"
-	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginConfigFile)
+	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin init command should not have returned an error")
 	assert.Equal(t,
-		fmt.Sprintf("Config file '%s' was created successfully.", pluginConfigFile),
+		fmt.Sprintf("Config file '%s' was created successfully.", pluginTestConfigFile),
 		output,
 		"plugin init command should have returned the correct output")
-	assert.FileExists(t, pluginConfigFile, "plugin init command should have created a config file")
+	assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file")
 
 	// Clean up.
-	err = os.Remove(pluginConfigFile)
+	err = os.Remove(pluginTestConfigFile)
 	assert.NoError(t, err)
 }
diff --git a/cmd/plugin_install.go b/cmd/plugin_install.go
index 3cc56eb5..9ec7dac4 100644
--- a/cmd/plugin_install.go
+++ b/cmd/plugin_install.go
@@ -139,7 +139,7 @@ var pluginInstallCmd = &cobra.Command{
 			})
 			if downloadURL != "" && releaseID != 0 {
 				cmd.Println("Downloading", downloadURL)
-				downloadFile(client, account, pluginName, downloadURL, releaseID, pluginFilename)
+				downloadFile(client, account, pluginName, releaseID, pluginFilename)
 				cmd.Println("Download completed successfully")
 			} else {
 				log.Panic("The plugin file could not be found in the release assets")
@@ -151,7 +151,7 @@ var pluginInstallCmd = &cobra.Command{
 			})
 			if checksumsFilename != "" && downloadURL != "" && releaseID != 0 {
 				cmd.Println("Downloading", downloadURL)
-				downloadFile(client, account, pluginName, downloadURL, releaseID, checksumsFilename)
+				downloadFile(client, account, pluginName, releaseID, checksumsFilename)
 				cmd.Println("Download completed successfully")
 			} else {
 				log.Panic("The checksum file could not be found in the release assets")
diff --git a/cmd/plugin_install_test.go b/cmd/plugin_install_test.go
index d8e97a63..a54efff0 100644
--- a/cmd/plugin_install_test.go
+++ b/cmd/plugin_install_test.go
@@ -10,21 +10,19 @@ import (
 
 func Test_pluginInstallCmd(t *testing.T) {
 	// Create a test config file.
-	pluginConfigFile := "./test.yaml"
-	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginConfigFile)
+	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin init should not return an error")
 	assert.Equal(t,
-		fmt.Sprintf("Config file '%s' was created successfully.", pluginConfigFile),
+		fmt.Sprintf("Config file '%s' was created successfully.", pluginTestConfigFile),
 		output,
 		"plugin init command should have returned the correct output")
-	assert.FileExists(t, pluginConfigFile, "plugin init command should have created a config file")
+	assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file")
 
 	// Test plugin install command.
 	output, err = executeCommandC(
 		rootCmd, "plugin", "install",
-		"github.com/gatewayd-io/gatewayd-plugin-cache@v0.2.4", "-p", pluginConfigFile)
+		"github.com/gatewayd-io/gatewayd-plugin-cache@v0.2.4", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin install should not return an error")
-	fmt.Println(output)
 	assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz") //nolint:lll
 	assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/checksums.txt")                                   //nolint:lll
 	assert.Contains(t, output, "Download completed successfully")
@@ -33,7 +31,7 @@ func Test_pluginInstallCmd(t *testing.T) {
 	assert.Contains(t, output, "Plugin installed successfully")
 
 	// See if the plugin was actually installed.
-	output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginConfigFile)
+	output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin list should not return an error")
 	assert.Contains(t, output, "Name: gatewayd-plugin-cache")
 
@@ -41,5 +39,5 @@ func Test_pluginInstallCmd(t *testing.T) {
 	assert.NoError(t, os.RemoveAll("plugins/"))
 	assert.NoError(t, os.Remove("checksums.txt"))
 	assert.NoError(t, os.Remove("gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz"))
-	assert.NoError(t, os.Remove(pluginConfigFile))
+	assert.NoError(t, os.Remove(pluginTestConfigFile))
 }
diff --git a/cmd/plugin_lint_test.go b/cmd/plugin_lint_test.go
index 88574b23..07dd57cf 100644
--- a/cmd/plugin_lint_test.go
+++ b/cmd/plugin_lint_test.go
@@ -8,8 +8,7 @@ import (
 
 func Test_pluginLintCmd(t *testing.T) {
 	// Test plugin lint command.
-	pluginConfigFile := "../gatewayd_plugins.yaml"
-	output, err := executeCommandC(rootCmd, "plugin", "lint", "-p", pluginConfigFile)
+	output, err := executeCommandC(rootCmd, "plugin", "lint", "-p", "../gatewayd_plugins.yaml")
 	assert.NoError(t, err, "plugin lint command should not have returned an error")
 	assert.Equal(t,
 		"plugins config is valid\n",
diff --git a/cmd/plugin_list_test.go b/cmd/plugin_list_test.go
index ea85318d..8b53dc53 100644
--- a/cmd/plugin_list_test.go
+++ b/cmd/plugin_list_test.go
@@ -10,16 +10,15 @@ import (
 
 func Test_pluginListCmd(t *testing.T) {
 	// Test plugin list command.
-	pluginConfigFile := "./test.yaml"
-	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginConfigFile)
+	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin init command should not have returned an error")
 	assert.Equal(t,
-		fmt.Sprintf("Config file '%s' was created successfully.", pluginConfigFile),
+		fmt.Sprintf("Config file '%s' was created successfully.", pluginTestConfigFile),
 		output,
 		"plugin init command should have returned the correct output")
-	assert.FileExists(t, pluginConfigFile, "plugin init command should have created a config file")
+	assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file")
 
-	output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginConfigFile)
+	output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin list command should not have returned an error")
 	assert.Equal(t,
 		"No plugins found\n",
@@ -27,15 +26,15 @@ func Test_pluginListCmd(t *testing.T) {
 		"plugin list command should have returned empty output")
 
 	// Clean up.
-	err = os.Remove(pluginConfigFile)
+	err = os.Remove(pluginTestConfigFile)
 	assert.NoError(t, err)
 }
 
 func Test_pluginListCmdWithPlugins(t *testing.T) {
 	// Test plugin list command.
 	// Read the plugin config file from the root directory.
-	pluginConfigFile := "../gatewayd_plugins.yaml"
-	output, err := executeCommandC(rootCmd, "plugin", "list", "-p", pluginConfigFile)
+	pluginTestConfigFile := "../gatewayd_plugins.yaml"
+	output, err := executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin list command should not have returned an error")
 	assert.Equal(t, `Total plugins: 1
 Plugins:
diff --git a/cmd/utils.go b/cmd/utils.go
index ea4d4598..05e459b8 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -390,8 +390,7 @@ func findAsset(release *github.RepositoryRelease, match func(string) bool) (stri
 }
 
 func downloadFile(
-	client *github.Client, account, pluginName, downloadURL string,
-	releaseID int64, filename string,
+	client *github.Client, account, pluginName string, releaseID int64, filename string,
 ) {
 	// Download the plugin.
 	readCloser, redirectURL, err := client.Repositories.DownloadReleaseAsset(

From 90e40cbc9b147ae556e0fe162281e7f425305cb3 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Sun, 17 Sep 2023 17:06:13 +0200
Subject: [PATCH 15/20] Make config filenames constant

---
 cmd/cmd_helpers_test.go    |  5 ++++-
 cmd/config_init_test.go    | 13 +++++--------
 cmd/config_lint_test.go    | 13 +++++--------
 cmd/plugin_install_test.go |  2 +-
 4 files changed, 15 insertions(+), 18 deletions(-)

diff --git a/cmd/cmd_helpers_test.go b/cmd/cmd_helpers_test.go
index 5e33166a..3f1699ef 100644
--- a/cmd/cmd_helpers_test.go
+++ b/cmd/cmd_helpers_test.go
@@ -6,7 +6,10 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var pluginTestConfigFile = "./test.yaml"
+var (
+	globalTestConfigFile = "./test_global.yaml"
+	pluginTestConfigFile = "./test_plugins.yaml"
+)
 
 // executeCommandC executes a cobra command and returns the command, output, and error.
 // Taken from https://github.com/spf13/cobra/blob/0c72800b8dba637092b57a955ecee75949e79a73/command_test.go#L48.
diff --git a/cmd/config_init_test.go b/cmd/config_init_test.go
index 1c737590..8c220cb2 100644
--- a/cmd/config_init_test.go
+++ b/cmd/config_init_test.go
@@ -9,28 +9,25 @@ import (
 )
 
 func Test_configInitCmd(t *testing.T) {
-	// Reset globalConfigFile to avoid conflicts with other tests.
-	globalConfigFile = "./test_config.yaml"
-
 	// Test configInitCmd.
-	output, err := executeCommandC(rootCmd, "config", "init", "-c", globalConfigFile)
+	output, err := executeCommandC(rootCmd, "config", "init", "-c", globalTestConfigFile)
 	assert.NoError(t, err, "configInitCmd should not return an error")
 	assert.Equal(t,
-		fmt.Sprintf("Config file '%s' was created successfully.", globalConfigFile),
+		fmt.Sprintf("Config file '%s' was created successfully.", globalTestConfigFile),
 		output,
 		"configInitCmd should print the correct output")
 	// Check that the config file was created.
-	assert.FileExists(t, globalConfigFile, "configInitCmd should create a config file")
+	assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file")
 
 	// Test configInitCmd with the --force flag to overwrite the config file.
 	output, err = executeCommandC(rootCmd, "config", "init", "--force")
 	assert.NoError(t, err, "configInitCmd should not return an error")
 	assert.Equal(t,
-		fmt.Sprintf("Config file '%s' was overwritten successfully.", globalConfigFile),
+		fmt.Sprintf("Config file '%s' was overwritten successfully.", globalTestConfigFile),
 		output,
 		"configInitCmd should print the correct output")
 
 	// Clean up.
-	err = os.Remove(globalConfigFile)
+	err = os.Remove(globalTestConfigFile)
 	assert.NoError(t, err)
 }
diff --git a/cmd/config_lint_test.go b/cmd/config_lint_test.go
index c046925e..82cf9a1a 100644
--- a/cmd/config_lint_test.go
+++ b/cmd/config_lint_test.go
@@ -9,21 +9,18 @@ import (
 )
 
 func Test_configLintCmd(t *testing.T) {
-	// Reset globalConfigFile to avoid conflicts with other tests.
-	globalConfigFile = "./test_config.yaml"
-
 	// Test configInitCmd.
-	output, err := executeCommandC(rootCmd, "config", "init", "-c", globalConfigFile)
+	output, err := executeCommandC(rootCmd, "config", "init", "-c", globalTestConfigFile)
 	assert.NoError(t, err, "configInitCmd should not return an error")
 	assert.Equal(t,
-		fmt.Sprintf("Config file '%s' was created successfully.", globalConfigFile),
+		fmt.Sprintf("Config file '%s' was created successfully.", globalTestConfigFile),
 		output,
 		"configInitCmd should print the correct output")
 	// Check that the config file was created.
-	assert.FileExists(t, globalConfigFile, "configInitCmd should create a config file")
+	assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file")
 
 	// Test configLintCmd.
-	output, err = executeCommandC(rootCmd, "config", "lint", "-c", globalConfigFile)
+	output, err = executeCommandC(rootCmd, "config", "lint", "-c", globalTestConfigFile)
 	assert.NoError(t, err, "configLintCmd should not return an error")
 	assert.Equal(t,
 		"global config is valid\n",
@@ -31,6 +28,6 @@ func Test_configLintCmd(t *testing.T) {
 		"configLintCmd should print the correct output")
 
 	// Clean up.
-	err = os.Remove(globalConfigFile)
+	err = os.Remove(globalTestConfigFile)
 	assert.NoError(t, err)
 }
diff --git a/cmd/plugin_install_test.go b/cmd/plugin_install_test.go
index a54efff0..0f931aa0 100644
--- a/cmd/plugin_install_test.go
+++ b/cmd/plugin_install_test.go
@@ -9,7 +9,7 @@ import (
 )
 
 func Test_pluginInstallCmd(t *testing.T) {
-	// Create a test config file.
+	// Create a test plugin config file.
 	output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginTestConfigFile)
 	assert.NoError(t, err, "plugin init should not return an error")
 	assert.Equal(t,

From 94ca796b76e1088bc8b186d3b8f68e7f27136f41 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Mon, 18 Sep 2023 20:58:05 +0200
Subject: [PATCH 16/20] Extract gracefull shutdown piece into a separate
 function

Use a stop channel to stop GatewayD
---
 cmd/run.go | 107 +++++++++++++++++++++++++++++++++++------------------
 1 file changed, 72 insertions(+), 35 deletions(-)

diff --git a/cmd/run.go b/cmd/run.go
index 188b19d5..3aead864 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -60,8 +60,71 @@ var (
 	proxies              = make(map[string]*network.Proxy)
 	servers              = make(map[string]*network.Server)
 	healthCheckScheduler = gocron.NewScheduler(time.UTC)
+
+	stopChan = make(chan struct{})
 )
 
+func StopGracefully(
+	runCtx context.Context,
+	pluginTimeoutCtx context.Context,
+	sig os.Signal,
+	metricsMerger *metrics.Merger,
+	pluginRegistry *plugin.Registry,
+	logger zerolog.Logger,
+	servers map[string]*network.Server,
+	stopChan chan struct{},
+) {
+	_, span := otel.Tracer(config.TracerName).Start(runCtx, "Shutdown server")
+	signal := "unknown"
+	if sig != nil {
+		signal = sig.String()
+	}
+
+	logger.Info().Msg("Notifying the plugins that the server is shutting down")
+	if pluginRegistry != nil {
+		_, err := pluginRegistry.Run(
+			pluginTimeoutCtx,
+			map[string]interface{}{"signal": signal},
+			v1.HookName_HOOK_NAME_ON_SIGNAL,
+		)
+		if err != nil {
+			logger.Error().Err(err).Msg("Failed to run OnSignal hooks")
+			span.RecordError(err)
+		}
+	}
+
+	logger.Info().Msg("Stopping GatewayD")
+	span.AddEvent("Stopping GatewayD", trace.WithAttributes(
+		attribute.String("signal", signal),
+	))
+	if healthCheckScheduler != nil {
+		healthCheckScheduler.Clear()
+		logger.Info().Msg("Stopped health check scheduler")
+		span.AddEvent("Stopped health check scheduler")
+	}
+	if metricsMerger != nil {
+		metricsMerger.Stop()
+		logger.Info().Msg("Stopped metrics merger")
+		span.AddEvent("Stopped metrics merger")
+	}
+	for name, server := range servers {
+		logger.Info().Str("name", name).Msg("Stopping server")
+		server.Shutdown()
+		span.AddEvent("Stopped server")
+	}
+	logger.Info().Msg("Stopped all servers")
+	if pluginRegistry != nil {
+		pluginRegistry.Shutdown()
+		logger.Info().Msg("Stopped plugin registry")
+		span.AddEvent("Stopped plugin registry")
+	}
+	span.End()
+
+	// Close the stop channel to notify the other goroutines to stop.
+	stopChan <- struct{}{}
+	close(stopChan)
+}
+
 // runCmd represents the run command.
 var runCmd = &cobra.Command{
 	Use:   "run",
@@ -632,42 +695,16 @@ var runCmd = &cobra.Command{
 			for sig := range signalsCh {
 				for _, s := range signals {
 					if sig != s {
-						_, span := otel.Tracer(config.TracerName).Start(runCtx, "Shutdown server")
-
-						logger.Info().Msg("Notifying the plugins that the server is shutting down")
-						_, err := pluginRegistry.Run(
+						StopGracefully(
+							runCtx,
 							pluginTimeoutCtx,
-							map[string]interface{}{"signal": sig.String()},
-							v1.HookName_HOOK_NAME_ON_SIGNAL,
+							sig,
+							metricsMerger,
+							pluginRegistry,
+							logger,
+							servers,
+							stopChan,
 						)
-						if err != nil {
-							logger.Error().Err(err).Msg("Failed to run OnSignal hooks")
-							span.RecordError(err)
-						}
-
-						logger.Info().Msg("Stopping GatewayD")
-						span.AddEvent("Stopping GatewayD", trace.WithAttributes(
-							attribute.String("signal", sig.String()),
-						))
-						healthCheckScheduler.Clear()
-						logger.Info().Msg("Stopped health check scheduler")
-						span.AddEvent("Stopped health check scheduler")
-						if metricsMerger != nil {
-							metricsMerger.Stop()
-							logger.Info().Msg("Stopped metrics merger")
-							span.AddEvent("Stopped metrics merger")
-						}
-						for name, server := range servers {
-							logger.Info().Str("name", name).Msg("Stopping server")
-							server.Shutdown()
-							span.AddEvent("Stopped server")
-						}
-						logger.Info().Msg("Stopped all servers")
-						pluginRegistry.Shutdown()
-						logger.Info().Msg("Stopped plugin registry")
-						span.AddEvent("Stopped plugin registry")
-
-						span.End()
 						os.Exit(0)
 					}
 				}
@@ -703,7 +740,7 @@ var runCmd = &cobra.Command{
 		span.End()
 
 		// Wait for the server to shutdown.
-		<-make(chan struct{})
+		<-stopChan
 	},
 }
 

From 6764208d5adc006c4dc06cb710b1017f74b168db Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Mon, 18 Sep 2023 20:58:52 +0200
Subject: [PATCH 17/20] Add test for run command

---
 cmd/run.go      |  2 +-
 cmd/run_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 73 insertions(+), 1 deletion(-)
 create mode 100644 cmd/run_test.go

diff --git a/cmd/run.go b/cmd/run.go
index 3aead864..57536d0c 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -109,7 +109,7 @@ func StopGracefully(
 	}
 	for name, server := range servers {
 		logger.Info().Str("name", name).Msg("Stopping server")
-		server.Shutdown()
+		server.Shutdown() //nolint:contextcheck
 		span.AddEvent("Stopped server")
 	}
 	logger.Info().Msg("Stopped all servers")
diff --git a/cmd/run_test.go b/cmd/run_test.go
new file mode 100644
index 00000000..4f8b6dee
--- /dev/null
+++ b/cmd/run_test.go
@@ -0,0 +1,72 @@
+package cmd
+
+import (
+	"os"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/gatewayd-io/gatewayd/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/zenizh/go-capturer"
+)
+
+func Test_runCmd(t *testing.T) {
+	// Create a test plugins config file.
+	_, err := executeCommandC(rootCmd, "plugin", "init", "--force", "-p", pluginTestConfigFile)
+	assert.NoError(t, err, "plugin init command should not have returned an error")
+	assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file")
+
+	// Create a test config file.
+	_, err = executeCommandC(rootCmd, "config", "init", "--force", "-c", globalTestConfigFile)
+	assert.NoError(t, err, "configInitCmd should not return an error")
+	// Check that the config file was created.
+	assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file")
+
+	var waitGroup sync.WaitGroup
+	waitGroup.Add(1)
+	go func(waitGroup *sync.WaitGroup) {
+		time.Sleep(100 * time.Millisecond)
+
+		StopGracefully(
+			runCmd.Context(),
+			runCmd.Context(),
+			nil,
+			nil,
+			nil,
+			loggers[config.Default],
+			servers,
+			stopChan,
+		)
+
+		waitGroup.Done()
+	}(&waitGroup)
+
+	waitGroup.Add(1)
+	go func(waitGroup *sync.WaitGroup) {
+		// Test run command.
+		output := capturer.CaptureOutput(func() {
+			_, err := executeCommandC(rootCmd, "run", "-c", globalTestConfigFile, "-p", pluginTestConfigFile)
+			assert.NoError(t, err, "run command should not have returned an error")
+		})
+		// Print the output for debugging purposes.
+		runCmd.Print(output)
+		// Check if GatewayD started and stopped correctly.
+		assert.Contains(t,
+			output,
+			"GatewayD is running",
+			"run command should have returned the correct output")
+		assert.Contains(t,
+			output,
+			"Stopped all servers\n",
+			"run command should have returned the correct output")
+
+		waitGroup.Done()
+	}(&waitGroup)
+
+	waitGroup.Wait()
+
+	// Clean up.
+	assert.NoError(t, os.Remove(pluginTestConfigFile))
+	assert.NoError(t, os.Remove(globalTestConfigFile))
+}

From 92c2e563cf1ec17834b72e745ad745572c912876 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Mon, 18 Sep 2023 23:38:19 +0200
Subject: [PATCH 18/20] Check if the metrics server is already running before
 registering the handler, as it causes errors

---
 cmd/run.go | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/cmd/run.go b/cmd/run.go
index 57536d0c..b06cd260 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -352,7 +352,13 @@ var runCmd = &cobra.Command{
 			if conf.Plugin.EnableMetricsMerger && metricsMerger != nil {
 				handler = mergedMetricsHandler(handler)
 			}
-			http.Handle(metricsConfig.Path, gziphandler.GzipHandler(handler))
+			// Check if the metrics server is already running before registering the handler.
+			if _, err = http.Get(address); err != nil {
+				http.Handle(metricsConfig.Path, gziphandler.GzipHandler(handler))
+			} else {
+				logger.Warn().Msg("Metrics server is already running, consider changing the port")
+				span.RecordError(err)
+			}
 
 			//nolint:gosec
 			if err = http.ListenAndServe(

From c1366bcb76913f18ecff071a7f7a39be0c1a7379 Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Mon, 18 Sep 2023 23:39:04 +0200
Subject: [PATCH 19/20] Test run command by loading the cache plugin

---
 cmd/run.go      |  2 ++
 cmd/run_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+)

diff --git a/cmd/run.go b/cmd/run.go
index b06cd260..14ae6192 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -41,6 +41,8 @@ import (
 	"google.golang.org/grpc/credentials"
 )
 
+// TODO: Get rid of the global variables.
+// https://github.com/gatewayd-io/gatewayd/issues/324
 var (
 	enableTracing     bool
 	collectorURL      string
diff --git a/cmd/run_test.go b/cmd/run_test.go
index 4f8b6dee..fd7ef8f0 100644
--- a/cmd/run_test.go
+++ b/cmd/run_test.go
@@ -70,3 +70,87 @@ func Test_runCmd(t *testing.T) {
 	assert.NoError(t, os.Remove(pluginTestConfigFile))
 	assert.NoError(t, os.Remove(globalTestConfigFile))
 }
+
+func Test_runCmdWithCachePlugin(t *testing.T) {
+	// TODO: Remove this once these global variables are removed from cmd/run.go.
+	// https://github.com/gatewayd-io/gatewayd/issues/324
+	stopChan = make(chan struct{})
+
+	// Create a test plugins config file.
+	_, err := executeCommandC(rootCmd, "plugin", "init", "--force", "-p", pluginTestConfigFile)
+	assert.NoError(t, err, "plugin init command should not have returned an error")
+	assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file")
+
+	// Create a test config file.
+	_, err = executeCommandC(rootCmd, "config", "init", "--force", "-c", globalTestConfigFile)
+	assert.NoError(t, err, "configInitCmd should not return an error")
+	// Check that the config file was created.
+	assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file")
+
+	// Test plugin install command.
+	output, err := executeCommandC(
+		rootCmd, "plugin", "install",
+		"github.com/gatewayd-io/gatewayd-plugin-cache@v0.2.4", "-p", pluginTestConfigFile)
+	assert.NoError(t, err, "plugin install should not return an error")
+	assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz") //nolint:lll
+	assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/checksums.txt")                                   //nolint:lll
+	assert.Contains(t, output, "Download completed successfully")
+	assert.Contains(t, output, "Checksum verification passed")
+	assert.Contains(t, output, "Plugin binary extracted to plugins/gatewayd-plugin-cache")
+	assert.Contains(t, output, "Plugin installed successfully")
+
+	// See if the plugin was actually installed.
+	output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile)
+	assert.NoError(t, err, "plugin list should not return an error")
+	assert.Contains(t, output, "Name: gatewayd-plugin-cache")
+
+	var waitGroup sync.WaitGroup
+	waitGroup.Add(1)
+	go func(waitGroup *sync.WaitGroup) {
+		time.Sleep(500 * time.Millisecond)
+
+		StopGracefully(
+			runCmd.Context(),
+			runCmd.Context(),
+			nil,
+			nil,
+			nil,
+			loggers[config.Default],
+			servers,
+			stopChan,
+		)
+
+		waitGroup.Done()
+	}(&waitGroup)
+
+	waitGroup.Add(1)
+	go func(waitGroup *sync.WaitGroup) {
+		// Test run command.
+		output := capturer.CaptureOutput(func() {
+			_, err := executeCommandC(rootCmd, "run", "-c", globalTestConfigFile, "-p", pluginTestConfigFile)
+			assert.NoError(t, err, "run command should not have returned an error")
+		})
+		// Print the output for debugging purposes.
+		runCmd.Print(output)
+		// Check if GatewayD started and stopped correctly.
+		assert.Contains(t,
+			output,
+			"GatewayD is running",
+			"run command should have returned the correct output")
+		assert.Contains(t,
+			output,
+			"Stopped all servers\n",
+			"run command should have returned the correct output")
+
+		waitGroup.Done()
+	}(&waitGroup)
+
+	waitGroup.Wait()
+
+	// Clean up.
+	assert.NoError(t, os.RemoveAll("plugins/"))
+	assert.NoError(t, os.Remove("checksums.txt"))
+	assert.NoError(t, os.Remove("gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz"))
+	assert.NoError(t, os.Remove(pluginTestConfigFile))
+	assert.NoError(t, os.Remove(globalTestConfigFile))
+}

From e6b2c0ded3df4941317421c790dad20893ac6ccc Mon Sep 17 00:00:00 2001
From: Mostafa Moradian <mostafa@gatewayd.io>
Date: Mon, 18 Sep 2023 23:43:24 +0200
Subject: [PATCH 20/20] Suppress gosec G107 warning, since the response body is
 ignored

---
 cmd/run.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/run.go b/cmd/run.go
index 14ae6192..42de63da 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -355,7 +355,7 @@ var runCmd = &cobra.Command{
 				handler = mergedMetricsHandler(handler)
 			}
 			// Check if the metrics server is already running before registering the handler.
-			if _, err = http.Get(address); err != nil {
+			if _, err = http.Get(address); err != nil { //nolint:gosec
 				http.Handle(metricsConfig.Path, gziphandler.GzipHandler(handler))
 			} else {
 				logger.Warn().Msg("Metrics server is already running, consider changing the port")